/** * Stat sheet template engine — parses *.sheet.svg files and extracts * text nodes containing ${key} template patterns. * * Walks elements. If a has children, we bind to * each individually (preserving their position attributes). * If a has no children, we bind to the 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 (no children) or a . */ 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 and 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 individually for (const tspan of tspans) { scanNode(tspan, templates); } } else { // Leaf — bind directly scanNode(textEl, templates); } } return { svgElement, templates }; }