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:
+51
-131
@@ -24,7 +24,6 @@ import {
|
||||
import {
|
||||
FENCED_BLOCK_RE,
|
||||
parseBlockAttrs,
|
||||
resolveBlockAs,
|
||||
} from "./completions/block-scanner.js";
|
||||
import type {
|
||||
CompletionsPayload,
|
||||
@@ -47,8 +46,6 @@ export interface DocContent {
|
||||
body: string;
|
||||
/** Origin role that produced this content, for debugging. */
|
||||
role?: string;
|
||||
/** Origin `as` value, for debugging. */
|
||||
as?: string;
|
||||
}
|
||||
|
||||
export interface ContentRegistry {
|
||||
@@ -113,83 +110,73 @@ export interface DocScanResult {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* - collects inline content (role=file, md-* bodies, declare)
|
||||
* into the doc's content store
|
||||
* - collects inline content (spark-table, declare, file) into the doc's
|
||||
* content store
|
||||
*
|
||||
* Does NOT touch the path index — the caller assembles the registry.
|
||||
*/
|
||||
export function scanDoc(content: string, docPath: string): DocScanResult {
|
||||
const contentStore: Record<string, DocContent> = {};
|
||||
|
||||
// ---- Pass 1: attributed fenced code blocks ----
|
||||
const stripped = content.replace(
|
||||
FENCED_BLOCK_RE,
|
||||
(
|
||||
_match: string,
|
||||
match: string,
|
||||
lang: string,
|
||||
infoString: string,
|
||||
body: string,
|
||||
): string => {
|
||||
const attrs = parseBlockAttrs(infoString);
|
||||
attrs.lang = attrs.lang || lang;
|
||||
const effectiveAs = resolveBlockAs(attrs.role, attrs.as);
|
||||
|
||||
if (attrs.role === "declare") {
|
||||
const id = deriveContentId("declare", body, attrs.id);
|
||||
contentStore[id] = {
|
||||
id,
|
||||
kind: "declare",
|
||||
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);
|
||||
switch (attrs.role) {
|
||||
case "declare": {
|
||||
const id = deriveContentId("declare", body, attrs.id);
|
||||
contentStore[id] = { id, kind: "declare", body, role: attrs.role };
|
||||
return "";
|
||||
}
|
||||
const id = deriveContentId("csv", blockBody, attrs.id);
|
||||
contentStore[id] = {
|
||||
id,
|
||||
kind: "csv",
|
||||
body: blockBody,
|
||||
role: attrs.role,
|
||||
as: effectiveAs,
|
||||
};
|
||||
|
||||
const extra = { ...attrs.extra };
|
||||
const extraStr = Object.keys(extra).length
|
||||
? `{${Object.entries(extra)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(" ")}}`
|
||||
: "";
|
||||
return `:${effectiveAs}[./${id}]${extraStr}`;
|
||||
case "file": {
|
||||
const id = deriveContentId("text", body, attrs.id);
|
||||
contentStore[id] = { id, kind: "text", body, role: attrs.role };
|
||||
return "";
|
||||
}
|
||||
|
||||
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
|
||||
* folder scan (`scanClientSide` in `components/journal/completions.ts`) so
|
||||
* 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
|
||||
* `scanDoc` and the spark-injection pass, other extensions are stored raw.
|
||||
* `scanDoc`, other extensions are stored raw.
|
||||
*/
|
||||
export function buildRegistryFromIndex(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -334,57 +313,6 @@ function markdownTableBodyToCsv(body: string, docPath: string): string {
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -636,14 +564,6 @@ export function inspectSparkTableCsv(csv: string): string[] | null {
|
||||
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. */
|
||||
export function buildSparkTableCompletion(
|
||||
csv: string,
|
||||
|
||||
Reference in New Issue
Block a user