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 ReactDOM from "react-dom";
|
||||||
|
|
||||||
import _SvgVocabulary from "./svg-vocabulary.json";
|
import _SvgVocabulary from "./svg-vocabulary.json";
|
||||||
import type { SvgSymbolData } from "./vocabulary";
|
import type { SvgSymbolData, SvgSymbolElement } from "./vocabulary";
|
||||||
|
|
||||||
const APP_ID = "app";
|
const APP_ID = "app";
|
||||||
|
|
||||||
const appEl = document.getElementById(APP_ID);
|
const appEl = document.getElementById(APP_ID);
|
||||||
|
|
||||||
const SvgVocabulary: SvgSymbolData[] = _SvgVocabulary;
|
const SvgVocabulary: SvgSymbolData[] = _SvgVocabulary as any;
|
||||||
|
|
||||||
if (!appEl) {
|
if (!appEl) {
|
||||||
throw new Error(`Unable to find #${APP_ID}!`);
|
throw new Error(`Unable to find #${APP_ID}!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
type SvgSymbolProps = {
|
type SvgSymbolContext = {
|
||||||
data: SvgSymbolData;
|
|
||||||
scale?: number;
|
|
||||||
stroke: string;
|
stroke: string;
|
||||||
fill: string;
|
fill: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SvgSymbolProps = {
|
||||||
|
data: SvgSymbolData;
|
||||||
|
scale?: number;
|
||||||
|
} & SvgSymbolContext;
|
||||||
|
|
||||||
const px = (value: number) => `${value}px`;
|
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 SvgSymbol: React.FC<SvgSymbolProps> = (props) => {
|
||||||
const d = props.data;
|
const d = props.data;
|
||||||
const scale = props.scale || 1;
|
const scale = props.scale || 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
stroke={props.stroke}
|
|
||||||
fill={props.fill}
|
|
||||||
viewBox={`0 0 ${d.width} ${d.height}`}
|
viewBox={`0 0 ${d.width} ${d.height}`}
|
||||||
width={px(d.width * scale)}
|
width={px(d.width * scale)}
|
||||||
height={px(d.height * 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>
|
</p>
|
||||||
{SvgVocabulary.map((symbolData) => (
|
{SvgVocabulary.map((symbolData) => (
|
||||||
<div
|
<div
|
||||||
|
key={symbolData.name}
|
||||||
style={{
|
style={{
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
border: "1px solid black",
|
border: "1px solid black",
|
||||||
|
|
|
@ -1,30 +1,103 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import cheerio from "cheerio";
|
import cheerio from "cheerio";
|
||||||
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
export type SvgSymbolData = {
|
export type SvgSymbolData = {
|
||||||
name: string;
|
name: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: 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 MY_DIR = __dirname;
|
||||||
const SVG_DIR = path.join(MY_DIR, "..", "svg");
|
const SVG_DIR = path.join(MY_DIR, "..", "svg");
|
||||||
const VOCAB_PATH = path.join(MY_DIR, "svg-vocabulary.json");
|
const VOCAB_PATH = path.join(MY_DIR, "svg-vocabulary.json");
|
||||||
const SVG_EXT = ".svg";
|
const SVG_EXT = ".svg";
|
||||||
|
|
||||||
function removeAttrIfNotNone(
|
function onlyTags(
|
||||||
attr: string,
|
elements: cheerio.Element[] | cheerio.Cheerio
|
||||||
$: cheerio.Root,
|
): cheerio.TagElement[] {
|
||||||
g: cheerio.Cheerio
|
const result: cheerio.TagElement[] = [];
|
||||||
) {
|
|
||||||
const items = g.find(`[${attr}]`);
|
for (let i = 0; i < elements.length; i++) {
|
||||||
items.each(function (this: any, i, el) {
|
const el = elements[i];
|
||||||
if ($(this).attr(attr) !== "none") {
|
if (el.type === "tag") {
|
||||||
$(this).removeAttr(attr);
|
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 {
|
function getSvgPixelDimension($: cheerio.Root, attr: string): number {
|
||||||
|
@ -35,7 +108,7 @@ function getSvgPixelDimension($: cheerio.Root, attr: string): number {
|
||||||
const match = value.match(/^(\d+)px$/);
|
const match = value.match(/^(\d+)px$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`unable to parse <svg> ${attr} attribute value '${value}'!`
|
`Unable to parse <svg> ${attr} attribute value '${value}'!`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return parseInt(match[1]);
|
return parseInt(match[1]);
|
||||||
|
@ -53,19 +126,17 @@ export function build() {
|
||||||
const $ = cheerio.load(svgMarkup);
|
const $ = cheerio.load(svgMarkup);
|
||||||
const width = getSvgPixelDimension($, "width");
|
const width = getSvgPixelDimension($, "width");
|
||||||
const height = getSvgPixelDimension($, "height");
|
const height = getSvgPixelDimension($, "height");
|
||||||
const g = $("svg > g");
|
const svgEl = $("svg");
|
||||||
removeAttrIfNotNone("fill", $, g);
|
|
||||||
removeAttrIfNotNone("stroke", $, g);
|
|
||||||
const name = path.basename(filename, SVG_EXT);
|
const name = path.basename(filename, SVG_EXT);
|
||||||
const svg = g.html();
|
const layers = onlyTags(svgEl.children()).map((ch) =>
|
||||||
if (!svg) {
|
serializeSvgSymbolElement($, ch)
|
||||||
throw new Error(`${filename} has no <g> with child elements!`);
|
);
|
||||||
}
|
|
||||||
const symbol: SvgSymbolData = {
|
const symbol: SvgSymbolData = {
|
||||||
name,
|
name,
|
||||||
svg,
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
layers,
|
||||||
};
|
};
|
||||||
vocab.push(symbol);
|
vocab.push(symbol);
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue