89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
/**
|
|
* Stat sheet template engine — parses *.sheet.svg files and extracts
|
|
* text nodes containing ${key} template patterns.
|
|
*
|
|
* Walks <text> elements. If a <text> has <tspan> children, we bind to
|
|
* each <tspan> individually (preserving their position attributes).
|
|
* If a <text> has no <tspan> children, we bind to the <text> directly.
|
|
*/
|
|
|
|
/** Regex matching ${key} template patterns (captures the key name). */
|
|
const TEMPLATE_RE = /\$\{(\w+)\}/g;
|
|
|
|
/** A single template binding targeting a specific text-bearing leaf node. */
|
|
export interface TextTemplate {
|
|
/** The DOM node to update — a <text> (no children) or a <tspan>. */
|
|
el: SVGTextContentElement;
|
|
/** Original text content with ${key} placeholders. */
|
|
template: string;
|
|
/** Bare key names extracted from the template. */
|
|
keys: string[];
|
|
}
|
|
|
|
/** The result of parsing a stat sheet SVG string. */
|
|
export interface ParsedSheet {
|
|
svgElement: SVGSVGElement;
|
|
templates: TextTemplate[];
|
|
}
|
|
|
|
/**
|
|
* Check if a text node's content has template patterns, and if so,
|
|
* register a binding.
|
|
*/
|
|
function scanNode(el: Element, templates: TextTemplate[]): void {
|
|
const content = el.textContent;
|
|
if (!content) return;
|
|
|
|
TEMPLATE_RE.lastIndex = 0;
|
|
const keys: string[] = [];
|
|
let m: RegExpExecArray | null;
|
|
while ((m = TEMPLATE_RE.exec(content)) !== null) {
|
|
keys.push(m[1]);
|
|
}
|
|
|
|
if (keys.length > 0) {
|
|
templates.push({
|
|
el: el as SVGTextContentElement,
|
|
template: content,
|
|
keys,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse an SVG string into a live DOM element and extract all template
|
|
* bindings from <text> and <tspan> leaf nodes.
|
|
*/
|
|
export function parseSheet(svgString: string): ParsedSheet {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(svgString, "image/svg+xml");
|
|
const svgElement = doc.documentElement as unknown as SVGSVGElement;
|
|
|
|
if (!svgElement || svgElement.tagName !== "svg") {
|
|
return {
|
|
svgElement: document.createElementNS(
|
|
"http://www.w3.org/2000/svg",
|
|
"svg",
|
|
) as SVGSVGElement,
|
|
templates: [],
|
|
};
|
|
}
|
|
|
|
const templates: TextTemplate[] = [];
|
|
|
|
for (const textEl of svgElement.querySelectorAll("text")) {
|
|
const tspans = textEl.querySelectorAll("tspan");
|
|
|
|
if (tspans.length > 0) {
|
|
// Has children — bind to each <tspan> individually
|
|
for (const tspan of tspans) {
|
|
scanNode(tspan, templates);
|
|
}
|
|
} else {
|
|
// Leaf <text> — bind directly
|
|
scanNode(textEl, templates);
|
|
}
|
|
}
|
|
|
|
return { svgElement, templates };
|
|
} |