mysticsymbolic.github.io/lib/vocabulary-builder.ts

180 wiersze
5.1 KiB
TypeScript

import fs from "fs";
import path from "path";
import cheerio from "cheerio";
import { getSvgBoundingBox } from "./bounding-box";
import { extractSpecs } from "./specs";
import { SvgSymbolData, SvgSymbolElement } from "./svg-symbol";
import toml from "toml";
import { validateSvgSymbolMetadata } from "./svg-symbol-metadata";
const SUPPORTED_SVG_TAG_ARRAY: SvgSymbolElement["tagName"][] = ["g", "path"];
const SUPPORTED_SVG_TAGS = new Set(SUPPORTED_SVG_TAG_ARRAY);
const MY_DIR = __dirname;
export const SVG_SYMBOLS_DIR = path.join(MY_DIR, "..", "assets", "symbols");
const VOCAB_JSON_PATH = path.join(
MY_DIR,
"_svg-vocabulary-pretty-printed.json"
);
const VOCAB_TS_PATH = path.join(MY_DIR, "_svg-vocabulary.ts");
const SVG_EXT = ".svg";
function onlyTags(
elements: cheerio.Element[] | cheerio.Cheerio
): cheerio.TagElement[] {
const result: cheerio.TagElement[] = [];
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
if (el.type === "tag") {
result.push(el);
}
}
return result;
}
function isSupportedSvgTag(
tagName: string
): tagName is SvgSymbolElement["tagName"] {
return SUPPORTED_SVG_TAGS.has(tagName as any);
}
const SVG_ATTRIB_TO_PROP_MAP: {
[key: string]: keyof SvgSymbolElement["props"] | undefined;
} = {
id: "id",
fill: "fill",
stroke: "stroke",
d: "d",
"stroke-linejoin": "strokeLinejoin",
"stroke-linecap": "strokeLinecap",
"stroke-width": "strokeWidth",
"fill-rule": "fillRule",
};
function attribsToProps(el: cheerio.TagElement): any {
const { attribs } = el;
const result: any = {};
for (let attrib of Object.keys(attribs)) {
const prop = SVG_ATTRIB_TO_PROP_MAP[attrib];
if (!prop) {
throw new Error(`Unknown SVG attribute '${attrib}' in <${el.tagName}>!`);
}
result[prop] = attribs[attrib];
}
return result;
}
function serializeSvgSymbolElement(
$: cheerio.Root,
el: cheerio.TagElement
): SvgSymbolElement {
let children = onlyTags(el.children).map((child) =>
serializeSvgSymbolElement($, child)
);
const { tagName } = el;
if (isSupportedSvgTag(tagName)) {
return {
tagName,
props: attribsToProps(el) as any,
children,
};
}
throw new Error(`Unsupported SVG element: <${tagName}>`);
}
function removeEmptyGroups(s: SvgSymbolElement[]): SvgSymbolElement[] {
return s
.filter((child) => !(child.tagName === "g" && child.children.length === 0))
.map((s) => ({
...s,
children: removeEmptyGroups(s.children),
}));
}
export function convertSvgMarkupToSymbolData(
filename: string,
svgMarkup: string
): SvgSymbolData {
const name = path.basename(filename, SVG_EXT).toLowerCase();
const $ = cheerio.load(svgMarkup);
const svgEl = $("svg");
const rawLayers = removeEmptyGroups(
onlyTags(svgEl.children()).map((ch) => serializeSvgSymbolElement($, ch))
);
const [specs, layers] = extractSpecs(rawLayers);
const bbox = getSvgBoundingBox(layers);
const symbol: SvgSymbolData = {
name,
bbox,
layers,
specs,
};
return symbol;
}
export function build() {
const filenames = fs.readdirSync(SVG_SYMBOLS_DIR);
const vocab: SvgSymbolData[] = [];
for (let filename of filenames) {
if (path.extname(filename) === SVG_EXT) {
let filenames = filename;
let metaToml: string | null = null;
const metaFilename = `${path.basename(filename, SVG_EXT)}.toml`;
const metaFilepath = path.join(SVG_SYMBOLS_DIR, metaFilename);
if (fs.existsSync(metaFilepath)) {
filenames += ` and ${metaFilename}`;
metaToml = fs.readFileSync(metaFilepath, {
encoding: "utf-8",
});
}
console.log(`Adding ${filenames} to vocabulary.`);
const svgMarkup = fs.readFileSync(path.join(SVG_SYMBOLS_DIR, filename), {
encoding: "utf-8",
});
const symbol = convertSvgMarkupToSymbolData(filename, svgMarkup);
if (metaToml) {
const { metadata, unknownProperties } = validateSvgSymbolMetadata(
toml.parse(metaToml)
);
symbol.meta = metadata;
if (unknownProperties.length) {
console.log(
`WARNING: Found unknown metadata properties ${unknownProperties.join(
", "
)}.`
);
}
}
vocab.push(symbol);
}
}
console.log(`Writing ${VOCAB_JSON_PATH} (for debugging output).`);
fs.writeFileSync(VOCAB_JSON_PATH, JSON.stringify(vocab, null, 2));
// Ugh, we need to write out a TypeScript file instead of importing
// the JSON directly because otherwise the TS compiler will spend
// a huge amount of resources doing type inference, which massively
// slows down type-checking (especially in IDEs and such).
console.log(`Writing ${VOCAB_TS_PATH} (for importing into code).`);
const stringified = JSON.stringify(vocab);
fs.writeFileSync(
VOCAB_TS_PATH,
[
"// This file is auto-generated, please do not modify it.",
`import type { SvgSymbolData } from "./svg-symbol";`,
`const _SvgVocabulary: SvgSymbolData[] = JSON.parse(${JSON.stringify(
stringified
)});`,
`export default _SvgVocabulary;`,
].join("\n")
);
}