refactor: remove as= override and render data-spark at runtime
- Drop resolveBlockAs and the as= attribute; scanDoc now switches directly on role and warns on unknown roles instead of silently stripping - md-table derives data-spark from its loaded CSV via parseSparkTableCsv; delete injectSparkDirectives and the second registry pass so pathIndex is exactly scanDoc output - Align spark-table dice header regex with isSparkTableHeader
This commit is contained in:
@@ -2,11 +2,15 @@
|
|||||||
* Shared block scanning utilities — safe for both CLI and browser.
|
* Shared block scanning utilities — safe for both CLI and browser.
|
||||||
*
|
*
|
||||||
* Parses fenced code blocks with attributes:
|
* Parses fenced code blocks with attributes:
|
||||||
* ```lang id=xxx role=xxx as=xxx
|
* ```lang id=xxx role=xxx key=value
|
||||||
*
|
*
|
||||||
* - `role` dispatches to content scanners (declare, spark-table)
|
* The `role` fully determines block behavior (see `scanDoc` in
|
||||||
* - `as` controls rendering (none, codeblock, md-table, md-card, md-dice)
|
* `content-registry.ts`):
|
||||||
* - `id` is used for cross-references (file paths)
|
* - spark-table → stored as CSV, rendered as an `:md-table` directive
|
||||||
|
* - declare / file → stored, block stripped
|
||||||
|
* - tag → block kept intact for the yaml-tag render extension
|
||||||
|
* - unknown role → warned about and stripped
|
||||||
|
* - no role → kept as a visible code block
|
||||||
*
|
*
|
||||||
* Attribute parsing itself lives in `src/markdown/block-attrs.ts` so render
|
* Attribute parsing itself lives in `src/markdown/block-attrs.ts` so render
|
||||||
* extensions can share the same syntax.
|
* extensions can share the same syntax.
|
||||||
@@ -19,28 +23,4 @@ export { parseBlockAttrs, type BlockAttrs } from "../../markdown/block-attrs.js"
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/** Matches fenced code blocks with an info string (at least one attr). */
|
/** Matches fenced code blocks with an info string (at least one attr). */
|
||||||
export const FENCED_BLOCK_RE = /^```(\w*)\s+(\S.*?)\s*\n([\s\S]*?)```\s*$/gm;
|
export const FENCED_BLOCK_RE = /^```(\w*)\s+(\S.*?)\s*\n([\s\S]*?)```\s*$/gm;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// as resolution
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the effective `as` value.
|
|
||||||
*
|
|
||||||
* Defaults:
|
|
||||||
* - role is set, no explicit as → "none" (strip — it's metadata)
|
|
||||||
* - role=spark-table → "md-table" (render as md-table directive)
|
|
||||||
* - role=tag → "codeblock" (kept for the marked yaml-tag extension)
|
|
||||||
* - no role, no as → "codeblock" (keep as visible code block)
|
|
||||||
*/
|
|
||||||
export function resolveBlockAs(role: string | undefined, as: string | undefined): string {
|
|
||||||
if (as) return as;
|
|
||||||
// spark-table blocks render as md-table by default
|
|
||||||
if (role === "spark-table") return "md-table";
|
|
||||||
// yaml-defined tag blocks must survive stripping so the
|
|
||||||
// code-block-yaml-tag marked extension can render them
|
|
||||||
if (role === "tag") return "codeblock";
|
|
||||||
if (role) return "none";
|
|
||||||
return "codeblock";
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,7 @@ jest.mock("github-slugger", () => {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
import { parseDeclareCsv } from "./declare-parser";
|
import { parseDeclareCsv } from "./declare-parser";
|
||||||
import { parseBlockAttrs, resolveBlockAs } from "./block-scanner";
|
import { parseBlockAttrs } from "./block-scanner";
|
||||||
import { evaluateExpression, exprValueToString } from "../../components/journal/variable-expression";
|
import { evaluateExpression, exprValueToString } from "../../components/journal/variable-expression";
|
||||||
import {
|
import {
|
||||||
initReactivity,
|
initReactivity,
|
||||||
@@ -220,10 +220,9 @@ describe("parseBlockAttrs", () => {
|
|||||||
// parseBlockAttrs receives the info string AFTER the lang.
|
// parseBlockAttrs receives the info string AFTER the lang.
|
||||||
// The lang is extracted from the fenced block regex capture group
|
// The lang is extracted from the fenced block regex capture group
|
||||||
// and applied separately in content-registry.
|
// and applied separately in content-registry.
|
||||||
const attrs = parseBlockAttrs("id=stats role=declare as=none");
|
const attrs = parseBlockAttrs("id=stats role=declare");
|
||||||
expect(attrs.id).toBe("stats");
|
expect(attrs.id).toBe("stats");
|
||||||
expect(attrs.role).toBe("declare");
|
expect(attrs.role).toBe("declare");
|
||||||
expect(attrs.as).toBe("none");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parses quoted values", () => {
|
test("parses quoted values", () => {
|
||||||
@@ -242,7 +241,6 @@ describe("parseBlockAttrs", () => {
|
|||||||
expect(attrs.lang).toBe("");
|
expect(attrs.lang).toBe("");
|
||||||
expect(attrs.id).toBeUndefined();
|
expect(attrs.id).toBeUndefined();
|
||||||
expect(attrs.role).toBeUndefined();
|
expect(attrs.role).toBeUndefined();
|
||||||
expect(attrs.as).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles empty info string (lang extracted separately)", () => {
|
test("handles empty info string (lang extracted separately)", () => {
|
||||||
@@ -252,28 +250,8 @@ describe("parseBlockAttrs", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveBlockAs", () => {
|
// Role behavior is covered by the scanDoc tests in
|
||||||
test("returns explicit as value", () => {
|
// src/cli/content-registry.test.ts.
|
||||||
expect(resolveBlockAs("declare", "codeblock")).toBe("codeblock");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("defaults to none when role is set", () => {
|
|
||||||
expect(resolveBlockAs("declare", undefined)).toBe("none");
|
|
||||||
expect(resolveBlockAs("file", undefined)).toBe("none");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("defaults to md-table for spark-table role", () => {
|
|
||||||
expect(resolveBlockAs("spark-table", undefined)).toBe("md-table");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("defaults to codeblock for tag role", () => {
|
|
||||||
expect(resolveBlockAs("tag", undefined)).toBe("codeblock");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("defaults to codeblock when no role and no as", () => {
|
|
||||||
expect(resolveBlockAs(undefined, undefined)).toBe("codeblock");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// variable-expression
|
// variable-expression
|
||||||
|
|||||||
@@ -80,6 +80,48 @@ describe("scanDoc", () => {
|
|||||||
expect(stripped).toBe(md);
|
expect(stripped).toBe(md);
|
||||||
expect(Object.keys(content)).toHaveLength(0);
|
expect(Object.keys(content)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("tag blocks are kept intact", () => {
|
||||||
|
const md = "```yaml role=tag\ntag: md-deck\nbody: ./cards.csv\n```";
|
||||||
|
const { stripped, content } = scanDoc(md, "test.md");
|
||||||
|
|
||||||
|
expect(stripped).toBe(md);
|
||||||
|
expect(Object.keys(content)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("blocks without role are kept as code blocks", () => {
|
||||||
|
const md = "```csv\nlabel,body\n1,text\n```";
|
||||||
|
const { stripped, content } = scanDoc(md, "test.md");
|
||||||
|
|
||||||
|
expect(stripped).toBe(md);
|
||||||
|
expect(Object.keys(content)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unknown roles warn and strip", () => {
|
||||||
|
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
const md = "```csv role=frobnicate\nlabel,body\n```";
|
||||||
|
const { stripped, content } = scanDoc(md, "test.md");
|
||||||
|
|
||||||
|
expect(stripped).toBe("");
|
||||||
|
expect(Object.keys(content)).toHaveLength(0);
|
||||||
|
expect(warn).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('unknown role "frobnicate"'),
|
||||||
|
);
|
||||||
|
warn.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("legacy as= attribute is ignored", () => {
|
||||||
|
const md = [
|
||||||
|
"```csv role=spark-table as=codeblock",
|
||||||
|
"d6,Name",
|
||||||
|
"1,Alice",
|
||||||
|
"```",
|
||||||
|
].join("\n");
|
||||||
|
const { stripped } = scanDoc(md, "test.md");
|
||||||
|
|
||||||
|
// Role wins — the block still becomes a directive.
|
||||||
|
expect(stripped).toMatch(/^:md-table\[\.\/csv_/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveContent", () => {
|
describe("resolveContent", () => {
|
||||||
@@ -91,7 +133,6 @@ describe("resolveContent", () => {
|
|||||||
kind: "csv",
|
kind: "csv",
|
||||||
body: "d6,Name\n1,Alice",
|
body: "d6,Name\n1,Alice",
|
||||||
role: "spark-table",
|
role: "spark-table",
|
||||||
as: "md-table",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+51
-131
@@ -24,7 +24,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
FENCED_BLOCK_RE,
|
FENCED_BLOCK_RE,
|
||||||
parseBlockAttrs,
|
parseBlockAttrs,
|
||||||
resolveBlockAs,
|
|
||||||
} from "./completions/block-scanner.js";
|
} from "./completions/block-scanner.js";
|
||||||
import type {
|
import type {
|
||||||
CompletionsPayload,
|
CompletionsPayload,
|
||||||
@@ -47,8 +46,6 @@ export interface DocContent {
|
|||||||
body: string;
|
body: string;
|
||||||
/** Origin role that produced this content, for debugging. */
|
/** Origin role that produced this content, for debugging. */
|
||||||
role?: string;
|
role?: string;
|
||||||
/** Origin `as` value, for debugging. */
|
|
||||||
as?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContentRegistry {
|
export interface ContentRegistry {
|
||||||
@@ -113,83 +110,73 @@ export interface DocScanResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a single markdown doc:
|
* Process a single markdown doc:
|
||||||
* - strips/replaces attributed fenced code blocks based on `as`
|
* - dispatches attributed fenced code blocks by `role`
|
||||||
* - converts `markdown`-lang `role=spark-table` bodies (pipe tables) to CSV
|
* - converts `markdown`-lang `role=spark-table` bodies (pipe tables) to CSV
|
||||||
* - collects inline content (role=file, md-* bodies, declare)
|
* - collects inline content (spark-table, declare, file) into the doc's
|
||||||
* into the doc's content store
|
* content store
|
||||||
*
|
*
|
||||||
* Does NOT touch the path index — the caller assembles the registry.
|
* Does NOT touch the path index — the caller assembles the registry.
|
||||||
*/
|
*/
|
||||||
export function scanDoc(content: string, docPath: string): DocScanResult {
|
export function scanDoc(content: string, docPath: string): DocScanResult {
|
||||||
const contentStore: Record<string, DocContent> = {};
|
const contentStore: Record<string, DocContent> = {};
|
||||||
|
|
||||||
// ---- Pass 1: attributed fenced code blocks ----
|
|
||||||
const stripped = content.replace(
|
const stripped = content.replace(
|
||||||
FENCED_BLOCK_RE,
|
FENCED_BLOCK_RE,
|
||||||
(
|
(
|
||||||
_match: string,
|
match: string,
|
||||||
lang: string,
|
lang: string,
|
||||||
infoString: string,
|
infoString: string,
|
||||||
body: string,
|
body: string,
|
||||||
): string => {
|
): string => {
|
||||||
const attrs = parseBlockAttrs(infoString);
|
const attrs = parseBlockAttrs(infoString);
|
||||||
attrs.lang = attrs.lang || lang;
|
attrs.lang = attrs.lang || lang;
|
||||||
const effectiveAs = resolveBlockAs(attrs.role, attrs.as);
|
|
||||||
|
|
||||||
if (attrs.role === "declare") {
|
switch (attrs.role) {
|
||||||
const id = deriveContentId("declare", body, attrs.id);
|
case "declare": {
|
||||||
contentStore[id] = {
|
const id = deriveContentId("declare", body, attrs.id);
|
||||||
id,
|
contentStore[id] = { id, kind: "declare", body, role: attrs.role };
|
||||||
kind: "declare",
|
return "";
|
||||||
body,
|
|
||||||
role: attrs.role,
|
|
||||||
as: effectiveAs,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attrs.role === "file") {
|
|
||||||
const id = deriveContentId("text", body, attrs.id);
|
|
||||||
contentStore[id] = {
|
|
||||||
id,
|
|
||||||
kind: "text",
|
|
||||||
body,
|
|
||||||
role: attrs.role,
|
|
||||||
as: effectiveAs,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (effectiveAs === "codeblock") {
|
|
||||||
return _match;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (effectiveAs === "none") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (effectiveAs.startsWith("md-")) {
|
|
||||||
let blockBody = body;
|
|
||||||
if (attrs.role === "spark-table" && isMarkdownTableLang(attrs.lang)) {
|
|
||||||
blockBody = markdownTableBodyToCsv(body, docPath);
|
|
||||||
}
|
}
|
||||||
const id = deriveContentId("csv", blockBody, attrs.id);
|
|
||||||
contentStore[id] = {
|
|
||||||
id,
|
|
||||||
kind: "csv",
|
|
||||||
body: blockBody,
|
|
||||||
role: attrs.role,
|
|
||||||
as: effectiveAs,
|
|
||||||
};
|
|
||||||
|
|
||||||
const extra = { ...attrs.extra };
|
case "file": {
|
||||||
const extraStr = Object.keys(extra).length
|
const id = deriveContentId("text", body, attrs.id);
|
||||||
? `{${Object.entries(extra)
|
contentStore[id] = { id, kind: "text", body, role: attrs.role };
|
||||||
.map(([k, v]) => `${k}=${v}`)
|
return "";
|
||||||
.join(" ")}}`
|
}
|
||||||
: "";
|
|
||||||
return `:${effectiveAs}[./${id}]${extraStr}`;
|
case "spark-table": {
|
||||||
|
let blockBody = body;
|
||||||
|
if (isMarkdownTableLang(attrs.lang)) {
|
||||||
|
blockBody = markdownTableBodyToCsv(body, docPath);
|
||||||
|
}
|
||||||
|
const id = deriveContentId("csv", blockBody, attrs.id);
|
||||||
|
contentStore[id] = {
|
||||||
|
id,
|
||||||
|
kind: "csv",
|
||||||
|
body: blockBody,
|
||||||
|
role: attrs.role,
|
||||||
|
};
|
||||||
|
|
||||||
|
const extraStr = Object.entries(attrs.extra)
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join(" ");
|
||||||
|
return `:md-table[./${id}]${extraStr ? `{${extraStr}}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "tag":
|
||||||
|
// Kept intact so the code-block-yaml-tag render extension sees it.
|
||||||
|
return match;
|
||||||
|
|
||||||
|
case undefined:
|
||||||
|
// No role declared — plain visible code block.
|
||||||
|
return match;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(
|
||||||
|
`[content-registry] ${docPath}: unknown role "${attrs.role}" — stripping block`,
|
||||||
|
);
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -202,10 +189,12 @@ export function scanDoc(content: string, docPath: string): DocScanResult {
|
|||||||
* Shared by the CLI (`buildRegistry` in `commands/serve.ts`) and the browser
|
* Shared by the CLI (`buildRegistry` in `commands/serve.ts`) and the browser
|
||||||
* folder scan (`scanClientSide` in `components/journal/completions.ts`) so
|
* folder scan (`scanClientSide` in `components/journal/completions.ts`) so
|
||||||
* both modes produce identical `pathIndex`/`docContent` — including the
|
* both modes produce identical `pathIndex`/`docContent` — including the
|
||||||
* `data-spark` injection pass that only the CLI used to run.
|
* Shared by the CLI (`buildRegistry` in `commands/serve.ts`) and the browser
|
||||||
|
* folder scan (`scanClientSide` in `components/journal/completions.ts`) so
|
||||||
|
* both modes produce identical `pathIndex`/`docContent`.
|
||||||
*
|
*
|
||||||
* Keys are normalized via `normalizePathKey`; `.md` files are run through
|
* Keys are normalized via `normalizePathKey`; `.md` files are run through
|
||||||
* `scanDoc` and the spark-injection pass, other extensions are stored raw.
|
* `scanDoc`, other extensions are stored raw.
|
||||||
*/
|
*/
|
||||||
export function buildRegistryFromIndex(
|
export function buildRegistryFromIndex(
|
||||||
index: Record<string, string>,
|
index: Record<string, string>,
|
||||||
@@ -223,16 +212,6 @@ export function buildRegistryFromIndex(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject data-spark into real-file spark table directives (idempotent).
|
|
||||||
for (const [relPath, content] of Object.entries(registry.pathIndex)) {
|
|
||||||
if (!relPath.endsWith(".md")) continue;
|
|
||||||
registry.pathIndex[relPath] = injectSparkDirectives(
|
|
||||||
content,
|
|
||||||
relPath,
|
|
||||||
registry,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,57 +313,6 @@ function markdownTableBodyToCsv(body: string, docPath: string): string {
|
|||||||
return markdownTableToCsv(headerRow, separatorRow, rows) ?? body;
|
return markdownTableToCsv(headerRow, separatorRow, rows) ?? body;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Directive spark injection (real-file `:md-table` / `:md-card` references)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan a doc's stripped content for `:md-table[...]` / `:md-card[...]`
|
|
||||||
* directives that resolve to a spark table, and inject `data-spark` so the
|
|
||||||
* reveal feature can match them. Idempotent — only injects when missing.
|
|
||||||
*
|
|
||||||
* Returns the possibly-rewritten content.
|
|
||||||
*/
|
|
||||||
export function injectSparkDirectives(
|
|
||||||
content: string,
|
|
||||||
docPath: string,
|
|
||||||
registry: ContentRegistry,
|
|
||||||
): string {
|
|
||||||
const tableDirectiveRegex = /:md-(table|card)\[([^[\]]+)\](?:\{([^}]*)\})?/gi;
|
|
||||||
let rewritten = content;
|
|
||||||
let m: RegExpExecArray | null;
|
|
||||||
|
|
||||||
while ((m = tableDirectiveRegex.exec(content)) !== null) {
|
|
||||||
const [, , ref, extraStr] = m;
|
|
||||||
if (extraStr && extraStr.includes("data-spark=")) continue;
|
|
||||||
|
|
||||||
const csv = resolveContent(registry, docPath, ref);
|
|
||||||
if (!csv) continue;
|
|
||||||
|
|
||||||
const slug = sparkSlug(csv);
|
|
||||||
if (!slug) continue;
|
|
||||||
|
|
||||||
const fullMatch = m[0];
|
|
||||||
const insertPos = fullMatch.indexOf("]") + 1;
|
|
||||||
const before = fullMatch.slice(0, insertPos);
|
|
||||||
const after = fullMatch.slice(insertPos);
|
|
||||||
|
|
||||||
let replacement: string;
|
|
||||||
if (after.startsWith("{")) {
|
|
||||||
replacement = before + after.replace(/^\{/, `{data-spark="${slug}" `);
|
|
||||||
} else {
|
|
||||||
replacement = before + `{data-spark="${slug}"}` + after;
|
|
||||||
}
|
|
||||||
|
|
||||||
rewritten =
|
|
||||||
rewritten.slice(0, m.index) +
|
|
||||||
replacement +
|
|
||||||
rewritten.slice(m.index + fullMatch.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewritten;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Resolution
|
// Resolution
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -636,14 +564,6 @@ export function inspectSparkTableCsv(csv: string): string[] | null {
|
|||||||
return headers.slice(1);
|
return headers.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compute the spark slug for a CSV body, or null if it's not a spark table. */
|
|
||||||
function sparkSlug(csv: string): string | null {
|
|
||||||
const dataHeaders = inspectSparkTableCsv(csv);
|
|
||||||
if (!dataHeaders) return null;
|
|
||||||
const slugger = new Slugger();
|
|
||||||
return dataHeaders.map((h) => slugger.slug(h.toLowerCase())).join("-");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Build a SparkTableCompletion from a CSV body. */
|
/** Build a SparkTableCompletion from a CSV body. */
|
||||||
export function buildSparkTableCompletion(
|
export function buildSparkTableCompletion(
|
||||||
csv: string,
|
csv: string,
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
|||||||
const paths = await getPathsByExtension("md");
|
const paths = await getPathsByExtension("md");
|
||||||
|
|
||||||
// Load all .md content into a raw index, then build the registry through
|
// Load all .md content into a raw index, then build the registry through
|
||||||
// the same shared pipeline as the CLI (scanDoc + spark injection).
|
// the same shared pipeline as the CLI (scanDoc).
|
||||||
const index: Record<string, string> = {};
|
const index: Record<string, string> = {};
|
||||||
for (const filePath of paths) {
|
for (const filePath of paths) {
|
||||||
const content = await getIndexedData(filePath);
|
const content = await getIndexedData(filePath);
|
||||||
@@ -122,8 +122,8 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
|||||||
activeRegistry = registry;
|
activeRegistry = registry;
|
||||||
|
|
||||||
// Write the processed (stripped) content back into the file index so
|
// Write the processed (stripped) content back into the file index so
|
||||||
// Article/md-embed render the same content as CLI mode (spark tables
|
// Article/md-embed render the same content as CLI mode (attributed blocks
|
||||||
// coerced, attributed blocks processed, data-spark injected).
|
// processed by role).
|
||||||
for (const [path, content] of Object.entries(registry.pathIndex)) {
|
for (const [path, content] of Object.entries(registry.pathIndex)) {
|
||||||
setIndexedData(path, content);
|
setIndexedData(path, content);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import {
|
|||||||
import { parseMarkdown } from "../markdown";
|
import { parseMarkdown } from "../markdown";
|
||||||
import { parseCSVString, CSV, processVariables } from "./utils/csv-loader";
|
import { parseCSVString, CSV, processVariables } from "./utils/csv-loader";
|
||||||
import { resolveContentRef } from "./utils/resolve-content";
|
import { resolveContentRef } from "./utils/resolve-content";
|
||||||
|
import { parseSparkTableCsv } from "./utils/spark-table";
|
||||||
import {
|
import {
|
||||||
areAllLabelsNumeric,
|
areAllLabelsNumeric,
|
||||||
weightedRandomIndex,
|
weightedRandomIndex,
|
||||||
} from "./utils/weighted-random";
|
} from "./utils/weighted-random";;
|
||||||
|
|
||||||
export interface TableProps {
|
export interface TableProps {
|
||||||
roll?: boolean;
|
roll?: boolean;
|
||||||
@@ -59,16 +60,23 @@ customElement(
|
|||||||
if (content === null) {
|
if (content === null) {
|
||||||
throw new Error(`Failed to resolve table content: "${ref}"`);
|
throw new Error(`Failed to resolve table content: "${ref}"`);
|
||||||
}
|
}
|
||||||
return parseCSVString(content);
|
return content;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 当数据加载完成后更新 rows
|
// 当数据加载完成后更新 rows,并为火花表设置 data-spark(供 RevealManager
|
||||||
|
// 悬停触发 /roll)。data-spark 在渲染时派生,不在扫描时注入。
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const data = csvData();
|
const content = csvData();
|
||||||
if (data) {
|
if (!content) return;
|
||||||
// 将加载的数据赋值给 rows,CSV 类型已经包含 sourcePath 等属性
|
setRows(parseCSVString(content) as unknown as CSV<TableRow>);
|
||||||
setRows(data as unknown as CSV<TableRow>);
|
if (element) {
|
||||||
|
const meta = parseSparkTableCsv(content);
|
||||||
|
if (meta) {
|
||||||
|
element.setAttribute("data-spark", meta.slug);
|
||||||
|
} else {
|
||||||
|
element.removeAttribute("data-spark");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ export interface SparkTableMeta {
|
|||||||
// CSV parsing
|
// CSV parsing
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const DICE_HEADER_RE = /^d\d+$/i;
|
// Matches the scanner's `isSparkTableHeader` (content-registry): plain dice,
|
||||||
|
// counts, and modifiers all count as spark-table first columns.
|
||||||
|
const DICE_HEADER_RE = /^\d*d\d+(?:[+-]\d+)?$/i;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a CSV string into a SparkTableMeta.
|
* Parse a CSV string into a SparkTableMeta.
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export interface BlockAttrs {
|
|||||||
lang: string;
|
lang: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
as?: string;
|
|
||||||
/** Any other attributes not in the standard set */
|
/** Any other attributes not in the standard set */
|
||||||
extra: Record<string, string>;
|
extra: Record<string, string>;
|
||||||
}
|
}
|
||||||
@@ -24,6 +23,8 @@ export function parseBlockAttrs(info: string): BlockAttrs {
|
|||||||
attrs[m[1]] = m[2].replace(/^"|"$/g, "");
|
attrs[m[1]] = m[2].replace(/^"|"$/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { lang, id, role, as, ...extra } = attrs;
|
// `as` was a render-target override; roles now fully determine behavior,
|
||||||
return { lang: lang || "", id, role, as, extra };
|
// so it is parsed out and ignored (kept out of `extra` for directives).
|
||||||
|
const { lang, id, role, as: _legacyAs, ...extra } = attrs;
|
||||||
|
return { lang: lang || "", id, role, extra };
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user