mirror of
https://github.com/wassname/talk.git
synced 2026-06-28 03:19:52 +08:00
5c863f1307
* Repair theming and CSS variables for the rebrand This separates the CSS variables into three sections for admin, stream, as well as a shared set between them. This also adds injection of the raw colours from the design team into post css variables. Lastly, this re-maps the existing theme variables to a new consolidated set with fallbacks for compatibility. Since this is a singular commit from various WIP commits, I am crediting @cvle as well as myself for this work. Thank you all who helped make this rebrand with theming possible! CORL-1155 Co-authored-by: Chi Vinh Le <vinh@vinh.tech> * Convert old css variables in CallOut * Rename postive/negative => success/error CORL-1155 * Change some CSS Variable names * fix TextField/PasswordField colors Co-authored-by: Chi Vinh Le <vinh@vinh.tech>
307 lines
8.2 KiB
TypeScript
307 lines
8.2 KiB
TypeScript
import { stripIndent } from "common-tags";
|
|
import fs from "fs";
|
|
import { kebabCase, trim } from "lodash";
|
|
import path from "path";
|
|
import ts from "typescript";
|
|
|
|
import colors from "../src/core/client/ui/theme/colors";
|
|
|
|
/** We collect inforamtion from the AST and put it into DocEntry */
|
|
interface DocEntry {
|
|
/** Name of property */
|
|
key: string;
|
|
/** Documentation if available */
|
|
docs?: string;
|
|
/** Value if it is a leaf */
|
|
value?: string;
|
|
/** Children if it is a node */
|
|
children?: DocEntry[];
|
|
}
|
|
|
|
/**
|
|
* We use this regexp to find a previous block that we
|
|
* are going to update in the readme file.
|
|
*/
|
|
const BLOCK_REGEXP = /<!-- START docs:css-variables -->(.|\n)*<!-- END docs:css-variables -->/gm;
|
|
|
|
/** Sort doc entries will sort childrenless entries first */
|
|
function sortDocEntries(data: DocEntry[]) {
|
|
data.forEach((d) => {
|
|
if (d.children) {
|
|
d.children = sortDocEntries(d.children);
|
|
}
|
|
});
|
|
return data.sort((a, b) => {
|
|
if (a.children && !b.children) {
|
|
return 1;
|
|
}
|
|
if (b.children && !a.children) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
/** Generate documentation for all classes in a set of .ts files */
|
|
function gatherEntries(
|
|
fileName: string,
|
|
options: ts.CompilerOptions
|
|
): DocEntry[] {
|
|
// Build a program using the set of root file names in fileNames
|
|
const program = ts.createProgram([fileName], options);
|
|
|
|
// Get the checker, we will use it to find more about classes
|
|
const checker = program.getTypeChecker();
|
|
|
|
const data: DocEntry[] = [];
|
|
const currentSourceFile = program.getSourceFile(fileName)!;
|
|
|
|
ts.forEachChild(currentSourceFile, visit);
|
|
|
|
return sortDocEntries(data);
|
|
|
|
/** visit nodes finding css variables */
|
|
function visit(node: ts.Node) {
|
|
if (ts.isVariableStatement(node)) {
|
|
// TODO (cvle) - Currently the variable name is hardcoded. We might want to change that.
|
|
if (!node.getFullText().includes("streamVariables")) {
|
|
return;
|
|
}
|
|
const firstChild = node.declarationList.declarations[0];
|
|
if (ts.isVariableDeclaration(firstChild)) {
|
|
const symbol = checker.getSymbolAtLocation(firstChild.name);
|
|
if (symbol) {
|
|
const type = checker.getTypeOfSymbolAtLocation(
|
|
symbol,
|
|
symbol.valueDeclaration
|
|
);
|
|
type.getProperties().forEach((property) => {
|
|
serializePropertySymbol(property);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** This will evaluate `symbol` and addthe doc entries */
|
|
function serializePropertySymbol(symbol: ts.Symbol, parent?: DocEntry) {
|
|
const entry: DocEntry = { key: symbol.name };
|
|
const pt = checker.getTypeOfSymbolAtLocation(
|
|
symbol,
|
|
symbol.valueDeclaration
|
|
);
|
|
if (pt.symbol?.name === "__object") {
|
|
pt.getProperties().forEach((p2) => {
|
|
serializePropertySymbol(p2, entry);
|
|
});
|
|
} else {
|
|
entry.value = symbol.valueDeclaration
|
|
// Last child contains value.
|
|
.getChildAt(symbol.valueDeclaration.getChildCount() - 1)!
|
|
.getFullText();
|
|
}
|
|
entry.docs = ts.displayPartsToString(
|
|
symbol.getDocumentationComment(checker)
|
|
);
|
|
if (parent) {
|
|
if (parent.children) {
|
|
parent.children.push(entry);
|
|
} else {
|
|
parent.children = [entry];
|
|
}
|
|
} else {
|
|
data.push(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds any references to `object` by `variableName` and replaces the text
|
|
* with the actual value.
|
|
*
|
|
* E.g. `replaceObjectVariablesInText("colors.teal100", colors, "colors") === "#E2FAF7"`;
|
|
*/
|
|
function replaceObjectVariablesInText(
|
|
text: string,
|
|
object: any,
|
|
variableName: string
|
|
) {
|
|
let result = text;
|
|
Object.keys(object).forEach((c: string) => {
|
|
if (typeof object[c] === "object") {
|
|
result = replaceObjectVariablesInText(
|
|
result,
|
|
object[c],
|
|
`${variableName}.${c}`
|
|
);
|
|
} else {
|
|
result = result.replace(`${variableName}.${c}`, object[c]);
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* transforms value to be used in the documentation.
|
|
*/
|
|
function transformValue(value: string) {
|
|
let compat = "";
|
|
value = trim(value);
|
|
// Detect compat value.
|
|
if (value.startsWith("compat")) {
|
|
const result = /compat\((.*), *"(.*)"\)/.exec(value);
|
|
if (!result) {
|
|
throw new Error("Unrecognized compat format");
|
|
}
|
|
value = result[1];
|
|
compat = result[2];
|
|
}
|
|
// If it's a raw string, evaluate it to get rid of the initial quotes.
|
|
if (value[0] === '"' || value[0] === "'") {
|
|
// eslint-disable-next-line no-eval
|
|
value = eval(value);
|
|
}
|
|
// Replace all references to colors.
|
|
value = replaceObjectVariablesInText(value, colors, "colors");
|
|
if (compat) {
|
|
// add compat information.
|
|
return `${value}; /* Before 6.3.0: --${compat} */`;
|
|
}
|
|
return `${value};`;
|
|
}
|
|
|
|
function prefixLines(text: string, prefix: string) {
|
|
return text.split("\n").join(`\n${prefix}`);
|
|
}
|
|
|
|
function entries2Summary(
|
|
entries: DocEntry[],
|
|
keyprefix = "",
|
|
nestprefix = ""
|
|
): string {
|
|
let doc = "";
|
|
entries.forEach((entry) => {
|
|
if (!entry.children) {
|
|
return;
|
|
}
|
|
const header = kebabCase(keyprefix + entry.key);
|
|
doc += `${nestprefix}- <a href="#${header}">${header}</a>\n`;
|
|
if (entry.children) {
|
|
doc += entries2Summary(
|
|
entry.children,
|
|
keyprefix + entry.key + "-",
|
|
nestprefix + " "
|
|
);
|
|
}
|
|
});
|
|
return doc;
|
|
}
|
|
|
|
function entries2Doc(entries: DocEntry[], header = "###", prefix = ""): string {
|
|
let doc = "";
|
|
entries.forEach((entry) => {
|
|
if (entry.children) {
|
|
doc += `\n${header} ${kebabCase(prefix + entry.key)}\n`;
|
|
if (entry.docs) {
|
|
doc += `\n${entry.docs}\n`;
|
|
}
|
|
doc += entries2Doc(
|
|
entry.children,
|
|
header + "#",
|
|
`${prefix}${entry.key}-`
|
|
);
|
|
} else {
|
|
if (entry.docs) {
|
|
doc += `\n${entry.docs}\n`;
|
|
}
|
|
doc += `\n\`--${kebabCase(prefix + entry.key)}: ${transformValue(
|
|
entry.value!
|
|
)}\`\n`;
|
|
}
|
|
});
|
|
return doc;
|
|
}
|
|
|
|
/**
|
|
* Append or update previous documention in markdownFile.
|
|
*
|
|
* @param markdownFile The markdown file we want to inject the docs too.
|
|
* @param entries data as returned by gatherEntries.
|
|
*/
|
|
function emitDocs(markdownFile: string, entries: DocEntry[], verify = false) {
|
|
const previousContent = fs.existsSync(markdownFile)
|
|
? fs.readFileSync(markdownFile).toString()
|
|
: "";
|
|
|
|
const summary = entries2Summary(entries, "", " ");
|
|
const list = entries2Doc(entries);
|
|
|
|
const output = stripIndent`
|
|
<!-- START docs:css-variables -->
|
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN npm run docs:css-variables -->
|
|
### Index
|
|
- <a href="#variables">Variables</a>
|
|
${prefixLines(summary, " ")}
|
|
|
|
### Variables
|
|
${prefixLines(list, " ")}
|
|
<!-- END docs:css-variables -->
|
|
`;
|
|
|
|
let newContent;
|
|
// Find previous block.
|
|
if (BLOCK_REGEXP.test(previousContent)) {
|
|
newContent = previousContent.replace(BLOCK_REGEXP, output);
|
|
} else {
|
|
newContent = previousContent + "\n" + output;
|
|
}
|
|
if (previousContent === newContent) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`${markdownFile} is up to date`);
|
|
return;
|
|
}
|
|
if (verify) {
|
|
// eslint-disable-next-line no-console
|
|
console.error(
|
|
`${markdownFile} is outdated, please run \`npm run docs:css-variables\``
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
fs.writeFileSync(markdownFile, newContent);
|
|
// eslint-disable-next-line no-console
|
|
console.log(`Successfully injected documentation into ${markdownFile}`);
|
|
}
|
|
|
|
function main() {
|
|
if (process.argv.length < 4) {
|
|
throw new Error("Must provide path to css variables and a markdown file.");
|
|
}
|
|
|
|
const variableFile = process.argv[2];
|
|
const markdownFile = process.argv[3];
|
|
|
|
// Find tsconfig file.
|
|
const configFile = ts.findConfigFile(variableFile, fs.existsSync);
|
|
if (!configFile) {
|
|
throw new Error("tsconfig file not found");
|
|
}
|
|
const configText = fs.readFileSync(configFile).toString();
|
|
const result = ts.parseConfigFileTextToJson(configFile, configText);
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
|
|
// Parse the JSON raw data into actual consumable compiler options.
|
|
const config = ts.parseJsonConfigFileContent(
|
|
result.config,
|
|
ts.sys,
|
|
path.dirname(configFile)
|
|
);
|
|
|
|
const entries = gatherEntries(variableFile, config.options);
|
|
emitDocs(markdownFile, entries, process.argv[4] === "--verify");
|
|
}
|
|
|
|
main();
|