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' }; } 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 // 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; } /** 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)}
` : ''} `; }