Process multiple children of <svg>, store SVG as struct.
rodzic
67d8c8f3e2
commit
5df222edea
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue