diff --git a/src/components/journal/stat-sheet.ts b/src/components/journal/stat-sheet.ts index 8fb21c8..485e767 100644 --- a/src/components/journal/stat-sheet.ts +++ b/src/components/journal/stat-sheet.ts @@ -1,18 +1,22 @@ /** * Stat sheet template engine — parses *.sheet.svg files and extracts - * elements containing ${key} template patterns. + * text nodes containing ${key} template patterns. * - * The SVG element is parsed once via DOMParser. The caller (StatsView) - * reactively updates the text nodes whenever the stats store changes. + * 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 in a element. */ +/** A single template binding targeting a specific text-bearing leaf node. */ export interface TextTemplate { - el: SVGTextElement; + /** 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[]; } @@ -23,8 +27,32 @@ export interface ParsedSheet { } /** - * Parse an SVG string into a live DOM element and extract all text - * elements that contain ${key} template patterns. + * 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(); @@ -42,21 +70,18 @@ export function parseSheet(svgString: string): ParsedSheet { } const templates: TextTemplate[] = []; - const textEls = svgElement.querySelectorAll("text"); - for (const el of textEls) { - const content = el.textContent; - if (!content) continue; + for (const textEl of svgElement.querySelectorAll("text")) { + const tspans = textEl.querySelectorAll("tspan"); - 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 SVGTextElement, template: content, keys }); + 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); } }