refactor: replace stat system with variable system

Replaces the specialized `stat` system with a more general-purpose
variable system. This includes:

- Renaming `ReactiveStatManager` to `ReactiveVariableManager`
- Changing template syntax from `${key}` to `{{$key}}`
- Replacing `StatsView` with `VariableView` to show declared, plain,
  and tag-typed variables
- Updating command `/stat` to `/set`
- Refactoring the reactivity engine to support variable declarations
  and tag modifiers via `csv role=declare` blocks
This commit is contained in:
2026-07-12 17:29:03 +08:00
parent 2983aa4440
commit 894f1735e5
18 changed files with 464 additions and 1063 deletions
+29 -41
View File
@@ -2,7 +2,8 @@
* Journal completions — client-side loader for /__COMPLETIONS.json
*
* In CLI mode, fetches the pre-computed index. In dev/browser mode, falls
* back to scanning the in-memory file index for dice expressions and headings.
* back to scanning the in-memory file index for dice expressions, headings,
* and declare blocks.
*
* The fetch runs eagerly on module import. Call `useJournalCompletions()`
* from any Solid component to reactively read the state.
@@ -15,13 +16,6 @@ import {
getPathsByExtension,
getIndexedData,
} from "../../data-loader/file-index";
import {
parseStatYaml,
parseStatCsv,
parseTemplateCsv,
parseStatModifiers,
} from "../../cli/completions/stat-parser";
import type { StatDef, StatTemplate } from "../../cli/completions/stat-parser";
import {
FENCED_BLOCK_RE,
parseBlockAttrs,
@@ -29,8 +23,11 @@ import {
import {
scanDirectives,
} from "../../cli/completions/directive-scanner";
import { parseDeclareCsv } from "./declare-parser";
import type { VarDeclaration, TagModifier } from "./declare-parser";
import { initReactivity } from "./var-reactivity";
export type { StatDef, StatTemplate };
export type { VarDeclaration, TagModifier };
// ------------------- Types (mirrors CLI) -------------------
@@ -60,8 +57,8 @@ export interface JournalCompletions {
dice: DiceCompletion[];
links: LinkCompletion[];
sparkTables: SparkTableCompletion[];
stats: StatDef[];
statTemplates: StatTemplate[];
declarations: VarDeclaration[];
tagModifiers: TagModifier[];
}
export type CompletionsState =
@@ -87,9 +84,11 @@ async function tryServer(): Promise<JournalCompletions | null> {
dice: Array.isArray(data.dice) ? data.dice : [],
links: Array.isArray(data.links) ? data.links : [],
sparkTables: Array.isArray(data.sparkTables) ? data.sparkTables : [],
stats: Array.isArray(data.stats) ? data.stats : [],
statTemplates: Array.isArray(data.statTemplates)
? data.statTemplates
declarations: Array.isArray(data.declarations)
? data.declarations
: [],
tagModifiers: Array.isArray(data.tagModifiers)
? data.tagModifiers
: [],
};
} catch {
@@ -104,8 +103,8 @@ async function scanClientSide(): Promise<JournalCompletions> {
const dice: DiceCompletion[] = [];
const links: LinkCompletion[] = [];
const sparkTables: SparkTableCompletion[] = [];
const stats: StatDef[] = [];
const statTemplates: StatTemplate[] = [];
const declarations: VarDeclaration[] = [];
const tagModifiers: TagModifier[] = [];
// Build a temporary index for resolving CSV paths
const tempIndex: Record<string, string> = {};
@@ -135,7 +134,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
});
}
// ---- Unified block scanning (stats) ----
// ---- Unified block scanning (declare) ----
FENCED_BLOCK_RE.lastIndex = 0;
let blockMatch: RegExpExecArray | null;
while ((blockMatch = FENCED_BLOCK_RE.exec(content)) !== null) {
@@ -143,26 +142,15 @@ async function scanClientSide(): Promise<JournalCompletions> {
const attrs = parseBlockAttrs(infoString);
attrs.lang = attrs.lang || lang;
if (attrs.role === "stat") {
if (attrs.lang === "yaml" || attrs.lang === "yml") {
stats.push(...parseStatYaml(body, filePath));
} else if (attrs.lang === "csv") {
stats.push(...parseStatCsv(body, filePath));
if (attrs.role === "declare") {
try {
const result = parseDeclareCsv(body, filePath);
declarations.push(...result.variables);
tagModifiers.push(...result.tagModifiers);
} catch (e) {
console.warn(`[completions] ${filePath}: ${e}`);
}
}
if (attrs.role === "stat-template") {
const name = attrs.id || `_tpl_${filePath}_${stats.length}`;
statTemplates.push(parseTemplateCsv(body, filePath, name));
}
if (attrs.role === "stat-modifiers") {
const id = attrs.id || `_mod_${filePath}_${stats.length}`;
const result = parseStatModifiers(body, filePath, id, "player", attrs.extra["label"]);
stats.push(result.statDef);
stats.push(...result.modifierDefs);
statTemplates.push(result.template);
}
}
// ---- Directive scanning (dice + spark tables) ----
@@ -172,7 +160,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
sparkTables.push(...directiveResult.sparkTables);
}
return { dice, links, sparkTables, stats, statTemplates };
return { dice, links, sparkTables, declarations, tagModifiers };
}
// ------------------- Init (runs eagerly at import time) -------------------
@@ -183,6 +171,7 @@ const _initPromise: Promise<void> = (async () => {
const serverData = await tryServer();
if (serverData) {
setCompletionsState({ status: "loaded", data: serverData });
try { initReactivity({ declarations: serverData.declarations, tagModifiers: serverData.tagModifiers }); } catch (e) { console.warn("[completions] reactivity init error:", e); }
return;
}
@@ -191,6 +180,7 @@ const _initPromise: Promise<void> = (async () => {
const data = await scanClientSide();
if (data.dice.length > 0 || data.links.length > 0) {
setCompletionsState({ status: "loaded", data });
try { initReactivity({ declarations: data.declarations, tagModifiers: data.tagModifiers }); } catch (e) { console.warn("[completions] reactivity init error:", e); }
} else {
setCompletionsState({ status: "empty" });
}
@@ -216,8 +206,6 @@ export function ensureCompletions(): Promise<void> {
let _invalidated = false;
export function invalidateCompletions(): void {
_invalidated = true;
// On next import (page reload), the module will re-init.
// For a runtime invalidation, you could call init again.
}
/**
@@ -238,8 +226,8 @@ export function useJournalCompletions(): {
dice: [],
links: [],
sparkTables: [],
stats: [],
statTemplates: [],
declarations: [],
tagModifiers: [],
},
};
}
}