/** * 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, headings, * and declare blocks. * * The fetch runs eagerly on module import. Call `useJournalCompletions()` * from any Solid component to reactively read the state. */ import { createSignal } from "solid-js"; import { getPathsByExtension, getIndexedData, setIndexedData, setInlineResolver, } from "../../data-loader/file-index"; import { buildRegistryFromIndex, deriveCompletions, resolveInlineByPath, type ContentRegistry, } from "../../cli/content-registry"; import type { CompletionsPayload, DiceCompletion, LinkCompletion, SparkTableCompletion, VarDeclaration, TagModifier, } from "../../cli/completions/types"; import { initReactivity, computeInitialValues } from "./var-reactivity"; import { sendMessage, journalStreamState } from "../stores/journalStream"; export type { VarDeclaration, TagModifier }; // Re-export CLI types so consumers don't need to know about the CLI path export type { DiceCompletion, LinkCompletion, SparkTableCompletion }; /** Convenience alias — same shape as the CLI's CompletionsPayload. */ export type JournalCompletions = CompletionsPayload; export type CompletionsState = | { status: "loading" } | { status: "loaded"; data: JournalCompletions } | { status: "empty" } | { status: "error"; message: string }; // ------------------- Reactive state (module-level signal) ------------------- const [completionsState, setCompletionsState] = createSignal({ 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 { try { const resp = await fetch("/__COMPLETIONS.json"); if (!resp.ok) return null; const data = await resp.json(); return { 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 : [], }; } catch { return null; } } /** Load the content registry from the server (CLI mode). */ async function tryServerRegistry(): Promise { 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 { const paths = await getPathsByExtension("md"); // Load all .md content into a raw index, then build the registry through // the same shared pipeline as the CLI (scanDoc + spark injection). const index: Record = {}; for (const filePath of paths) { const content = await getIndexedData(filePath); if (!content) continue; index[filePath] = content; } const registry = buildRegistryFromIndex(index); activeRegistry = registry; // Write the processed (stripped) content back into the file index so // Article/md-embed render the same content as CLI mode (spark tables // coerced, attributed blocks processed, data-spark injected). for (const [path, content] of Object.entries(registry.pathIndex)) { setIndexedData(path, content); } return deriveCompletions(registry); } // ------------------- Init (runs eagerly at import time) ------------------- // Using a top-level IIFE so the promise starts immediately const _initPromise: Promise = (async () => { // 1. Try server first (CLI mode) const serverData = await tryServer(); if (serverData) { setCompletionsState({ status: "loaded", data: serverData }); await tryServerRegistry(); try { initReactivity({ declarations: serverData.declarations, tagModifiers: serverData.tagModifiers, }); seedDeclaredVariables(); } catch (e) { console.warn("[completions] reactivity init error:", e); } return; } // 2. Fall back to client-side scan (dev/browser mode) try { 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, }); seedDeclaredVariables(); } catch (e) { console.warn("[completions] reactivity init error:", e); } } else { setCompletionsState({ status: "empty" }); } } catch (e) { setCompletionsState({ status: "error", message: e instanceof Error ? e.message : "Failed to scan", }); } })(); // ------------------- Public API ------------------- /** * Returns a Promise that resolves once completions are loaded (or failed). * Useful for waiting before showing the completions dropdown. */ export function ensureCompletions(): Promise { return _initPromise; } /** * Reactive hooks for the journal input. * Returns the current completions state + a convenience `data` extractor. */ export function useJournalCompletions(): { state: CompletionsState; data: JournalCompletions; } { const s = completionsState(); if (s.status === "loaded") { return { state: s, data: s.data }; } return { state: s, data: { dice: [], links: [], sparkTables: [], declarations: [], tagModifiers: [], }, }; } // --------------------------------------------------------------------------- // Seed declared variables into the store on load // --------------------------------------------------------------------------- function seedDeclaredVariables(): void { const initial = computeInitialValues(journalStreamState.variables); for (const { key, value } of initial) { sendMessage("var", { action: "set", key, value }); } }