Process multiple children of <svg>, store SVG as struct.

pull/1/head
Atul Varma 2021-02-06 07:50:51 -05:00
rodzic 67d8c8f3e2
commit 5df222edea
2 zmienionych plików z 128 dodań i 29 usunięć

Wyświetl plik

@ -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<SvgSymbolProps> = (props) => {
const d = props.data;
const scale = props.scale || 1;
return (
<svg
stroke={props.stroke}
fill={props.fill}
viewBox={`0 0 ${d.width} ${d.height}`}
width={px(d.width * scale)}
height={px(d.height * scale)}
dangerouslySetInnerHTML={{ __html: d.svg }}
></svg>
>
{props.data.layers.map(reactifySvgSymbolElement.bind(null, props))}
</svg>
);
};
@ -64,6 +91,7 @@ const App: React.FC<{}> = () => {
</p>
{SvgVocabulary.map((symbolData) => (
<div
key={symbolData.name}
style={{
display: "inline-block",
border: "1px solid black",

Wyświetl plik

@ -1,30 +1,103 @@
import fs from "fs";
import path from "path";
import cheerio from "cheerio";
import { SVGProps } from "react";
export type SvgSymbolData = {
name: string;
width: number;
height: number;
svg: string;
layers: SvgSymbolElement[];
};
export type SvgSymbolElement = (
| {
tagName: "g";
props: SVGProps<SVGGElement>;
}
| {
tagName: "path";
props: SVGProps<SVGPathElement>;
}
) & {
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 <svg> ${attr} attribute value '${value}'!`
`Unable to parse <svg> ${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 <g> with child elements!`);
}
const layers = onlyTags(svgEl.children()).map((ch) =>
serializeSvgSymbolElement($, ch)
);
const symbol: SvgSymbolData = {
name,
svg,
width,
height,
layers,
};
vocab.push(symbol);
}