export type ViewerTheme = 'dark' | 'light' | undefined; export interface PageOptions { /** webview.cspSource */ cspSource: string; nonce: string; /** Forced diagram theme; undefined keeps the diagram's own choice. */ theme: ViewerTheme; /** Location hash to restore (Archify keeps focus/route state there). */ hash?: string; /** Opaque state handed to vscode.setState for panel restore. */ state?: unknown; /** Banner shown on top of the diagram. */ notice?: { text: string; kind: 'info' | 'warning' | 'error' }; /** The 3D viewer script (a webview URI), added to Archify pages. */ viewer3d?: { scriptUri: string; defaultView: '2d' | '3d' }; /** Node id → diagram path (relative to the model root); double-click opens it. */ links?: Record; /** The model page: metadata above the diagram, the diagram list and registry below. */ model?: ModelPanel; /** Node id → registry description, shown as tooltips in the Elements panel. */ descriptions?: Record; /** Registry page: the elements of one .archify-registry file. */ registry?: RegistryPanel; /** Decision page: one .archify-decision file. */ decision?: DecisionPanel; /** Diagram page: decisions of this diagram, for the Decisions rail panel. */ decisions?: DiagramDecision[]; /** Pages inside a model get a switch to the model's Graph page; `left` names the page they are on. */ graphSwitch?: { left: 'Model' | 'Diagram'; query?: string }; /** The Graph page: the model as one graph, the bundled script that draws it, and the query to start with. */ graph?: { graph: ModelGraph; query?: string; scriptUri: string }; } import type { ModelGraph } from '../model/graph'; import type { DecisionPanel, ModelPanel, RegistryPanel } from '../model/panel'; import type { DiagramDecision } from '../model/prepare'; export function makeNonce(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let nonce = ''; for (let i = 0; i < 32; i += 1) nonce += chars.charAt(Math.floor(Math.random() * chars.length)); return nonce; } export function escapeHtml(value: string): string { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** JSON that is safe to embed inside an inline \n` + `\n`; let page = html // Archify calls its node rail "Node index"; next to the registry it reads better as Elements. .replace(/(id="node-outline-title">)Node index(<)/, '$1Elements$2') .replace(/("viewer\.outline\.title":")Node index(")/, '$1Elements$2') // Drop any CSP the file brings along; ours is the one that applies. .replace(/]*>/gi, '') // Allow the page's own inline scripts to run under the nonce policy. .replace(/]*\bnonce=)/gi, `\n`; page = /<\/head>/i.test(page) ? page.replace(/<\/head>/i, `${tag}`) : `${tag}${page}`; } const headMatch = /]*>/i.exec(page); if (headMatch) { const at = headMatch.index + headMatch[0].length; page = `${page.slice(0, at)}\n${head}${page.slice(at)}`; } else { page = `${head}${page}`; } return page; } /** The model page for a main.archify without a diagram of its own. */ export function modelPage(options: Pick, model: ModelPanel): string { const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, model, links: {}, graphSwitch: { left: 'Model' } }); return ` `; } /** The Graph page of a model: the bundled script builds everything from the host's graph data. */ export function graphPage(options: Pick, graph: NonNullable): string { const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, links: {}, graph }); return ` `; } /** The registry page for one .archify-registry file. */ export function registryPage(options: Pick, registry: RegistryPanel): string { const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, registry, links: {} }); return ` `; } /** The decision page for one .archify-decision file. */ export function decisionPage(options: Pick, decision: DecisionPanel): string { const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, decision, links: {} }); return ` `; } /** A small VS Code-styled page for loading and error states. */ export function messagePage( options: Pick, title: string, body: string, detail?: string, ): string { return `

${escapeHtml(title)}

${escapeHtml(body)}

${detail ? `
${escapeHtml(detail)}
` : ''} `; }