feat(journal): support <tspan> in stat sheet templates
Update the stat sheet parser to scan <tspan> elements within <text> nodes. This allows for individual binding of template patterns to specific tspans, preserving their position attributes.
This commit is contained in:
parent
30d1c5dc1b
commit
fe87b1f80c
|
|
@ -1,18 +1,22 @@
|
|||
/**
|
||||
* Stat sheet template engine — parses *.sheet.svg files and extracts
|
||||
* <text> 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 <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 in a <text> element. */
|
||||
/** A single template binding targeting a specific text-bearing leaf node. */
|
||||
export interface TextTemplate {
|
||||
el: SVGTextElement;
|
||||
/** 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[];
|
||||
}
|
||||
|
||||
|
|
@ -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 <text> and <tspan> 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 (tspans.length > 0) {
|
||||
// Has children — bind to each <tspan> individually
|
||||
for (const tspan of tspans) {
|
||||
scanNode(tspan, templates);
|
||||
}
|
||||
|
||||
if (keys.length > 0) {
|
||||
templates.push({ el: el as SVGTextElement, template: content, keys });
|
||||
} else {
|
||||
// Leaf <text> — bind directly
|
||||
scanNode(textEl, templates);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue