refactor(completions): centralize content scanning
Add a browser-safe ContentRegistry owning path and inline content stores, then derive completions from it. Remove the old block-processor, directive-scanner, and link-source modules in favor of scanDoc, deriveCompletions, and injectSparkDirectives, and register an inline resolver for the frontend file index.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* - variable-expression (expression evaluation)
|
||||
* - var-reactivity (dependency graph, cascade, tag activation)
|
||||
* - command-parser (input parsing)
|
||||
* - directive-scanner (spark table detection)
|
||||
* - content-registry (spark table detection)
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -47,8 +47,7 @@ import { parseInput } from "../../components/journal/command-parser";
|
||||
import {
|
||||
inspectSparkTableCsv,
|
||||
buildSparkTableCompletion,
|
||||
} from "./directive-scanner";
|
||||
import Slugger from "github-slugger";
|
||||
} from "../content-registry";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -70,7 +69,10 @@ describe("parseDeclareCsv", () => {
|
||||
,$ac,10+$dex`;
|
||||
const result = parseDeclareCsv(csv, "test.md");
|
||||
expect(result.variables).toHaveLength(2);
|
||||
expect(result.variables[0]).toEqual({ key: "$hp", expression: "$con*5+$mod_hp" });
|
||||
expect(result.variables[0]).toEqual({
|
||||
key: "$hp",
|
||||
expression: "$con*5+$mod_hp",
|
||||
});
|
||||
expect(result.variables[1]).toEqual({ key: "$ac", expression: "10+$dex" });
|
||||
expect(result.tagModifiers).toHaveLength(0);
|
||||
});
|
||||
@@ -154,7 +156,7 @@ describe("parseDeclareCsv", () => {
|
||||
const csv = `tag,key,expr
|
||||
warrior,$mod_hp,20`;
|
||||
expect(() => parseDeclareCsv(csv, "test.md")).toThrow(
|
||||
'tag must start with #',
|
||||
"tag must start with #",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -162,7 +164,7 @@ warrior,$mod_hp,20`;
|
||||
const csv = `tag,key,expr
|
||||
,hp,$con*5`;
|
||||
expect(() => parseDeclareCsv(csv, "test.md")).toThrow(
|
||||
'key must start with $',
|
||||
"key must start with $",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -170,7 +172,7 @@ warrior,$mod_hp,20`;
|
||||
const csv = `tag,key,expr
|
||||
#warrior,mod_hp,20`;
|
||||
expect(() => parseDeclareCsv(csv, "test.md")).toThrow(
|
||||
'key must start with $',
|
||||
"key must start with $",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -219,8 +221,8 @@ describe("parseBlockAttrs", () => {
|
||||
test("parses standard attributes", () => {
|
||||
// parseBlockAttrs receives the info string AFTER the lang.
|
||||
// The lang is extracted from the fenced block regex capture group
|
||||
// and applied separately in block-processor.
|
||||
const attrs = parseBlockAttrs('id=stats role=declare as=none');
|
||||
// and applied separately in content-registry.
|
||||
const attrs = parseBlockAttrs("id=stats role=declare as=none");
|
||||
expect(attrs.id).toBe("stats");
|
||||
expect(attrs.role).toBe("declare");
|
||||
expect(attrs.as).toBe("none");
|
||||
@@ -233,7 +235,7 @@ describe("parseBlockAttrs", () => {
|
||||
});
|
||||
|
||||
test("collects unknown attributes in extra", () => {
|
||||
const attrs = parseBlockAttrs('csv role=declare foo=bar baz=42');
|
||||
const attrs = parseBlockAttrs("csv role=declare foo=bar baz=42");
|
||||
expect(attrs.extra).toEqual({ foo: "bar", baz: "42" });
|
||||
});
|
||||
|
||||
@@ -282,7 +284,9 @@ describe("evaluateExpression", () => {
|
||||
});
|
||||
|
||||
test("evaluates with parentheses", () => {
|
||||
const result = evaluateExpression("(2 + 3) * 4", { lookup: () => undefined });
|
||||
const result = evaluateExpression("(2 + 3) * 4", {
|
||||
lookup: () => undefined,
|
||||
});
|
||||
expect(result.value).toBe(20);
|
||||
});
|
||||
|
||||
@@ -309,11 +313,13 @@ describe("evaluateExpression", () => {
|
||||
evaluateExpression("$class + 5", {
|
||||
lookup: (name) => (name === "class" ? "#warrior" : undefined),
|
||||
}),
|
||||
).toThrow('$class is a tag');
|
||||
).toThrow("$class is a tag");
|
||||
});
|
||||
|
||||
test("evaluates floor function", () => {
|
||||
const result = evaluateExpression("floor(3.7)", { lookup: () => undefined });
|
||||
const result = evaluateExpression("floor(3.7)", {
|
||||
lookup: () => undefined,
|
||||
});
|
||||
expect(result.value).toBe(3);
|
||||
});
|
||||
|
||||
@@ -323,7 +329,9 @@ describe("evaluateExpression", () => {
|
||||
});
|
||||
|
||||
test("evaluates round function", () => {
|
||||
const result = evaluateExpression("round(3.5)", { lookup: () => undefined });
|
||||
const result = evaluateExpression("round(3.5)", {
|
||||
lookup: () => undefined,
|
||||
});
|
||||
expect(result.value).toBe(4);
|
||||
});
|
||||
|
||||
@@ -424,9 +432,7 @@ describe("var-reactivity", () => {
|
||||
test("detects self-referencing circular dependency", () => {
|
||||
expect(() =>
|
||||
initReactivity({
|
||||
declarations: [
|
||||
{ key: "$a", expression: "$a + 1" },
|
||||
],
|
||||
declarations: [{ key: "$a", expression: "$a + 1" }],
|
||||
tagModifiers: [],
|
||||
}),
|
||||
).toThrow("Circular dependency");
|
||||
@@ -527,14 +533,28 @@ describe("var-reactivity", () => {
|
||||
initReactivity({
|
||||
declarations: [],
|
||||
tagModifiers: [
|
||||
{ tag: "#warrior", target: "$mod_hp", expression: "20", threshold: 1 },
|
||||
{ tag: "#warrior", target: "$mod_str", expression: "5", threshold: 1 },
|
||||
{
|
||||
tag: "#warrior",
|
||||
target: "$mod_hp",
|
||||
expression: "20",
|
||||
threshold: 1,
|
||||
},
|
||||
{
|
||||
tag: "#warrior",
|
||||
target: "$mod_str",
|
||||
expression: "5",
|
||||
threshold: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Set $class to #warrior:1 — should activate both modifiers
|
||||
setBase("$class", "#warrior:1");
|
||||
const cascade = computeCascade("$class", undefined, store({ $class: "#warrior:1" }));
|
||||
const cascade = computeCascade(
|
||||
"$class",
|
||||
undefined,
|
||||
store({ $class: "#warrior:1" }),
|
||||
);
|
||||
|
||||
// Should produce combined values for both targets
|
||||
const modHp = cascade.find((r) => r.key === "$mod_hp");
|
||||
@@ -547,7 +567,12 @@ describe("var-reactivity", () => {
|
||||
initReactivity({
|
||||
declarations: [],
|
||||
tagModifiers: [
|
||||
{ tag: "#warrior", target: "$mod_hp", expression: "20", threshold: 1 },
|
||||
{
|
||||
tag: "#warrior",
|
||||
target: "$mod_hp",
|
||||
expression: "20",
|
||||
threshold: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -572,7 +597,12 @@ describe("var-reactivity", () => {
|
||||
initReactivity({
|
||||
declarations: [],
|
||||
tagModifiers: [
|
||||
{ tag: "#warrior", target: "$mod_hp", expression: "20", threshold: 1 },
|
||||
{
|
||||
tag: "#warrior",
|
||||
target: "$mod_hp",
|
||||
expression: "20",
|
||||
threshold: 1,
|
||||
},
|
||||
{ tag: "#mage", target: "$mod_hp", expression: "10", threshold: 1 },
|
||||
],
|
||||
});
|
||||
@@ -600,19 +630,32 @@ describe("var-reactivity", () => {
|
||||
initReactivity({
|
||||
declarations: [],
|
||||
tagModifiers: [
|
||||
{ tag: "#warrior", target: "$mod_hp", expression: "20", threshold: 2 },
|
||||
{
|
||||
tag: "#warrior",
|
||||
target: "$mod_hp",
|
||||
expression: "20",
|
||||
threshold: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Count 1 < threshold 2 — should NOT activate
|
||||
setBase("$class", "#warrior:1");
|
||||
const cascade1 = computeCascade("$class", undefined, store({ $class: "#warrior:1" }));
|
||||
const cascade1 = computeCascade(
|
||||
"$class",
|
||||
undefined,
|
||||
store({ $class: "#warrior:1" }),
|
||||
);
|
||||
const modHp1 = cascade1.find((r) => r.key === "$mod_hp");
|
||||
expect(modHp1).toBeUndefined();
|
||||
|
||||
// Increase to count 2 >= threshold 2 — should activate
|
||||
setBase("$class", "#warrior:2");
|
||||
const cascade2 = computeCascade("$class", "#warrior:1", store({ $class: "#warrior:2" }));
|
||||
const cascade2 = computeCascade(
|
||||
"$class",
|
||||
"#warrior:1",
|
||||
store({ $class: "#warrior:2" }),
|
||||
);
|
||||
const modHp2 = cascade2.find((r) => r.key === "$mod_hp");
|
||||
expect(modHp2?.value).toBe("20");
|
||||
});
|
||||
@@ -621,7 +664,12 @@ describe("var-reactivity", () => {
|
||||
initReactivity({
|
||||
declarations: [],
|
||||
tagModifiers: [
|
||||
{ tag: "#warrior", target: "$mod_hp", expression: "20", threshold: 2 },
|
||||
{
|
||||
tag: "#warrior",
|
||||
target: "$mod_hp",
|
||||
expression: "20",
|
||||
threshold: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -645,13 +693,22 @@ describe("var-reactivity", () => {
|
||||
initReactivity({
|
||||
declarations: [],
|
||||
tagModifiers: [
|
||||
{ tag: "#warrior", target: "$mod_hp", expression: "20", threshold: 1 },
|
||||
{
|
||||
tag: "#warrior",
|
||||
target: "$mod_hp",
|
||||
expression: "20",
|
||||
threshold: 1,
|
||||
},
|
||||
{ tag: "#druid", target: "$mod_mp", expression: "15", threshold: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
setBase("$class", "#warrior:2;#druid:1");
|
||||
const cascade = computeCascade("$class", undefined, store({ $class: "#warrior:2;#druid:1" }));
|
||||
const cascade = computeCascade(
|
||||
"$class",
|
||||
undefined,
|
||||
store({ $class: "#warrior:2;#druid:1" }),
|
||||
);
|
||||
|
||||
const modHp = cascade.find((r) => r.key === "$mod_hp");
|
||||
const modMp = cascade.find((r) => r.key === "$mod_mp");
|
||||
@@ -663,9 +720,7 @@ describe("var-reactivity", () => {
|
||||
describe("computeCascade — declaration re-evaluation", () => {
|
||||
test("re-evaluates dependents when a dependency changes", () => {
|
||||
initReactivity({
|
||||
declarations: [
|
||||
{ key: "$hp", expression: "$con * 5" },
|
||||
],
|
||||
declarations: [{ key: "$hp", expression: "$con * 5" }],
|
||||
tagModifiers: [],
|
||||
});
|
||||
|
||||
@@ -701,11 +756,14 @@ describe("var-reactivity", () => {
|
||||
|
||||
test("handles tagmap transition during re-evaluation", () => {
|
||||
initReactivity({
|
||||
declarations: [
|
||||
{ key: "$class", expression: "0" },
|
||||
],
|
||||
declarations: [{ key: "$class", expression: "0" }],
|
||||
tagModifiers: [
|
||||
{ tag: "#warrior", target: "$mod_hp", expression: "20", threshold: 1 },
|
||||
{
|
||||
tag: "#warrior",
|
||||
target: "$mod_hp",
|
||||
expression: "20",
|
||||
threshold: 1,
|
||||
},
|
||||
{ tag: "#novice", target: "$mod_hp", expression: "5", threshold: 1 },
|
||||
],
|
||||
});
|
||||
@@ -744,7 +802,12 @@ describe("var-reactivity", () => {
|
||||
initReactivity({
|
||||
declarations: [],
|
||||
tagModifiers: [
|
||||
{ tag: "#warrior", target: "$mod_hp", expression: "20", threshold: 1 },
|
||||
{
|
||||
tag: "#warrior",
|
||||
target: "$mod_hp",
|
||||
expression: "20",
|
||||
threshold: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -861,7 +924,7 @@ describe("parseInput", () => {
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// directive-scanner
|
||||
// content-registry (spark tables)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("inspectSparkTableCsv", () => {
|
||||
@@ -915,13 +978,11 @@ describe("buildSparkTableCompletion", () => {
|
||||
test("builds completion from CSV data", () => {
|
||||
const csv = `d6,Name,Description
|
||||
1,Alice,The brave`;
|
||||
const slugger = new Slugger();
|
||||
const result = buildSparkTableCompletion(
|
||||
csv,
|
||||
"/rules/combat.md",
|
||||
"/rules/combat/test.csv",
|
||||
false,
|
||||
slugger,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.notation).toBe("d6");
|
||||
@@ -933,13 +994,11 @@ describe("buildSparkTableCompletion", () => {
|
||||
test("returns null for non-spark CSV", () => {
|
||||
const csv = `Name,Value
|
||||
Alice,10`;
|
||||
const slugger = new Slugger();
|
||||
const result = buildSparkTableCompletion(
|
||||
csv,
|
||||
"/test.md",
|
||||
"/test.csv",
|
||||
false,
|
||||
slugger,
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
@@ -947,13 +1006,11 @@ Alice,10`;
|
||||
test("sets remix flag", () => {
|
||||
const csv = `d6,Result
|
||||
1,Yes`;
|
||||
const slugger = new Slugger();
|
||||
const result = buildSparkTableCompletion(
|
||||
csv,
|
||||
"/test.md",
|
||||
"/test.csv",
|
||||
true,
|
||||
slugger,
|
||||
);
|
||||
expect(result!.remix).toBe(true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user