180 wiersze
5.1 KiB
TypeScript
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")
|
|
);
|
|
}
|