/** * 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 and headings. * * The fetch runs eagerly on module import. Call `useJournalCompletions()` * from any Solid component to reactively read the state. */ import { createSignal } from "solid-js"; import { extractHeadings } from "../../data-loader/toc"; import { getPathsByExtension, getIndexedData, } from "../../data-loader/file-index"; // ------------------- Types (mirrors CLI) ------------------- export interface DiceCompletion { label: string; notation: string; source: string; } export interface LinkCompletion { path: string; label: string; section: string | null; } export interface JournalCompletions { dice: DiceCompletion[]; links: LinkCompletion[]; } 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", }); // ------------------- 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 : [], }; } catch { return null; } } // ------------------- Client-side fallback scan ------------------- async function scanClientSide(): Promise { const paths = await getPathsByExtension("md"); const dice: DiceCompletion[] = []; const links: LinkCompletion[] = []; const tagRegex = /]*>\s*([\s\S]*?)\s*<\/md-dice>/gi; for (const filePath of paths) { const content = await getIndexedData(filePath); if (!content) continue; // Dice scan let match: RegExpExecArray | null; tagRegex.lastIndex = 0; while ((match = tagRegex.exec(content)) !== null) { const raw = match[1].trim(); if (!raw || raw.length > 80) continue; if (!/^\d*d\d+/i.test(raw) && !/^[+-]/.test(raw)) continue; dice.push({ label: raw, notation: raw, source: filePath }); } // Link scan (headings) 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, }); } } return { dice, links }; } // ------------------- 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 }); 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 }); } 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; } /** Force a re-fetch on the next page load. For runtime, call before invalidate triggers. */ let _invalidated = false; export function invalidateCompletions(): void { _invalidated = true; // On next import (page reload), the module will re-init. // For a runtime invalidation, you could call init again. } /** * 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: [] } }; }