feat: add ReactiveStatManager for markdown template support
Introduce ReactiveStatManager to allow dynamic stat replacement within
rendered markdown content. This component scans the article DOM for
`${key}` patterns and reactively updates them based on the journal
stream.
As part of this change, the SVG-based StatSheet system is removed in
favor of this more flexible text-based approach.
This commit is contained in:
@@ -19,7 +19,6 @@ import {
|
||||
scanDirectives,
|
||||
type DirectiveScanResult,
|
||||
} from "../completions/directive-scanner.js";
|
||||
import type { StatSheet } from "../completions/types.js";
|
||||
|
||||
interface ContentIndex {
|
||||
[path: string]: string;
|
||||
@@ -88,22 +87,6 @@ function getBestIP(): string {
|
||||
return best || "localhost";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 SVG 字符串中提取 <title> 内容,无则返回 null
|
||||
*/
|
||||
function extractSvgTitle(svg: string): string | null {
|
||||
const m = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(svg);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件名生成可读标签
|
||||
* 如 "character-sheet" -> "Character Sheet"
|
||||
*/
|
||||
function labelFromId(id: string): string {
|
||||
return id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描目录内的 .md 等文件,生成内容索引与块数据
|
||||
*/
|
||||
@@ -116,7 +99,6 @@ export function scanDirectory(dir: string): {
|
||||
const blocks: ProcessedBlocks = {
|
||||
stats: [],
|
||||
statTemplates: [],
|
||||
statSheets: [],
|
||||
};
|
||||
const directiveResults: DirectiveScanResult[] = [];
|
||||
const mdFiles: { content: string; relPath: string }[] = [];
|
||||
@@ -148,17 +130,6 @@ export function scanDirectory(dir: string): {
|
||||
blocks.stats.push(...result.blocks.stats);
|
||||
blocks.statTemplates.push(...result.blocks.statTemplates);
|
||||
mdFiles.push({ content: result.stripped, relPath: normalizedRelPath });
|
||||
} else if (entry.endsWith(".sheet.svg")) {
|
||||
index[normalizedRelPath] = content;
|
||||
const id = entry.replace(/\.sheet\.svg$/i, "");
|
||||
const title = extractSvgTitle(content);
|
||||
const sheet: StatSheet = {
|
||||
id,
|
||||
label: title || labelFromId(id),
|
||||
svg: content,
|
||||
source: normalizedRelPath,
|
||||
};
|
||||
blocks.statSheets.push(sheet);
|
||||
} else {
|
||||
index[normalizedRelPath] = content;
|
||||
}
|
||||
@@ -341,7 +312,6 @@ export function createContentServer(
|
||||
let collectedBlocks: ProcessedBlocks = {
|
||||
stats: [],
|
||||
statTemplates: [],
|
||||
statSheets: [],
|
||||
};
|
||||
let directiveResults: DirectiveScanResult[] = [];
|
||||
let completionsIndex: CompletionsPayload = {
|
||||
@@ -350,14 +320,13 @@ export function createContentServer(
|
||||
sparkTables: [],
|
||||
stats: [],
|
||||
statTemplates: [],
|
||||
statSheets: [],
|
||||
};
|
||||
|
||||
/** 从当前内容索引和已收集的块重新扫描补全数据 */
|
||||
function recomputeCompletions(): void {
|
||||
completionsIndex = scanCompletions(contentIndex, collectedBlocks, directiveResults);
|
||||
console.log(
|
||||
`[completions] dice=${completionsIndex.dice.length} links=${completionsIndex.links.length} sparkTables=${completionsIndex.sparkTables.length} stats=${completionsIndex.stats.length} templates=${completionsIndex.statTemplates.length} sheets=${completionsIndex.statSheets.length}`,
|
||||
`[completions] dice=${completionsIndex.dice.length} links=${completionsIndex.links.length} sparkTables=${completionsIndex.sparkTables.length} stats=${completionsIndex.stats.length} templates=${completionsIndex.statTemplates.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,12 +368,6 @@ export function createContentServer(
|
||||
recomputeCompletions();
|
||||
} else {
|
||||
contentIndex[relPath] = content;
|
||||
if (relPath.endsWith(".sheet.svg")) {
|
||||
const rescan = scanDirectory(contentDir);
|
||||
collectedBlocks = rescan.blocks;
|
||||
directiveResults = rescan.directiveResults;
|
||||
recomputeCompletions();
|
||||
}
|
||||
}
|
||||
console.log(`[新增] ${relPath}`);
|
||||
} catch (e) {
|
||||
@@ -431,12 +394,6 @@ export function createContentServer(
|
||||
recomputeCompletions();
|
||||
} else {
|
||||
contentIndex[relPath] = content;
|
||||
if (relPath.endsWith(".sheet.svg")) {
|
||||
const rescan = scanDirectory(contentDir);
|
||||
collectedBlocks = rescan.blocks;
|
||||
directiveResults = rescan.directiveResults;
|
||||
recomputeCompletions();
|
||||
}
|
||||
}
|
||||
console.log(`[更新] ${relPath}`);
|
||||
} catch (e) {
|
||||
@@ -454,7 +411,7 @@ export function createContentServer(
|
||||
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
|
||||
delete contentIndex[relPath];
|
||||
console.log(`[删除] ${relPath}`);
|
||||
if (relPath.endsWith(".md") || relPath.endsWith(".sheet.svg")) {
|
||||
if (relPath.endsWith(".md")) {
|
||||
const rescan = scanDirectory(contentDir);
|
||||
collectedBlocks = rescan.blocks;
|
||||
directiveResults = rescan.directiveResults;
|
||||
|
||||
Reference in New Issue
Block a user