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; /** Registry page: the elements of one .archify-registry file. */ registry?: RegistryPanel; } import type { ModelPanel, RegistryPanel } from '../model/panel'; 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: {} }); 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 ` `; } /** 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)}
` : ''} `; }