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:
hyper
2026-08-07 12:17:35 +08:00
parent 9e081891df
commit e8aa7165cb
13 changed files with 985 additions and 848 deletions
+65 -52
View File
@@ -10,15 +10,17 @@
*/
import { createSignal } from "solid-js";
import { extractHeadings } from "../../data-loader/toc";
import {
getPathsByExtension,
getIndexedData,
setInlineResolver,
} from "../../data-loader/file-index";
import {
scanDirectives,
} from "../../cli/completions/directive-scanner";
import { scanDeclareBlocks } from "../../cli/completions/declare-parser";
scanDoc,
deriveCompletions,
resolveInlineByPath,
type ContentRegistry,
} from "../../cli/content-registry";
import type {
CompletionsPayload,
DiceCompletion,
@@ -50,6 +52,23 @@ const [completionsState, setCompletionsState] = createSignal<CompletionsState>({
status: "loading",
});
// The registry backing the completions. Populated in both CLI and client
// modes so inline content ids can be resolved at runtime (e.g. spark rolls).
let activeRegistry: ContentRegistry = { pathIndex: {}, docContent: {} };
/**
* The registry backing the current completions.
* In CLI mode this is fetched from the server; in browser mode it is built
* client-side. Used to resolve inline content ids (e.g. spark table CSVs).
*/
export function getRegistry(): ContentRegistry {
return activeRegistry;
}
// Register the inline-content resolver so `getIndexedData` can resolve
// directive refs (e.g. `./csv_abc123`) that aren't real files.
setInlineResolver((path) => resolveInlineByPath(activeRegistry, path));
// ------------------- Fetch (CLI mode) -------------------
async function tryServer(): Promise<JournalCompletions | null> {
@@ -61,66 +80,49 @@ 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 : [],
declarations: Array.isArray(data.declarations)
? data.declarations
: [],
tagModifiers: Array.isArray(data.tagModifiers)
? data.tagModifiers
: [],
declarations: Array.isArray(data.declarations) ? data.declarations : [],
tagModifiers: Array.isArray(data.tagModifiers) ? data.tagModifiers : [],
};
} catch {
return null;
}
}
/** Load the content registry from the server (CLI mode). */
async function tryServerRegistry(): Promise<void> {
try {
const resp = await fetch("/__CONTENT_REGISTRY.json");
if (!resp.ok) return;
const data = await resp.json();
activeRegistry = {
pathIndex: data.pathIndex ?? {},
docContent: data.docContent ?? {},
};
} catch {
// Registry unavailable — leave empty; client scan will populate it.
}
}
// ------------------- Client-side fallback scan -------------------
async function scanClientSide(): Promise<JournalCompletions> {
const paths = await getPathsByExtension("md");
const dice: DiceCompletion[] = [];
const links: LinkCompletion[] = [];
const sparkTables: SparkTableCompletion[] = [];
const declarations: VarDeclaration[] = [];
const tagModifiers: TagModifier[] = [];
// Build a temporary index for resolving CSV paths
const tempIndex: Record<string, string> = {};
// Build a registry from the in-memory file index, then derive completions
// through the same shared pipeline as the CLI.
const registry: ContentRegistry = { pathIndex: {}, docContent: {} };
// First pass: load all .md content into temp index
// First pass: load all .md content into the registry.
for (const filePath of paths) {
const content = await getIndexedData(filePath);
if (content) tempIndex[filePath] = content;
}
for (const filePath of paths) {
const content = tempIndex[filePath];
if (!content) continue;
// ---- Links (headings) - from original content ----
const basePath = filePath.replace(/\.md$/, "");
const fileName = basePath.split("/").filter(Boolean).pop() || basePath;
links.push({ path: basePath, label: fileName, section: null });
for (const heading of extractHeadings(content)) {
links.push({
path: basePath,
label: `${fileName} § ${heading.title}`,
section: heading.id ?? null,
});
}
// ---- Declare block scanning (shared with CLI) ----
const declareResult = scanDeclareBlocks(content, filePath);
declarations.push(...declareResult.variables);
tagModifiers.push(...declareResult.tagModifiers);
// ---- Directive scanning (dice + spark tables) ----
const fileDir = filePath.split("/").slice(0, -1).join("/") || ".";
const directiveResult = scanDirectives(content, filePath, tempIndex, fileDir);
dice.push(...directiveResult.dice);
sparkTables.push(...directiveResult.sparkTables);
const result = scanDoc(content, filePath);
registry.pathIndex[filePath] = result.stripped;
registry.docContent[filePath] = result.content;
}
return { dice, links, sparkTables, declarations, tagModifiers };
activeRegistry = registry;
return deriveCompletions(registry);
}
// ------------------- Init (runs eagerly at import time) -------------------
@@ -131,10 +133,16 @@ const _initPromise: Promise<void> = (async () => {
const serverData = await tryServer();
if (serverData) {
setCompletionsState({ status: "loaded", data: serverData });
await tryServerRegistry();
try {
initReactivity({ declarations: serverData.declarations, tagModifiers: serverData.tagModifiers });
initReactivity({
declarations: serverData.declarations,
tagModifiers: serverData.tagModifiers,
});
seedDeclaredVariables();
} catch (e) { console.warn("[completions] reactivity init error:", e); }
} catch (e) {
console.warn("[completions] reactivity init error:", e);
}
return;
}
@@ -144,9 +152,14 @@ const _initPromise: Promise<void> = (async () => {
if (data.dice.length > 0 || data.links.length > 0) {
setCompletionsState({ status: "loaded", data });
try {
initReactivity({ declarations: data.declarations, tagModifiers: data.tagModifiers });
initReactivity({
declarations: data.declarations,
tagModifiers: data.tagModifiers,
});
seedDeclaredVariables();
} catch (e) { console.warn("[completions] reactivity init error:", e); }
} catch (e) {
console.warn("[completions] reactivity init error:", e);
}
} else {
setCompletionsState({ status: "empty" });
}
@@ -201,4 +214,4 @@ function seedDeclaredVariables(): void {
for (const { key, value } of initial) {
sendMessage("var", { action: "set", key, value });
}
}
}