diff --git a/lib/browser-main.tsx b/lib/browser-main.tsx index 3120fea..616d0d4 100644 --- a/lib/browser-main.tsx +++ b/lib/browser-main.tsx @@ -2,40 +2,67 @@ import React, { useState } from "react"; import ReactDOM from "react-dom"; import _SvgVocabulary from "./svg-vocabulary.json"; -import type { SvgSymbolData } from "./vocabulary"; +import type { SvgSymbolData, SvgSymbolElement } from "./vocabulary"; const APP_ID = "app"; const appEl = document.getElementById(APP_ID); -const SvgVocabulary: SvgSymbolData[] = _SvgVocabulary; +const SvgVocabulary: SvgSymbolData[] = _SvgVocabulary as any; if (!appEl) { throw new Error(`Unable to find #${APP_ID}!`); } -type SvgSymbolProps = { - data: SvgSymbolData; - scale?: number; +type SvgSymbolContext = { stroke: string; fill: string; }; +type SvgSymbolProps = { + data: SvgSymbolData; + scale?: number; +} & SvgSymbolContext; + const px = (value: number) => `${value}px`; +function reactifySvgSymbolElement( + ctx: SvgSymbolContext, + el: SvgSymbolElement, + key: number +): JSX.Element { + let { fill, stroke } = el.props; + if (fill && fill !== "none") { + fill = ctx.fill; + } + if (stroke && stroke !== "none") { + stroke = ctx.stroke; + } + return React.createElement( + el.tagName, + { + ...el.props, + id: undefined, + fill, + stroke, + key, + }, + el.children.map(reactifySvgSymbolElement.bind(null, ctx)) + ); +} + const SvgSymbol: React.FC = (props) => { const d = props.data; const scale = props.scale || 1; return ( + > + {props.data.layers.map(reactifySvgSymbolElement.bind(null, props))} + ); }; @@ -64,6 +91,7 @@ const App: React.FC<{}> = () => {

{SvgVocabulary.map((symbolData) => (
; + } + | { + tagName: "path"; + props: SVGProps; + } +) & { + children: SvgSymbolElement[]; +}; + +const SUPPORTED_SVG_TAG_ARRAY: SvgSymbolElement["tagName"][] = ["g", "path"]; +const SUPPORTED_SVG_TAGS = new Set(SUPPORTED_SVG_TAG_ARRAY); + const MY_DIR = __dirname; const SVG_DIR = path.join(MY_DIR, "..", "svg"); const VOCAB_PATH = path.join(MY_DIR, "svg-vocabulary.json"); const SVG_EXT = ".svg"; -function removeAttrIfNotNone( - attr: string, - $: cheerio.Root, - g: cheerio.Cheerio -) { - const items = g.find(`[${attr}]`); - items.each(function (this: any, i, el) { - if ($(this).attr(attr) !== "none") { - $(this).removeAttr(attr); +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 getSvgPixelDimension($: cheerio.Root, attr: string): number { @@ -35,7 +108,7 @@ function getSvgPixelDimension($: cheerio.Root, attr: string): number { const match = value.match(/^(\d+)px$/); if (!match) { throw new Error( - `unable to parse ${attr} attribute value '${value}'!` + `Unable to parse ${attr} attribute value '${value}'!` ); } return parseInt(match[1]); @@ -53,19 +126,17 @@ export function build() { const $ = cheerio.load(svgMarkup); const width = getSvgPixelDimension($, "width"); const height = getSvgPixelDimension($, "height"); - const g = $("svg > g"); - removeAttrIfNotNone("fill", $, g); - removeAttrIfNotNone("stroke", $, g); + const svgEl = $("svg"); const name = path.basename(filename, SVG_EXT); - const svg = g.html(); - if (!svg) { - throw new Error(`${filename} has no with child elements!`); - } + const layers = onlyTags(svgEl.children()).map((ch) => + serializeSvgSymbolElement($, ch) + ); + const symbol: SvgSymbolData = { name, - svg, width, height, + layers, }; vocab.push(symbol); }