refactor: optimize reactive variable updates and parsing

- Replace `scanDeclareBlocks` with `parseDeclareCsv` in block processor
- Add error handling for declare block parsing
- Optimize `ReactiveVariableManager` by pre-computing variable keys
- Store template text and keys in data attributes for faster updates
- Adjust `VariableView` popup width via CSS class
This commit is contained in:
hypercross 2026-07-13 11:13:07 +08:00
parent c106b6f79c
commit ffa023b92a
3 changed files with 26 additions and 20 deletions

View File

@ -7,7 +7,7 @@
import { posix } from "path"; import { posix } from "path";
import { createHash } from "crypto"; import { createHash } from "crypto";
import { scanDeclareBlocks, type VarDeclaration, type TagModifier } from "./declare-parser.js"; import { parseDeclareCsv, type VarDeclaration, type TagModifier } from "./declare-parser.js";
import { import {
FENCED_BLOCK_RE, FENCED_BLOCK_RE,
parseBlockAttrs, parseBlockAttrs,
@ -86,9 +86,13 @@ export function processBlocks(
// ---- Dispatch by role ---- // ---- Dispatch by role ----
if (attrs.role === "declare") { if (attrs.role === "declare") {
const result = scanDeclareBlocks(_match, fileRelativePath); try {
blocks.declarations.push(...result.variables); const result = parseDeclareCsv(body, fileRelativePath);
blocks.tagModifiers.push(...result.tagModifiers); blocks.declarations.push(...result.variables);
blocks.tagModifiers.push(...result.tagModifiers);
} catch (e) {
console.warn(`[block-processor] ${fileRelativePath}: ${e}`);
}
} }
if (attrs.role === "file") { if (attrs.role === "file") {

View File

@ -15,8 +15,11 @@ import { useJournalStream } from "../stores/journalStream";
const TEMPLATE_RE = /\{\{(\$[a-zA-Z_][a-zA-Z0-9_]*)\}\}/g; const TEMPLATE_RE = /\{\{(\$[a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
/** Tag name used for marker spans — must stay in sync with the scan pass. */ /** Attribute storing the original template text on marker spans. */
const MARKER = "data-reactive-var"; const ATTR_TEMPLATE = "data-reactive-var";
/** Attribute storing pre-computed key list (comma-separated) for fast updates. */
const ATTR_KEYS = "data-reactive-keys";
export const ReactiveVariableManager: Component = () => { export const ReactiveVariableManager: Component = () => {
const contentDom = useArticleDom(); const contentDom = useArticleDom();
@ -32,7 +35,7 @@ export const ReactiveVariableManager: Component = () => {
// Only scan once — after the first scan, the spans exist and we // Only scan once — after the first scan, the spans exist and we
// switch to the reactive update effect below. // switch to the reactive update effect below.
if (dom.querySelector(`[${MARKER}]`)) return; if (dom.querySelector(`[${ATTR_TEMPLATE}]`)) return;
scanAndWrap(dom); scanAndWrap(dom);
}); });
@ -49,17 +52,14 @@ export const ReactiveVariableManager: Component = () => {
if (!dom) return; if (!dom) return;
const v = values(); const v = values();
const spans = dom.querySelectorAll<HTMLElement>(`[${MARKER}]`); const spans = dom.querySelectorAll<HTMLElement>(`[${ATTR_TEMPLATE}]`);
for (const span of spans) { for (const span of spans) {
const template = span.getAttribute(MARKER) ?? ""; const template = span.getAttribute(ATTR_TEMPLATE) ?? "";
const keys = span.getAttribute(ATTR_KEYS)?.split(",") ?? [];
let result = template; let result = template;
TEMPLATE_RE.lastIndex = 0; for (const key of keys) {
let m: RegExpExecArray | null; result = result.replaceAll(`{{${key}}}`, v[key] ?? "");
while ((m = TEMPLATE_RE.exec(template)) !== null) {
const key = m[1]; // includes $ prefix
const resolved = v[key] ?? "";
result = result.replace(`{{${key}}}`, resolved);
} }
if (span.textContent !== result) { if (span.textContent !== result) {
span.textContent = result; span.textContent = result;
@ -90,7 +90,8 @@ function scanAndWrap(root: Element): void {
TEMPLATE_RE.lastIndex = 0; TEMPLATE_RE.lastIndex = 0;
if (!TEMPLATE_RE.test(text)) continue; if (!TEMPLATE_RE.test(text)) continue;
// Build replacement HTML: split on {{$key}} and wrap each key span // Build replacement HTML: split on {{$key}}, wrap each match in a span
// with pre-computed key for fast updates.
TEMPLATE_RE.lastIndex = 0; TEMPLATE_RE.lastIndex = 0;
const parts: string[] = []; const parts: string[] = [];
let lastIndex = 0; let lastIndex = 0;
@ -99,8 +100,9 @@ function scanAndWrap(root: Element): void {
if (m.index > lastIndex) { if (m.index > lastIndex) {
parts.push(escapeHtml(text.slice(lastIndex, m.index))); parts.push(escapeHtml(text.slice(lastIndex, m.index)));
} }
const key = m[1]; // includes $ prefix
parts.push( parts.push(
`<span ${MARKER}="${escapeAttr(m[0])}">${escapeHtml(m[0])}</span>`, `<span ${ATTR_TEMPLATE}="${escapeAttr(m[0])}" ${ATTR_KEYS}="${escapeAttr(key)}">${escapeHtml(m[0])}</span>`,
); );
lastIndex = TEMPLATE_RE.lastIndex; lastIndex = TEMPLATE_RE.lastIndex;
} }
@ -123,13 +125,13 @@ function scanAndWrap(root: Element): void {
* Called on cleanup so subsequent remounts get a clean slate. * Called on cleanup so subsequent remounts get a clean slate.
*/ */
function unwrapAll(root: Element): void { function unwrapAll(root: Element): void {
const spans = root.querySelectorAll<HTMLElement>(`[${MARKER}]`); const spans = root.querySelectorAll<HTMLElement>(`[${ATTR_TEMPLATE}]`);
// Process in reverse to avoid DOM mutation issues // Process in reverse to avoid DOM mutation issues
for (let i = spans.length - 1; i >= 0; i--) { for (let i = spans.length - 1; i >= 0; i--) {
const span = spans[i]; const span = spans[i];
const parent = span.parentNode; const parent = span.parentNode;
if (!parent) continue; if (!parent) continue;
const text = span.getAttribute(MARKER) ?? span.textContent ?? ""; const text = span.getAttribute(ATTR_TEMPLATE) ?? span.textContent ?? "";
span.replaceWith(document.createTextNode(text)); span.replaceWith(document.createTextNode(text));
} }
// Normalize to merge adjacent text nodes // Normalize to merge adjacent text nodes

View File

@ -90,7 +90,7 @@ const VariableRow: Component<{
{/* Hover popup */} {/* Hover popup */}
<Show when={hasPopup() && hovered()}> <Show when={hasPopup() && hovered()}>
<div class="absolute right-0 top-full mt-1 z-50 bg-white border border-gray-200 rounded-md shadow-lg p-2 min-w-[180px] text-xs"> <div class="absolute right-0 top-full mt-1 z-50 bg-white border border-gray-200 rounded-md shadow-lg p-2 min-w-45 text-xs">
{/* Declaration expression */} {/* Declaration expression */}
<Show when={declExpr()}> <Show when={declExpr()}>
<div class="mb-1"> <div class="mb-1">