@ai-matrx/print 0.12.35 → 0.12.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/exam.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core.ts","../src/katex-css.generated.ts","../src/math-text.ts","../src/document/math.ts","../src/exam.ts"],"sourcesContent":["/**\n * @ai-matrx/print/core — the Block Print contract & print-window composition.\n *\n * Ported VERBATIM from matrx-frontend `lib/block-print/block-print-utils.ts`\n * (2026-08-29 state, including the `number` and `select` PrintSetting types the\n * commerce label lane added). Zero dependencies; touches `window` only at call\n * time, so the module stays import-inert and RSC-safe.\n *\n * BlockPrinter is the only interface a host system needs to know about.\n * Each printable unit exports its own BlockPrinter alongside its render\n * component; host code only ever calls print(data, variantId, settings).\n *\n * `buildFitTextScript` is the ONE auto-shrink script (the flashcards\n * FIT_TEXT pattern, parameterized by selector so the label lane shares it).\n */\n\n// THE ONE HTML escaper (duplication census row 14). Nine copies of\n// `escapeHtml` were live across the fleet and THEY DID NOT ESCAPE THE SAME\n// CHARACTERS — a string safe through one was an attribute break-out through\n// another. It moved to `@ai-matrx/kit/html-escape` with a decided character\n// set: an HTML escaper must not require this package's jspdf + html2canvas\n// graph. Not re-exported from here; import `@ai-matrx/kit/html-escape`.\nimport { downloadFile } from \"@ai-matrx/kit/download\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\nimport { KATEX_CSS, KATEX_CSS_MARKER } from \"./katex-css.generated\";\nimport { openOutbound } from \"@ai-matrx/kit/outbound\";\n\n// ─── Contract ────────────────────────────────────────────────────────────────\n\nexport interface PrintVariant {\n id: string;\n label: string;\n description?: string;\n}\n\n/**\n * A single user-configurable setting shown in the print dialog.\n * Extend the discriminated union for new control types\n * without breaking existing printers.\n */\nexport type PrintSetting =\n | {\n type: \"boolean\";\n id: string;\n label: string;\n description?: string;\n /** Default value shown when the dialog first opens */\n defaultValue: boolean;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /**\n * A numeric \"from → to\" range control, rendered as two small number inputs.\n * Writes two entries into the resolved PrintSettings map — one under fromId,\n * one under toId. Printers interpret 0 (or missing) as \"no bound\" by convention.\n */\n type: \"range\";\n /** Stable id used as React key — not written to the settings map */\n id: string;\n label: string;\n description?: string;\n /** Settings map key for the lower bound value */\n fromId: string;\n /** Settings map key for the upper bound value */\n toId: string;\n defaultFrom: number;\n defaultTo: number;\n /** Minimum accepted value for each input (inclusive). Default 0. */\n min?: number;\n /** Placeholder shown when the input is empty (0). Default \"start\" / \"end\". */\n fromPlaceholder?: string;\n toPlaceholder?: string;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /** A single numeric input (e.g. \"start at label #\", \"copies\"). */\n type: \"number\";\n id: string;\n label: string;\n description?: string;\n defaultValue: number;\n /** Minimum accepted value (inclusive). Default 0. */\n min?: number;\n /** Maximum accepted value (inclusive). Omit = unbounded. */\n max?: number;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /** A one-of-N choice rendered as a compact segmented row. */\n type: \"select\";\n id: string;\n label: string;\n description?: string;\n options: { value: string; label: string }[];\n defaultValue: string;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n };\n\n/** A resolved map of setting id → current value, passed to print(). */\nexport type PrintSettings = Record<string, boolean | string | number>;\n\n/**\n * The self-contained print capability that each block can export.\n * Host code only ever calls print(data, variantId, settings) — it never inspects internals.\n */\nexport interface BlockPrinter {\n /** Tooltip / aria-label for the print button in the block header */\n label: string;\n\n /**\n * Variants offered to the user before printing.\n * Empty array = no options dialog shown; prints immediately with defaults.\n */\n variants: PrintVariant[];\n\n /**\n * Optional user-configurable settings shown below the variant picker.\n * Omit entirely if the block has no configurable settings.\n */\n settings?: PrintSetting[];\n\n /**\n * Execute the print.\n * data — the block's structured data (serverData or parsed content)\n * variantId — which variant was chosen (undefined = default / no variants)\n * settings — resolved setting values chosen by the user\n *\n * MAY resolve to a PrintOutcome (\"opened\" | \"downloaded\") so the host can\n * tell the user when the popup-blocked download fallback fired; `void`\n * stays legal for printers that don't go through a print window.\n */\n print: (\n data: unknown,\n variantId?: string,\n settings?: PrintSettings,\n ) => void | PrintOutcome | Promise<void | PrintOutcome>;\n\n /**\n * The block as it appears INSIDE a composed print (a message's Print / Full\n * Print, a document export): the same adapter the block's own print uses,\n * returning body HTML or a picture. A frame block (an HTML page) answers\n * with an image — a composed print window runs no author scripts — or an\n * honest `{ notice }` when no picture can be made. Absent ⇒ the composer\n * prints the block's source.\n */\n toPrintHtml?: (data: unknown, context: PrintBlockContext) => PrintBlockOutput | null | Promise<PrintBlockOutput | null>;\n}\n\n/** Where a block sits when a composer asks for its print form. */\nexport interface PrintBlockContext {\n /** The registry key the block was found under (artifact type or `__kind`). */\n readonly type: string;\n /** The block's raw source text, exactly as it sits in the message. */\n readonly raw: string;\n /** The block's title, when the wire form carries one. */\n readonly title?: string | undefined;\n /** The wire form's attributes (an `<artifact>`'s `id`, `version`) — how a printer finds the block's record. */\n readonly attributes?: Readonly<Record<string, string>> | undefined;\n}\n\n/**\n * A block's print form inside a composed print. `html` is trusted body HTML\n * from a printer (escape what you interpolate); `image` is a picture (a\n * captured page, a drawn diagram); `notice` is an honest line printed in the\n * block's place when neither exists.\n */\nexport type PrintBlockOutput =\n | {\n readonly html: string;\n /**\n * The block's stylesheet. A composed print sanitizes block HTML (no\n * `<style>`, no inline styles, only `matrx-*` / `print-*` classes), so a\n * printer that styles its layout passes its CSS here: the composer puts\n * each distinct sheet once in the document head. Scope it under your own\n * `matrx-…` class.\n */\n readonly css?: string | undefined;\n }\n | { readonly image: { readonly src: string; readonly alt?: string | undefined; readonly caption?: string | undefined } }\n | { readonly notice: string };\n\n// ─── The ONE block-printer registry ───────────────────────────────────────────\n//\n// Keyed by artifact type / `__kind`. A block's own print button, the canvas\n// tab's Print and the message composer all read the same entry, so a block\n// has one print adapter with every caller. Lives on globalThis under a\n// Symbol.for key so duplicated bundles (ESM + CJS) share one registry.\n\nconst PRINTER_REGISTRY = Symbol.for(\"ai-matrx.print.block-printers\");\n\nfunction printerRegistry(): Map<string, BlockPrinter> {\n const g = globalThis as unknown as { [PRINTER_REGISTRY]?: Map<string, BlockPrinter> };\n g[PRINTER_REGISTRY] ??= new Map();\n return g[PRINTER_REGISTRY];\n}\n\n/** Registers `printer` under each key (artifact type and/or `__kind`). Returns the unregister. */\nexport function registerBlockPrinter(keys: string | readonly string[], printer: BlockPrinter): () => void {\n const map = printerRegistry();\n const list = (typeof keys === \"string\" ? [keys] : keys).map((key) => key.toLowerCase());\n for (const key of list) map.set(key, printer);\n return () => {\n for (const key of list) if (map.get(key) === printer) map.delete(key);\n };\n}\n\n/** The printer registered for an artifact type or `__kind` (case-insensitive). */\nexport function getBlockPrinter(key: string | null | undefined): BlockPrinter | undefined {\n return key ? printerRegistry().get(key.toLowerCase()) : undefined;\n}\n\n/** Every registered key — for guards and diagnostics. */\nexport function listBlockPrinterKeys(): string[] {\n return [...printerRegistry().keys()];\n}\n\n/** The HTML a composer puts in a block's place for a `PrintBlockOutput`. */\nexport function printBlockOutputHtml(output: PrintBlockOutput): string {\n if (\"html\" in output) return `<div class=\"matrx-print-block\">${output.html}</div>`;\n if (\"image\" in output) {\n const { src, alt, caption } = output.image;\n return `<figure class=\"matrx-print-block matrx-print-image\"><img src=\"${escapeHtml(src)}\" alt=\"${escapeHtml(\n alt ?? \"Picture\",\n )}\" style=\"max-width:100%;height:auto\">${caption ? `<figcaption>${escapeHtml(caption)}</figcaption>` : \"\"}</figure>`;\n }\n return `<p class=\"matrx-print-block matrx-print-notice\" style=\"border:1px dashed #94a3b8;border-radius:6px;padding:10px 12px;color:#475569;font-style:italic\">${escapeHtml(\n output.notice,\n )}</p>`;\n}\n\n// ─── Shared Utilities ─────────────────────────────────────────────────────────\n\nconst PRINT_STYLES = `\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;\n font-size: 11pt;\n line-height: 1.6;\n color: #1a1a1a;\n background: #fff;\n max-width: 860px;\n margin: 0 auto;\n padding: 28px 36px;\n }\n\n .print-actions {\n display: flex;\n gap: 10px;\n margin-bottom: 24px;\n padding-bottom: 18px;\n border-bottom: 2px solid #e5e7eb;\n }\n\n .print-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 20px;\n border-radius: 8px;\n font-size: 13px;\n font-family: inherit;\n cursor: pointer;\n border: none;\n font-weight: 600;\n }\n\n .print-btn-primary { background: #2563eb; color: #fff; }\n .print-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }\n\n h1 { font-size: 22pt; font-weight: 700; margin-bottom: 0.5em; }\n h2 { font-size: 16pt; font-weight: 700; margin: 1.2em 0 0.4em; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.2em; }\n h3 { font-size: 13pt; font-weight: 600; margin: 1em 0 0.3em; }\n p { margin-bottom: 0.7em; }\n\n table {\n width: 100%;\n border-collapse: collapse;\n margin: 1em 0;\n font-size: 10pt;\n }\n th {\n background: #1e293b;\n color: #f8fafc;\n font-weight: 600;\n text-align: left;\n padding: 7px 12px;\n border: 1px solid #334155;\n }\n td {\n padding: 6px 12px;\n border: 1px solid #e2e8f0;\n vertical-align: top;\n }\n tr:nth-child(even) td { background: #f8fafc; }\n\n @media print {\n body { max-width: 100%; padding: 0; }\n .print-actions { display: none !important; }\n th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }\n }\n`;\n\n/**\n * Wrap body HTML in a complete, styled print document.\n */\nexport function buildPrintDocument(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${escapeHtml(title)}</title>\n <style>\n${PRINT_STYLES}\n${extraStyles}\n </style>\n</head>\n<body>\n <div class=\"print-actions\">\n <button class=\"print-btn print-btn-primary\" onclick=\"window.print()\">Print / Save as PDF</button>\n <button class=\"print-btn print-btn-secondary\" onclick=\"window.close()\">Close</button>\n </div>\n <div class=\"content\">\n${bodyHtml}\n </div>\n</body>\n</html>`;\n}\n\n/**\n * How a print request actually reached the user.\n *\n * \"opened\" — a print window/tab was opened and the document written into it.\n * \"downloaded\" — the popup was blocked, so the document was downloaded as an\n * .html file instead (the contract's fallback). Hosts SHOULD\n * surface this to the user (e.g. a toast saying the print file\n * was downloaded) — a blocked popup plus a silent download is\n * indistinguishable from \"nothing happened\", which is exactly\n * how QA filed it (2026-08-30, F5/F6/F8).\n */\nexport type PrintOutcome = \"opened\" | \"downloaded\";\n\n/**\n * Open a new browser window and write a complete HTML document into it.\n * Falls back to downloading an .html file if the popup is blocked.\n *\n * Returns which path was taken so callers can tell the user when the\n * fallback fired. `windowFeatures` customizes the popup geometry (the\n * flashcards fixed-geometry variants pass a wider window).\n */\n/**\n * Every printed document leaves through here, so this is where typeset math\n * gets its stylesheet: when the page carries KaTeX output and does not already\n * carry the sheet, it is added to <head>. A page with no math is untouched,\n * byte for byte (the flashcard golden master holds every printer to that).\n */\nexport function withMathStyles(htmlDocument: string): string {\n if (!htmlDocument.includes('class=\"katex\"') || htmlDocument.includes(KATEX_CSS_MARKER)) {\n return htmlDocument;\n }\n const style = `<style data-matrx-katex>${KATEX_CSS}</style>`;\n const head = htmlDocument.search(/<\\/head>/i);\n return head >= 0\n ? htmlDocument.slice(0, head) + style + htmlDocument.slice(head)\n : style + htmlDocument;\n}\n\nexport function openPrintWindow(\n rawDocument: string,\n filename = \"print\",\n windowFeatures = \"width=920,height=720,scrollbars=yes\",\n): PrintOutcome {\n const htmlDocument = withMathStyles(rawDocument);\n const win = openOutbound(\"\", \"_blank\", windowFeatures);\n // Popup blocked — fall back to file download.\n if (!win) return openPrintWindowFallback(htmlDocument, filename);\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n return \"opened\";\n}\n\n/** A print window opened before its document is ready (see `openPendingPrintWindow`). */\nexport interface PendingPrintWindow {\n /** Write the finished document; downloads it when the popup was blocked or closed. */\n write(htmlDocument: string): PrintOutcome;\n}\n\n/**\n * Open the print window NOW — inside the click, where popup blockers allow it —\n * and write the document later, once async preparation (drawing diagrams) is\n * done. The window says \"Preparing…\" meanwhile.\n */\nexport function openPendingPrintWindow(\n filename = \"print\",\n windowFeatures = \"width=920,height=720,scrollbars=yes\",\n): PendingPrintWindow {\n const win = openOutbound(\"\", \"_blank\", windowFeatures);\n if (win) {\n win.document.open();\n win.document.write(\n '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>Preparing…</title></head><body style=\"font:14px -apple-system,BlinkMacSystemFont,sans-serif;color:#555;padding:2rem\">Preparing the document for print…</body></html>',\n );\n win.document.close();\n }\n return {\n write(rawDocument: string): PrintOutcome {\n const htmlDocument = withMathStyles(rawDocument);\n if (!win || win.closed) return openPrintWindowFallback(htmlDocument, filename);\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n return \"opened\";\n },\n };\n}\n\nfunction openPrintWindowFallback(htmlDocument: string, filename: string): PrintOutcome {\n downloadFile(`${filename.replace(/\\s+/g, \"-\").toLowerCase()}.html`, htmlDocument, \"text/html;charset=utf-8\");\n return \"downloaded\";\n}\n\n/**\n * Shorthand: build the document and open it in one call.\n * Returns the same PrintOutcome as openPrintWindow.\n */\nexport function printHtmlContent(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): PrintOutcome {\n const doc = buildPrintDocument(bodyHtml, title, extraStyles);\n return openPrintWindow(doc, title);\n}\n\n/**\n * Build the auto-shrink script that runs INSIDE a printed window: for every\n * element matching each selector, its font-size is reduced (0.5pt steps, 6pt\n * floor) until it fits its parent element's box. This is the flashcards\n * FIT_TEXT behavior verbatim, parameterized by selector so every lane\n * (flashcards card text, Avery text, label captions) shares the ONE loop.\n *\n * Pass the same selectors the original used to get byte-identical behavior:\n * flashcards passes `[\".card-text .card-text-inner\", \".avery-text-wrap .avery-text\"]`.\n */\nexport function buildFitTextScript(selectors: readonly string[]): string {\n const selectorsJson = JSON.stringify([...selectors]);\n return `\n<script>\n(function() {\n function fitText(el, container) {\n // container is the bounding box we must fit inside\n var maxW = container.clientWidth;\n var maxH = container.clientHeight;\n var size = parseFloat(window.getComputedStyle(el).fontSize);\n var minSize = 6;\n // Give the element a chance to reflow at current size before measuring\n el.style.fontSize = size + \"pt\";\n while (size > minSize && (el.scrollWidth > maxW + 1 || el.scrollHeight > maxH + 1)) {\n size -= 0.5;\n el.style.fontSize = size + \"pt\";\n }\n }\n function runAll() {\n var selectors = ${selectorsJson};\n for (var s = 0; s < selectors.length; s++) {\n var els = document.querySelectorAll(selectors[s]);\n for (var i = 0; i < els.length; i++) {\n fitText(els[i], els[i].parentElement);\n }\n }\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", runAll);\n } else {\n // document.write pages may already be loaded\n setTimeout(runAll, 0);\n }\n})();\n</script>`;\n}\n\n\n// ─── DOM print: a rendered body, as it looks ─────────────────────────────────\n\nexport interface PrintElementOptions {\n /** The print window's title (and the download fallback's file name). */\n title?: string;\n /** Open the system print dialog once the window has loaded. Default true. */\n autoPrint?: boolean;\n /** Keep buttons, toolbars and menus in the print (default false: they are chrome, not content). */\n keepInteractive?: boolean;\n}\n\n/**\n * Interactive chrome that is not content: anything marked `data-print-hide`,\n * toolbars and menus, and buttons (\"Chart this\", \"Save to\", icon actions).\n * A button stays when it IS content — marked `data-print-keep`, or a disclosure\n * trigger (`aria-expanded`) whose label is a heading — and so does anything\n * inside a `data-print-keep` region. Removal runs on the print clone only.\n */\nconst PRINT_CHROME_SELECTOR = [\n \"[data-print-hide]\",\n '[role=\"toolbar\"]',\n '[role=\"menu\"]',\n '[role=\"menubar\"]',\n \"button\",\n '[role=\"button\"]',\n 'input[type=\"button\"]',\n 'input[type=\"submit\"]',\n 'input[type=\"reset\"]',\n].join(\",\");\n\nexport function stripInteractiveChrome(root: ParentNode): number {\n let removed = 0;\n root.querySelectorAll(PRINT_CHROME_SELECTOR).forEach((node) => {\n if (!root.contains(node)) return; // already gone with a removed ancestor\n const explicit = node.hasAttribute(\"data-print-hide\");\n if (!explicit) {\n if (node.closest(\"[data-print-keep]\")) return;\n if (node.matches(\"button, [role=\\\"button\\\"]\") && node.hasAttribute(\"aria-expanded\")) return;\n }\n node.remove();\n removed += 1;\n });\n return removed;\n}\n\nconst ELEMENT_PRINT_CSS = `\n html, body { height: auto !important; min-height: 0 !important; overflow: visible !important; background: #fff !important; }\n body { margin: 0; padding: 16px; }\n *, *::before, *::after { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }\n .matrx-print-root, .matrx-print-root * { max-height: none !important; }\n .matrx-print-root [class*=\"overflow-\"], .matrx-print-root [class*=\"h-full\"], .matrx-print-root [class*=\"min-h-0\"] { overflow: visible !important; height: auto !important; }\n .matrx-print-frame-notice { border: 1px dashed #94a3b8; border-radius: 6px; padding: 10px 12px; color: #475569; font: italic 12px sans-serif; }\n @media print { .matrx-print-root * { break-inside: auto; } img, svg, figure, tr { break-inside: avoid; } }\n`;\n\n/**\n * Prints a same-origin DOM body exactly as it is drawn: the element's markup\n * with the page's own stylesheets, light theme, backgrounds kept, scroll\n * containers opened to full height. A `<canvas>` prints as its picture; an\n * `<iframe>` cannot be copied and prints an honest notice in its place (a\n * frame kind declares its own print instead). Opens inside the click.\n */\nexport function printElement(element: HTMLElement, options: PrintElementOptions = {}): PrintOutcome {\n const title = options.title ?? (document.title || \"Print\");\n const head = [...document.querySelectorAll<HTMLLinkElement | HTMLStyleElement>('link[rel=\"stylesheet\"], style')]\n .map((node) => {\n if (node instanceof HTMLLinkElement) return `<link rel=\"stylesheet\" href=\"${escapeHtml(node.href)}\">`;\n return `<style>${node.textContent ?? \"\"}</style>`;\n })\n .join(\"\\n\");\n const clone = element.cloneNode(true) as HTMLElement;\n const liveCanvases = element.querySelectorAll(\"canvas\");\n clone.querySelectorAll(\"canvas\").forEach((canvas, i) => {\n const live = liveCanvases[i];\n const img = document.createElement(\"img\");\n try {\n img.src = live ? live.toDataURL(\"image/png\") : \"\";\n } catch {\n img.alt = \"This drawing cannot be copied for print.\";\n }\n img.style.maxWidth = \"100%\";\n canvas.replaceWith(img);\n });\n clone.querySelectorAll(\"iframe\").forEach((frame) => {\n const note = document.createElement(\"p\");\n note.className = \"matrx-print-frame-notice\";\n note.textContent = \"An embedded page is here — print it from its own Print.\";\n frame.replaceWith(note);\n });\n clone.querySelectorAll(\"script\").forEach((node) => node.remove());\n if (options.keepInteractive !== true) stripInteractiveChrome(clone);\n const htmlClass = document.documentElement.className.replace(/\\bdark\\b/g, \"light\");\n const autoPrint = options.autoPrint ?? true;\n const doc = `<!DOCTYPE html><html lang=\"en\" class=\"${escapeHtml(htmlClass)}\"><head><meta charset=\"UTF-8\"><base href=\"${escapeHtml(\n document.baseURI,\n )}\"><title>${escapeHtml(title)}</title>${head}<style>${ELEMENT_PRINT_CSS}</style></head><body><div class=\"matrx-print-root\">${\n clone.outerHTML\n }</div>${\n autoPrint\n ? '<script>window.addEventListener(\"load\",function(){setTimeout(function(){window.focus();window.print();},250);});</script>'\n : \"\"\n }</body></html>`;\n return openPrintWindow(doc, title);\n}\n","// GENERATED by scripts/gen-katex-css.mjs from katex@0.18.9 — do not edit.\nexport const KATEX_VERSION = \"0.18.9\";\n/** Leads KATEX_CSS; lets openPrintWindow see the sheet is already present. */\nexport const KATEX_CSS_MARKER = \"/* matrx-katex@0.18.9 */\";\nexport const KATEX_CSS = \"/* matrx-katex@0.18.9 */@font-face{font-display:block;font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.ttf) format(\\\"truetype\\\")}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;position:relative;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:\\\"0.18.9\\\"}.katex .katex-mathml{border:0;-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.katex-newline{display:block}.katex .katex-base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .katex-base,.katex .katex-strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .katex-vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .katex-thinbox{display:inline-flex;flex-direction:row;max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline,.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-rule,.katex .katex-underline .underline-line,.katex .mfrac .frac-line{min-height:1px}.katex .mspace{display:inline-block}.katex .katex-smash{display:inline;line-height:0}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.katex-inner,.katex .llap>.katex-inner,.katex .rlap>.katex-inner{position:absolute}.katex .clap>.katex-fix,.katex .llap>.katex-fix,.katex .rlap>.katex-fix{display:inline-block}.katex .llap>.katex-inner{right:0}.katex .clap>.katex-inner,.katex .rlap>.katex-inner{left:0}.katex .clap>.katex-inner>span{margin-left:-50%;margin-right:50%}.katex .katex-rule{border:0 solid;display:inline-block;position:relative}.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.katex-root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .katex-sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .katex-sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .katex-sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .katex-sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .katex-sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .katex-sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .katex-sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .katex-sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .katex-sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .katex-sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .katex-sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .katex-sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .katex-sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .katex-sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .katex-sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .katex-sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .katex-sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .katex-sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .katex-sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .katex-sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .katex-sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .katex-sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .katex-sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .katex-sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .katex-sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .katex-sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .katex-sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .katex-sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .katex-sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .katex-sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .katex-sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .katex-sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .katex-sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .katex-sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .katex-sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .katex-sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .katex-sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .katex-sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .katex-sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .katex-sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .katex-sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .katex-sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .katex-sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .katex-sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .katex-sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .katex-sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .katex-sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .katex-sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .katex-sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .katex-sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .katex-sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .katex-sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .katex-sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .katex-sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .katex-sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .katex-sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .katex-sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .katex-sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .katex-sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .katex-sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .katex-sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .katex-sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .katex-sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .katex-sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .katex-sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .katex-sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .katex-sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .katex-sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .katex-sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .katex-sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .katex-sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .katex-sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .katex-sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .katex-sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .katex-sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .katex-sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .katex-sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .katex-sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .katex-sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .katex-sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .katex-sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .katex-sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .katex-sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .katex-sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .katex-sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .katex-sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .katex-sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .katex-sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .katex-sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .katex-sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .katex-sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .katex-sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .katex-sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .katex-sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .katex-sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .katex-sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .katex-sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .katex-sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .katex-sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .katex-sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .katex-sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .katex-sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .katex-sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .katex-sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .katex-sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .katex-sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .katex-sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .katex-sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .katex-sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .katex-sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .katex-sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .katex-sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .katex-sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .katex-sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .katex-sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .katex-sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .katex-sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .katex-sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .katex-sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .katex-sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .katex-sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .katex-accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .katex-accent .accent-body{position:relative}.katex .katex-accent .accent-body:not(.accent-full){width:0}.katex .katex-overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex svg{fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .katex-stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .katex-stretchy:after,.katex .katex-stretchy:before{content:\\\"\\\"}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .katex-sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .reflectbox{display:inline-block;transform:scaleX(-1)}.katex .eqn-num:before{content:\\\"(\\\" counter(katexEqnNo) \\\")\\\";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:\\\"(\\\" counter(mmlEqnNo) \\\")\\\";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.katex-tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.katex-tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}\\n\";\n","/**\n * Plain text with math → safe HTML: THE one way a printer turns authored text\n * (a flashcard face, an exam prompt, a glossary definition) into markup.\n *\n * Every non-markdown printer used to `escapeHtml()` its text, so `\\(\\text{IO}_3^-\\)`\n * on a flashcard printed as that literal source (Arman, 2026-09-27). What is math\n * is decided by the shared tokenizer (`extractMath` — the same rule the screen,\n * the markdown lane and the /document pipeline use: code and `$5` prices are\n * never math); everything else is escaped exactly as before.\n *\n * Typeset output is KaTeX's laid-out HTML plus MathML, chemistry (mhchem)\n * loaded. KaTeX runs with `trust: false`, which refuses every command that could\n * inject HTML (\\href, \\url, \\htmlClass, \\htmlId, \\htmlStyle, \\htmlData,\n * \\includegraphics). The stylesheet is added by `openPrintWindow` whenever a\n * document contains math, so no printer can forget it.\n */\nimport katex from \"katex\";\n// Registers \\ce{…} and \\pu{…} on the same katex instance.\nimport \"katex/contrib/mhchem\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\nimport { extractMath, MATH_TOKEN_RE } from \"./document/math\";\n\n/** Typeset one TeX span — the ONE KaTeX configuration every print lane uses. */\nexport function typesetTex(tex: string, display: boolean): string {\n return katex.renderToString(tex, {\n displayMode: display,\n output: \"htmlAndMathml\",\n throwOnError: false,\n trust: false,\n strict: \"ignore\",\n });\n}\n\n/**\n * Escape `text` for HTML, with every math span typeset. Newlines are kept as\n * `\\n` in the prose so callers that turn them into `<br>` keep doing so; a\n * newline inside a formula is folded to a space (TeX treats it as one).\n */\nexport function mathTextHtml(text: string | null | undefined): string {\n // The tokenizer's own markers can never come from an author.\n const source = (text ?? \"\").replace(/[\\uE100\\uE101]/g, \"\");\n if (!/[$\\\\]/.test(source)) return escapeHtml(source);\n const { text: prose, spans } = extractMath(source);\n if (spans.length === 0) return escapeHtml(source);\n return escapeHtml(prose).replace(MATH_TOKEN_RE, (_m: string, i: string) => {\n const span = spans[Number(i)];\n return span ? typesetTex(span.tex.replace(/\\s*\\n\\s*/g, \" \"), span.display) : \"\";\n });\n}\n\n/**\n * A field that IS a formula (a cheat-sheet \"formula\" slot): delimited math is\n * typeset as usual; undelimited TeX (it has a command, `^` or `_`) is typeset\n * whole as inline math; anything else is escaped text.\n */\nexport function formulaHtml(formula: string | null | undefined): string {\n const source = (formula ?? \"\").trim();\n const withMath = mathTextHtml(source);\n if (withMath.includes('class=\"katex\"')) return withMath;\n return /\\\\[a-zA-Z]|[\\^_]/.test(source) ? typesetTex(source, false) : withMath;\n}\n","/**\n * Math in documents — ONE math dialect, typeset in every format.\n *\n * What is math is decided ONLY by the shared tokenizer (`@ai-matrx/content-ir/\n * source`): its fence-nesting rule finds code, its pairing finds `$$…$$`,\n * `$…$` (math, not currency), `\\[…\\]` and `\\(…\\)`. Math is lifted out\n * BEFORE markdown parsing, so a TeX `\\,` is never eaten as a markdown escape\n * and `_`/`*` inside a formula never become emphasis.\n *\n * Typesetting: MathJax (TeX → its internal tree) produces\n * - SVG for PDF display math (drawn as vector paths; allow-list sanitized),\n * - MathML for HTML and EPUB (rendered natively by browsers and readers),\n * - MathML that the Word writer turns into native OMML equations and the\n * PDF writer turns into inline text with real super/subscripts.\n */\n\nimport { listIslands, tokenizeSource } from \"@ai-matrx/content-ir/source\";\nimport { sanitizeMathml, sanitizeMathSvg } from \"../safe-html\";\n\nexport const MATH_OPEN = \"\\uE100\";\nexport const MATH_CLOSE = \"\\uE101\";\nexport const MATH_TOKEN_RE = /\\uE100(\\d+)\\uE101/g;\n\nexport interface MathSpan {\n tex: string;\n display: boolean;\n}\n\nconst DELIMITERS: Record<string, { open: number; close: number; display: boolean }> = {\n $$: { open: 2, close: 2, display: true },\n \"\\\\[\": { open: 2, close: 2, display: true },\n $: { open: 1, close: 1, display: false },\n \"\\\\(\": { open: 2, close: 2, display: false },\n};\n\n/**\n * Replace every math span with a private-use token; returns the spans.\n *\n * Print keeps NO fence, code-span or math detection of its own: the spans are\n * the shared tokenizer's complete `math_block` / `math_inline` islands\n * (`tokenizeSource` + `listIslands` from `@ai-matrx/content-ir/source` — the\n * one fence-nesting rule and the one math pairing), so print, the renderer and\n * the editor can never disagree about what is math. The delimiter decides the\n * form: `$$`/`\\[` display, `$`/`\\(` inline.\n */\nexport function extractMath(body: string): { text: string; spans: MathSpan[] } {\n const spans: MathSpan[] = [];\n if (!/[$\\\\]/.test(body)) return { text: body, spans };\n const islands = listIslands(tokenizeSource(body))\n .filter((isl) => isl.complete && (isl.islandType === \"math_block\" || isl.islandType === \"math_inline\"))\n .sort((a, b) => a.start - b.start);\n let out = \"\";\n let at = 0;\n for (const isl of islands) {\n const d = DELIMITERS[String(isl.meta.delimiter ?? \"\")];\n if (!d || isl.start < at) continue;\n out += body.slice(at, isl.start);\n spans.push({ tex: body.slice(isl.start + d.open, isl.end - d.close).trim(), display: d.display });\n out += `${MATH_OPEN}${spans.length - 1}${MATH_CLOSE}`;\n at = isl.end;\n }\n return { text: out + body.slice(at), spans };\n}\n\n// ── typesetting ────────────────────────────────────────────────────────────\n\nexport interface TypesetMath {\n /** MathML (<math …>…</math>) for HTML/EPUB and the Word/PDF-inline writers. */\n mathml: string;\n /** SVG for PDF display math; widths in ex (1ex ≈ 0.442em in MathJax). */\n svg?: string | undefined;\n widthEx?: number | undefined;\n heightEx?: number | undefined;\n /** Error message when the TeX could not be typeset. */\n error?: string | undefined;\n}\n\nexport const mathKey = (tex: string, display: boolean): string => `${display ? \"D\" : \"I\"}:${tex}`;\n\ntype MathJaxKit = import(\"../mathjax\").MathJaxKit;\n\nconst MATHJAX = Symbol.for(\"ai-matrx.print.document.mathjax\");\n\nasync function loadMathJax(): Promise<MathJaxKit> {\n const g = globalThis as unknown as Record<symbol, Promise<MathJaxKit> | undefined>;\n if (g[MATHJAX]) return g[MATHJAX] as Promise<MathJaxKit>;\n // The self-referencing specifier keeps this a real lazy edge in dist (see src/mathjax.ts).\n const pending = import(\"@ai-matrx/print/mathjax\").then((m) => m.createMathJaxKit());\n pending.catch(() => {\n if (g[MATHJAX] === pending) g[MATHJAX] = undefined;\n });\n g[MATHJAX] = pending;\n return pending;\n}\n\n/** Typeset every math span once; keyed by `mathKey`. */\nexport async function typesetMathSpans(spans: MathSpan[]): Promise<Map<string, TypesetMath>> {\n const out = new Map<string, TypesetMath>();\n if (spans.length === 0) return out;\n const mj = await loadMathJax();\n for (const s of spans) {\n const key = mathKey(s.tex, s.display);\n if (out.has(key)) continue;\n try {\n const mathml = mj.mml(s.tex, s.display);\n const err = /<merror[\\s\\S]*?<mtext[^>]*>([\\s\\S]*?)<\\/mtext>/.exec(mathml);\n if (err) {\n out.set(key, { mathml: sanitizeMathml(mathml), error: decodeXml(err[1] as string) });\n continue;\n }\n // TeX is author input: `\\href`, `\\style`, `\\bbox[…]`, `\\mmlToken` reach\n // MathML attributes, so the typeset MathML goes through the allow-list.\n const entry: TypesetMath = { mathml: sanitizeMathml(mathml.replace(/\\n\\s*/g, \"\")) };\n if (s.display) {\n // The same author TeX reaches the SVG as <a href>, style, class and id\n // (LIST T26): only the drawing survives.\n const svg = sanitizeMathSvg(mj.svg(s.tex, true));\n entry.svg = svg;\n entry.widthEx = Number(/width=\"([\\d.]+)ex\"/.exec(svg)?.[1] ?? 0);\n entry.heightEx = Number(/height=\"([\\d.]+)ex\"/.exec(svg)?.[1] ?? 0);\n }\n out.set(key, entry);\n } catch (e) {\n out.set(key, { mathml: \"\", error: (e as Error).message });\n }\n }\n return out;\n}\n\n// ── a small MathML reader (MathJax's serializer output is well-formed XML) ──\n\nexport interface MNode {\n tag: string;\n attrs: Record<string, string>;\n children: MNode[];\n text: string;\n}\n\nexport function decodeXml(s: string): string {\n return s.replace(/&(#x[0-9a-f]+|#\\d+|amp|lt|gt|quot|apos);/gi, (m, e: string) => {\n if (e[0] === \"#\") return String.fromCodePoint(e[1] === \"x\" || e[1] === \"X\" ? parseInt(e.slice(2), 16) : Number(e.slice(1)));\n return ({ amp: \"&\", lt: \"<\", gt: \">\", quot: '\"', apos: \"'\" } as Record<string, string>)[e.toLowerCase()] ?? m;\n });\n}\n\nexport function parseMathML(xml: string): MNode {\n const root: MNode = { tag: \"#root\", attrs: {}, children: [], text: \"\" };\n const stack: MNode[] = [root];\n for (const m of xml.matchAll(/<(\\/?)([\\w:-]+)((?:\\s+[\\w:-]+=\"[^\"]*\")*)\\s*(\\/?)>|([^<]+)/g)) {\n const top = stack[stack.length - 1] as MNode;\n if (m[5] !== undefined) {\n const t = decodeXml(m[5]);\n if (t.trim() || top.tag === \"mtext\" || top.tag === \"mo\") top.text += t;\n continue;\n }\n if (m[1] === \"/\") {\n if (stack.length > 1) stack.pop();\n continue;\n }\n const attrs: Record<string, string> = {};\n for (const a of (m[3] ?? \"\").matchAll(/([\\w:-]+)=\"([^\"]*)\"/g)) attrs[a[1] as string] = decodeXml(a[2] as string);\n const node: MNode = { tag: (m[2] as string).replace(/^m:/, \"\"), attrs, children: [], text: \"\" };\n top.children.push(node);\n if (m[4] !== \"/\") stack.push(node);\n }\n return root.children[0] ?? root;\n}\n\n/** Flatten to plain text (the linear reading of a formula). */\nexport function mathText(n: MNode): string {\n if (n.children.length === 0) return n.text;\n return n.children.map(mathText).join(\"\");\n}\n","/**\n * @ai-matrx/print/exam — the practice-test lane (question booklet, bubble\n * answer form, answer key, explanations).\n *\n * ── THE ANSWER-SEPARATION LAW ───────────────────────────────────────────────\n * The education research (PRINT-EDUCATION-RESEARCH.md, P0 row \"Practice test\n * booklets + bubble answer sheets\", modeled on Barron's / Princeton Review AP\n * and Kaplan) is explicit: the question booklet, the scannable bubble form and\n * the answer key are SEPARATE printable units. That is not a styling\n * preference — it is what makes a practice test re-usable: a student prints\n * ONE booklet and a fresh blank bubble form per attempt, and the key never\n * travels with the questions. So `question-booklet` emits no answers and no\n * explanations at all (there is a test asserting their ABSENCE), and every\n * variant here is an independently printable document.\n *\n * ── WHY THERE IS NO \"SHUFFLE\" SETTING ───────────────────────────────────────\n * Question order and choice order are DATA, not presentation. A printer that\n * shuffled would desynchronize the three units above — a bubble form or key\n * printed in a different session would no longer match the booklet, silently.\n * Shuffling belongs to whatever generates the PracticeTestData (where the\n * shuffled order can be persisted once and reused by all three units).\n *\n * Zero dependencies; `window` is touched only inside `print`, at call time, so\n * the module stays import-inert and RSC-safe.\n */\n\nimport {\n buildPrintDocument,\n openPrintWindow,\n type BlockPrinter,\n type PrintOutcome,\n type PrintSettings,\n} from \"./core\";\nimport { mathTextHtml } from \"./math-text\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface ExamChoice {\n /** Optional explicit label. Omitted = auto-lettered A, B, C, … by position. */\n label?: string;\n text: string;\n}\n\nexport interface ExamQuestion {\n prompt: string;\n /** Omitted or empty = free-response (ruled work space instead of bubbles). */\n choices?: ExamChoice[];\n /** The correct choice label (or, for free response, the model answer text). */\n answer?: string;\n explanation?: string;\n points?: number;\n /** Ruled lines to print under a free-response question in the booklet. */\n workSpaceLines?: number;\n}\n\nexport interface PracticeTestData {\n title: string;\n instructions?: string;\n questions: ExamQuestion[];\n}\n\nexport type PracticeTestVariant =\n | \"question-booklet\"\n | \"bubble-sheet\"\n | \"answer-key\"\n | \"explanations\";\n\n// ─── Geometry constants (bubble sheet) ───────────────────────────────────────\n//\n// Letter portrait, 0.5in page margin. 25 rows per column is the scanner-form\n// convention (Scantron 882-E and the AP answer sheets both group in 25s) and\n// it also keeps a 5-choice row inside a 1.75in column at a 0.28in pitch.\n// Everything below is INCH-exact so a physical ruler laid on the print\n// matches: circles are 0.16in across, drawn as CSS borders (never images, and\n// never a background fill that a \"save ink\" print mode could drop).\n\n/** Rows per bubble column — the scanner-form convention. */\nconst BUBBLE_ROWS_PER_COLUMN = 25;\n/** Columns printed side by side before a page break. */\nconst BUBBLE_COLUMNS_PER_PAGE = 4;\n/** Max bubbles a row can carry (A–J) before we stop lettering. */\nconst CHOICE_LETTERS = \"ABCDEFGHIJ\";\n\n/** Auto-letter for choice index i; falls back to the number past the alphabet. */\nfunction letterFor(index: number): string {\n return CHOICE_LETTERS[index] ?? String(index + 1);\n}\n\n/** The label a choice prints under (explicit label wins over the auto letter). */\nfunction choiceLabel(choice: ExamChoice, index: number): string {\n const explicit = choice.label?.trim();\n return explicit && explicit.length > 0 ? explicit : letterFor(index);\n}\n\nfunction isFreeResponse(question: ExamQuestion): boolean {\n return !question.choices || question.choices.length === 0;\n}\n\n// ─── Data extraction ─────────────────────────────────────────────────────────\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0\n ? value.trim()\n : undefined;\n}\n\nfunction asNumber(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\nfunction parseChoice(raw: unknown): ExamChoice | null {\n if (typeof raw === \"string\") {\n return raw.trim().length > 0 ? { text: raw } : null;\n }\n if (!raw || typeof raw !== \"object\") return null;\n const obj = raw as Record<string, unknown>;\n const text = asString(obj[\"text\"]);\n if (text === undefined) return null;\n const label = asString(obj[\"label\"]);\n // exactOptionalPropertyTypes: only spread the key when it actually has a value.\n return label === undefined ? { text } : { label, text };\n}\n\nfunction parseQuestion(raw: unknown): ExamQuestion | null {\n if (!raw || typeof raw !== \"object\") return null;\n const obj = raw as Record<string, unknown>;\n const prompt = asString(obj[\"prompt\"]) ?? asString(obj[\"question\"]);\n if (prompt === undefined) return null;\n\n const question: ExamQuestion = { prompt };\n\n const rawChoices = obj[\"choices\"] ?? obj[\"options\"];\n if (Array.isArray(rawChoices)) {\n const choices = rawChoices\n .map((c) => parseChoice(c))\n .filter((c): c is ExamChoice => c !== null);\n if (choices.length > 0) question.choices = choices;\n }\n const answer = asString(obj[\"answer\"]);\n if (answer !== undefined) question.answer = answer;\n const explanation = asString(obj[\"explanation\"]);\n if (explanation !== undefined) question.explanation = explanation;\n const points = asNumber(obj[\"points\"]);\n if (points !== undefined) question.points = points;\n const workSpaceLines = asNumber(obj[\"workSpaceLines\"]);\n if (workSpaceLines !== undefined) {\n question.workSpaceLines = Math.max(0, Math.floor(workSpaceLines));\n }\n return question;\n}\n\n/**\n * Accept the canonical shape, a bare questions array, or a wrapper object that\n * carries one — the same tolerance the flashcards lane offers its hosts.\n */\nexport function extractPracticeTest(data: unknown): PracticeTestData {\n const empty: PracticeTestData = { title: \"Practice Test\", questions: [] };\n if (!data) return empty;\n\n if (Array.isArray(data)) {\n return {\n title: \"Practice Test\",\n questions: data\n .map(parseQuestion)\n .filter((q): q is ExamQuestion => q !== null),\n };\n }\n if (typeof data !== \"object\") return empty;\n\n const obj = data as Record<string, unknown>;\n const rawQuestions = obj[\"questions\"] ?? obj[\"items\"];\n const questions = Array.isArray(rawQuestions)\n ? rawQuestions.map(parseQuestion).filter((q): q is ExamQuestion => q !== null)\n : [];\n\n const test: PracticeTestData = {\n title: asString(obj[\"title\"]) ?? \"Practice Test\",\n questions,\n };\n const instructions = asString(obj[\"instructions\"]);\n if (instructions !== undefined) test.instructions = instructions;\n return test;\n}\n\n// ─── Settings ────────────────────────────────────────────────────────────────\n\ninterface ResolvedExamSettings {\n /** 1-based inclusive question range; both 0 = whole test. */\n from: number;\n to: number;\n /** 0 = derive the bubble count from each question's own choices. */\n bubbleLetters: number;\n showPointsColumn: boolean;\n}\n\nfunction resolveSettings(raw?: PrintSettings): ResolvedExamSettings {\n const num = (key: string): number => {\n const v = raw?.[key];\n return typeof v === \"number\" && Number.isFinite(v) && v > 0\n ? Math.floor(v)\n : 0;\n };\n return {\n from: num(\"fromQuestion\"),\n to: num(\"toQuestion\"),\n bubbleLetters: Math.min(num(\"bubbleLetters\"), CHOICE_LETTERS.length),\n showPointsColumn: raw?.[\"showPointsColumn\"] === true,\n };\n}\n\n/**\n * Slice by the range setting. Returns the questions AND the 0-based offset of\n * the first one, so every variant keeps the ORIGINAL question numbers (a key\n * for questions 26–50 must say \"26.\", never \"1.\").\n */\nfunction applyRange(\n questions: readonly ExamQuestion[],\n s: ResolvedExamSettings,\n): { questions: ExamQuestion[]; startIndex: number } {\n if (questions.length === 0) return { questions: [], startIndex: 0 };\n const start = s.from > 0 ? Math.min(s.from, questions.length) : 1;\n const end = s.to > 0 ? Math.min(s.to, questions.length) : questions.length;\n const [lo, hi] = start <= end ? [start, end] : [end, start];\n return { questions: questions.slice(lo - 1, hi), startIndex: lo - 1 };\n}\n\n// ─── Shared styles ───────────────────────────────────────────────────────────\n\nconst EXAM_BASE_STYLES = `\n .ex-title { font-size: 18pt; font-weight: 700; margin-bottom: 2px; }\n .ex-subtitle { font-size: 10pt; color: #475569; margin-bottom: 14px; }\n .ex-instructions {\n border: 2px solid #1e293b;\n border-radius: 6px;\n padding: 10px 14px;\n margin: 0 0 18px;\n font-size: 10.5pt;\n line-height: 1.5;\n background: #f8fafc;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n .ex-instructions-heading {\n font-size: 9pt;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: #1e293b;\n margin-bottom: 4px;\n }\n`;\n\n// ─── Variant: question booklet ───────────────────────────────────────────────\n\nconst BOOKLET_VARIANT_STYLES = `\n ${EXAM_BASE_STYLES}\n .ex-q {\n /* A question never splits across a page mid-choice-list — a student\n reading choice (C) at the top of the next page misreads the item. */\n break-inside: avoid;\n page-break-inside: avoid;\n margin: 0 0 16px;\n }\n .ex-q-head { display: flex; gap: 8px; align-items: baseline; }\n .ex-q-num { font-weight: 700; min-width: 1.9em; }\n .ex-q-prompt { flex: 1; font-size: 11pt; line-height: 1.5; }\n .ex-q-points {\n font-size: 8.5pt;\n color: #475569;\n white-space: nowrap;\n font-weight: 600;\n }\n .ex-choices { list-style: none; margin: 6px 0 0 1.9em; padding: 0; }\n .ex-choice { display: flex; gap: 7px; font-size: 10.5pt; line-height: 1.45; margin-bottom: 3px; }\n .ex-choice-label { font-weight: 600; min-width: 1.3em; }\n .ex-work { margin: 8px 0 0 1.9em; }\n .ex-work-line {\n border-bottom: 1px solid #94a3b8;\n height: 0.32in;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n`;\n\nfunction renderPoints(question: ExamQuestion): string {\n if (question.points === undefined) return \"\";\n const unit = question.points === 1 ? \"pt\" : \"pts\";\n return `<span class=\"ex-q-points\">(${escapeHtml(String(question.points))} ${unit})</span>`;\n}\n\nfunction renderQuestionBooklet(\n test: PracticeTestData,\n questions: readonly ExamQuestion[],\n startIndex: number,\n): string {\n const instructions = test.instructions\n ? `<div class=\"ex-instructions\">\n <div class=\"ex-instructions-heading\">Instructions</div>\n ${mathTextHtml(test.instructions)}\n </div>`\n : \"\";\n\n const items = questions\n .map((q, i) => {\n const number = startIndex + i + 1;\n const choices = (q.choices ?? [])\n .map(\n (c, ci) =>\n `<li class=\"ex-choice\"><span class=\"ex-choice-label\">${escapeHtml(\n choiceLabel(c, ci),\n )}.</span><span>${mathTextHtml(c.text)}</span></li>`,\n )\n .join(\"\");\n const choiceList =\n choices.length > 0 ? `<ul class=\"ex-choices\">${choices}</ul>` : \"\";\n const lineCount = isFreeResponse(q) ? (q.workSpaceLines ?? 0) : 0;\n const work =\n lineCount > 0\n ? `<div class=\"ex-work\">${'<div class=\"ex-work-line\"></div>'.repeat(lineCount)}</div>`\n : \"\";\n return `<div class=\"ex-q\" data-question=\"${number}\">\n <div class=\"ex-q-head\">\n <span class=\"ex-q-num\">${number}.</span>\n <span class=\"ex-q-prompt\">${mathTextHtml(q.prompt)}</span>\n ${renderPoints(q)}\n </div>\n ${choiceList}\n ${work}\n</div>`;\n })\n .join(\"\\n\");\n\n // NO answers, NO explanations here — the answer-separation law.\n return `<div class=\"ex-title\">${mathTextHtml(test.title)}</div>\n<div class=\"ex-subtitle\">Question booklet — mark your answers on the separate answer sheet</div>\n${instructions}\n${items}`;\n}\n\n// ─── Variant: bubble sheet ───────────────────────────────────────────────────\n\nconst BUBBLE_STYLES = `\n @page { size: 8.5in 11in; margin: 0.5in; }\n\n .bs-header {\n border: 2px solid #111;\n padding: 0.12in 0.16in;\n margin-bottom: 0.22in;\n break-inside: avoid;\n page-break-inside: avoid;\n }\n .bs-header-title { font-size: 13pt; font-weight: 700; margin-bottom: 0.06in; }\n .bs-header-note { font-size: 8pt; color: #334155; margin-bottom: 0.1in; }\n .bs-fields { display: flex; gap: 0.2in; }\n .bs-field { flex: 1; font-size: 8.5pt; }\n .bs-field-score { flex: 0 0 1.2in; }\n .bs-field-rule {\n border-bottom: 1px solid #111;\n height: 0.26in;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n .bs-field-label {\n font-size: 7.5pt;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: #334155;\n }\n\n .bs-columns {\n display: flex;\n gap: 0.3in;\n break-after: page;\n page-break-after: always;\n }\n .bs-columns:last-of-type { break-after: auto; page-break-after: auto; }\n\n .bs-column { flex: 1; }\n\n .bs-row {\n display: flex;\n align-items: center;\n height: 0.28in; /* the pitch — every row identical */\n break-inside: avoid;\n page-break-inside: avoid;\n }\n .bs-row-num {\n width: 0.3in;\n font-size: 8.5pt;\n text-align: right;\n padding-right: 0.06in;\n font-variant-numeric: tabular-nums;\n }\n .bs-bubble {\n width: 0.16in;\n height: 0.16in;\n border: 1.25px solid #111;\n border-radius: 50%;\n margin-right: 0.055in;\n font-size: 5.5pt;\n line-height: 0.16in;\n text-align: center;\n color: #111;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n .bs-blank-line {\n flex: 1;\n border-bottom: 1px solid #111;\n height: 0.16in;\n margin-right: 0.06in;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n .bs-points {\n width: 0.34in;\n font-size: 7.5pt;\n color: #334155;\n text-align: right;\n }\n`;\n\n/** How many bubbles row `q` prints: the override, else the question's choices. */\nfunction bubbleCountFor(\n question: ExamQuestion,\n s: ResolvedExamSettings,\n): number {\n if (isFreeResponse(question)) return s.bubbleLetters > 0 ? s.bubbleLetters : 0;\n const own = question.choices?.length ?? 0;\n const n = s.bubbleLetters > 0 ? s.bubbleLetters : own;\n return Math.min(n, CHOICE_LETTERS.length);\n}\n\nfunction renderBubbleRow(\n question: ExamQuestion,\n number: number,\n s: ResolvedExamSettings,\n): string {\n const count = bubbleCountFor(question, s);\n const cells =\n count > 0\n ? Array.from(\n { length: count },\n (_, i) =>\n `<span class=\"bs-bubble\" data-letter=\"${escapeHtml(letterFor(i))}\">${escapeHtml(\n letterFor(i),\n )}</span>`,\n ).join(\"\")\n : // Free response: a short rule to write the answer on, never bubbles.\n `<span class=\"bs-blank-line\"></span>`;\n const points =\n s.showPointsColumn && question.points !== undefined\n ? `<span class=\"bs-points\">${escapeHtml(String(question.points))}</span>`\n : \"\";\n return `<div class=\"bs-row\" data-question=\"${number}\" data-bubbles=\"${count}\">\n <span class=\"bs-row-num\">${number}.</span>${cells}${points}\n </div>`;\n}\n\nfunction renderBubbleSheet(\n test: PracticeTestData,\n questions: readonly ExamQuestion[],\n startIndex: number,\n s: ResolvedExamSettings,\n): string {\n const rows = questions.map((q, i) =>\n renderBubbleRow(q, startIndex + i + 1, s),\n );\n\n // Chunk into fixed 25-row columns, then group columns into page-sized sets.\n const columns: string[] = [];\n for (let i = 0; i < rows.length; i += BUBBLE_ROWS_PER_COLUMN) {\n columns.push(\n `<div class=\"bs-column\">${rows\n .slice(i, i + BUBBLE_ROWS_PER_COLUMN)\n .join(\"\")}</div>`,\n );\n }\n const sets: string[] = [];\n for (let i = 0; i < columns.length; i += BUBBLE_COLUMNS_PER_PAGE) {\n // Pad the last set with empty columns so the surviving columns keep their\n // width — a 1-column final page must not stretch to the full 7.5in.\n const group = columns.slice(i, i + BUBBLE_COLUMNS_PER_PAGE);\n while (group.length < BUBBLE_COLUMNS_PER_PAGE) {\n group.push(`<div class=\"bs-column\"></div>`);\n }\n sets.push(`<div class=\"bs-columns\">${group.join(\"\")}</div>`);\n }\n\n const header = `<div class=\"bs-header\">\n <div class=\"bs-header-title\">${mathTextHtml(test.title)} — Answer Sheet</div>\n <div class=\"bs-header-note\">Fill the bubble completely with a No. 2 pencil. Erase changes fully. Do not write outside the boxes.</div>\n <div class=\"bs-fields\">\n <div class=\"bs-field\"><div class=\"bs-field-rule\"></div><div class=\"bs-field-label\">Name</div></div>\n <div class=\"bs-field\"><div class=\"bs-field-rule\"></div><div class=\"bs-field-label\">Date</div></div>\n <div class=\"bs-field bs-field-score\"><div class=\"bs-field-rule\"></div><div class=\"bs-field-label\">Score</div></div>\n </div>\n</div>`;\n\n return `${header}\n${sets.join(\"\\n\")}`;\n}\n\n// ─── Variant: answer key ─────────────────────────────────────────────────────\n\nconst KEY_STYLES = `\n ${EXAM_BASE_STYLES}\n .ak-grid { column-count: 4; column-gap: 0.3in; }\n .ak-row {\n break-inside: avoid;\n page-break-inside: avoid;\n font-size: 10pt;\n line-height: 1.5;\n display: flex;\n gap: 6px;\n }\n .ak-num { font-weight: 600; min-width: 2em; text-align: right; }\n .ak-answer { font-weight: 700; }\n .ak-points { color: #475569; font-size: 8.5pt; margin-left: auto; }\n .ak-free { column-count: 1; }\n .ak-free .ak-answer { font-weight: 500; }\n`;\n\n/** The printed answer for a question — \"—\" when the data carries none. */\nfunction answerText(question: ExamQuestion): string {\n return question.answer ?? \"—\";\n}\n\nfunction renderAnswerKey(\n test: PracticeTestData,\n questions: readonly ExamQuestion[],\n startIndex: number,\n): string {\n const anyPoints = questions.some((q) => q.points !== undefined);\n // Free-response answers are model-answer PROSE — they can't ride a 4-column\n // grid without becoming unreadable, so they get their own single-column list.\n const compact: string[] = [];\n const prose: string[] = [];\n questions.forEach((q, i) => {\n const number = startIndex + i + 1;\n const points =\n anyPoints && q.points !== undefined\n ? `<span class=\"ak-points\">${escapeHtml(String(q.points))}</span>`\n : \"\";\n const row = `<div class=\"ak-row\" data-question=\"${number}\"><span class=\"ak-num\">${number}.</span><span class=\"ak-answer\">${mathTextHtml(\n answerText(q),\n )}</span>${points}</div>`;\n (isFreeResponse(q) ? prose : compact).push(row);\n });\n\n const compactHtml =\n compact.length > 0 ? `<div class=\"ak-grid\">${compact.join(\"\")}</div>` : \"\";\n const proseHtml =\n prose.length > 0\n ? `<h3>Free-response model answers</h3><div class=\"ak-grid ak-free\">${prose.join(\n \"\",\n )}</div>`\n : \"\";\n\n return `<div class=\"ex-title\">${mathTextHtml(test.title)}</div>\n<div class=\"ex-subtitle\">Answer key — keep separate from the question booklet</div>\n${compactHtml}\n${proseHtml}`;\n}\n\n// ─── Variant: explanations ───────────────────────────────────────────────────\n\nconst EXPLANATIONS_STYLES = `\n ${EXAM_BASE_STYLES}\n .exp-row {\n break-inside: avoid;\n page-break-inside: avoid;\n border-bottom: 1px solid #e2e8f0;\n padding: 6px 0;\n }\n .exp-head { display: flex; gap: 6px; font-size: 10pt; }\n .exp-num { font-weight: 700; min-width: 2em; }\n .exp-answer { font-weight: 700; }\n .exp-prompt { color: #334155; }\n .exp-body { font-size: 9.5pt; line-height: 1.45; margin: 3px 0 0 2em; color: #1f2937; }\n`;\n\nfunction renderExplanations(\n test: PracticeTestData,\n questions: readonly ExamQuestion[],\n startIndex: number,\n): string {\n const rows = questions\n .map((q, i) => {\n const number = startIndex + i + 1;\n const explanation = q.explanation\n ? `<div class=\"exp-body\">${mathTextHtml(q.explanation)}</div>`\n : \"\";\n return `<div class=\"exp-row\" data-question=\"${number}\">\n <div class=\"exp-head\">\n <span class=\"exp-num\">${number}.</span>\n <span class=\"exp-prompt\">${mathTextHtml(q.prompt)}</span>\n </div>\n <div class=\"exp-body\"><span class=\"exp-answer\">Answer: ${mathTextHtml(\n answerText(q),\n )}</span></div>\n ${explanation}\n</div>`;\n })\n .join(\"\\n\");\n\n return `<div class=\"ex-title\">${mathTextHtml(test.title)}</div>\n<div class=\"ex-subtitle\">Answers &amp; explanations — study sheet</div>\n${rows}`;\n}\n\n// ─── The printer ─────────────────────────────────────────────────────────────\n\nconst EMPTY_MESSAGE = \"No practice-test data available to print.\";\n\n/**\n * The practice-test printer. Four variants, each an INDEPENDENT printable unit\n * (see the answer-separation law at the top of this file).\n */\nexport const practiceTestPrinter: BlockPrinter = {\n label: \"Print practice test\",\n variants: [\n {\n id: \"question-booklet\",\n label: \"Question booklet (recommended)\",\n description:\n \"Numbered questions with lettered choices and ruled work space. Contains no answers — print it once and re-use it with a fresh answer sheet each attempt\",\n },\n {\n id: \"bubble-sheet\",\n label: \"Bubble answer sheet\",\n description:\n \"The scannable answer form: name/date/score header and a fixed grid of bubble rows, 25 per column. Free-response items get a write-in line\",\n },\n {\n id: \"answer-key\",\n label: \"Answer key\",\n description:\n \"Compact multi-column key for grading. Keep it away from the booklet\",\n },\n {\n id: \"explanations\",\n label: \"Answers & explanations\",\n description:\n \"Every question with its answer and explanation, at study-sheet density\",\n },\n ],\n settings: [\n {\n type: \"range\" as const,\n id: \"questionRange\",\n label: \"Questions to print\",\n description:\n \"Leave blank to print the whole test. Question numbers stay the original ones\",\n fromId: \"fromQuestion\",\n toId: \"toQuestion\",\n defaultFrom: 0,\n defaultTo: 0,\n min: 1,\n fromPlaceholder: \"1\",\n toPlaceholder: \"end\",\n },\n {\n type: \"number\" as const,\n id: \"bubbleLetters\",\n label: \"Bubbles per row (override)\",\n description:\n \"0 = derive from each question's own choices (recommended). Set 4 or 5 to force a uniform grid\",\n defaultValue: 0,\n min: 0,\n max: CHOICE_LETTERS.length,\n appliesTo: [\"bubble-sheet\"],\n },\n {\n type: \"boolean\" as const,\n id: \"showPointsColumn\",\n label: \"Show points column\",\n description: \"Prints each question's point value beside its bubble row\",\n defaultValue: false,\n appliesTo: [\"bubble-sheet\"],\n },\n ],\n\n print(\n data: unknown,\n variantId: string = \"question-booklet\",\n rawSettings?: PrintSettings,\n ): PrintOutcome {\n const test = extractPracticeTest(data);\n const s = resolveSettings(rawSettings);\n\n if (test.questions.length === 0) {\n return openPrintWindow(\n buildPrintDocument(`<p>${EMPTY_MESSAGE}</p>`, test.title, EXAM_BASE_STYLES),\n \"practice-test\",\n );\n }\n\n const { questions, startIndex } = applyRange(test.questions, s);\n\n switch (variantId as PracticeTestVariant) {\n case \"bubble-sheet\":\n return openPrintWindow(\n buildPrintDocument(\n renderBubbleSheet(test, questions, startIndex, s),\n `${test.title} — Answer Sheet`,\n BUBBLE_STYLES,\n ),\n `${test.title}-answer-sheet`,\n );\n case \"answer-key\":\n return openPrintWindow(\n buildPrintDocument(\n renderAnswerKey(test, questions, startIndex),\n `${test.title} — Answer Key`,\n KEY_STYLES,\n ),\n `${test.title}-answer-key`,\n );\n case \"explanations\":\n return openPrintWindow(\n buildPrintDocument(\n renderExplanations(test, questions, startIndex),\n `${test.title} — Explanations`,\n EXPLANATIONS_STYLES,\n ),\n `${test.title}-explanations`,\n );\n case \"question-booklet\":\n default:\n return openPrintWindow(\n buildPrintDocument(\n renderQuestionBooklet(test, questions, startIndex),\n test.title,\n BOOKLET_VARIANT_STYLES,\n ),\n `${test.title}-questions`,\n );\n }\n },\n};\n\n/** Geometry the bubble form is built on — exported so hosts/tests can assert it. */\nexport const BUBBLE_SHEET_GEOMETRY = {\n rowsPerColumn: BUBBLE_ROWS_PER_COLUMN,\n columnsPerPage: BUBBLE_COLUMNS_PER_PAGE,\n bubbleDiameterInches: 0.16,\n rowPitchInches: 0.28,\n} as const;\n"],"mappings":"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","names":["escapeHtml","escapeHtml","escapeHtml"]}
1
+ {"version":3,"sources":["../src/core.ts","../src/katex-css.generated.ts","../src/math-text.ts","../src/document/math.ts","../src/exam.ts"],"sourcesContent":["/**\n * @ai-matrx/print/core — the Block Print contract & print-window composition.\n *\n * Ported VERBATIM from matrx-frontend `lib/block-print/block-print-utils.ts`\n * (2026-08-29 state, including the `number` and `select` PrintSetting types the\n * commerce label lane added). Zero dependencies; touches `window` only at call\n * time, so the module stays import-inert and RSC-safe.\n *\n * BlockPrinter is the only interface a host system needs to know about.\n * Each printable unit exports its own BlockPrinter alongside its render\n * component; host code only ever calls print(data, variantId, settings).\n *\n * `buildFitTextScript` is the ONE auto-shrink script (the flashcards\n * FIT_TEXT pattern, parameterized by selector so the label lane shares it).\n */\n\n// THE ONE HTML escaper (duplication census row 14). Nine copies of\n// `escapeHtml` were live across the fleet and THEY DID NOT ESCAPE THE SAME\n// CHARACTERS — a string safe through one was an attribute break-out through\n// another. It moved to `@ai-matrx/kit/html-escape` with a decided character\n// set: an HTML escaper must not require this package's jspdf + html2canvas\n// graph. Not re-exported from here; import `@ai-matrx/kit/html-escape`.\nimport { downloadFile } from \"@ai-matrx/kit/download\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\nimport { KATEX_CSS, KATEX_CSS_MARKER } from \"./katex-css.generated\";\nimport { openOutbound } from \"@ai-matrx/kit/outbound\";\n\n// ─── Contract ────────────────────────────────────────────────────────────────\n\nexport interface PrintVariant {\n id: string;\n label: string;\n description?: string;\n}\n\n/**\n * A single user-configurable setting shown in the print dialog.\n * Extend the discriminated union for new control types\n * without breaking existing printers.\n */\nexport type PrintSetting =\n | {\n type: \"boolean\";\n id: string;\n label: string;\n description?: string;\n /** Default value shown when the dialog first opens */\n defaultValue: boolean;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /**\n * A numeric \"from → to\" range control, rendered as two small number inputs.\n * Writes two entries into the resolved PrintSettings map — one under fromId,\n * one under toId. Printers interpret 0 (or missing) as \"no bound\" by convention.\n */\n type: \"range\";\n /** Stable id used as React key — not written to the settings map */\n id: string;\n label: string;\n description?: string;\n /** Settings map key for the lower bound value */\n fromId: string;\n /** Settings map key for the upper bound value */\n toId: string;\n defaultFrom: number;\n defaultTo: number;\n /** Minimum accepted value for each input (inclusive). Default 0. */\n min?: number;\n /** Placeholder shown when the input is empty (0). Default \"start\" / \"end\". */\n fromPlaceholder?: string;\n toPlaceholder?: string;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /** A single numeric input (e.g. \"start at label #\", \"copies\"). */\n type: \"number\";\n id: string;\n label: string;\n description?: string;\n defaultValue: number;\n /** Minimum accepted value (inclusive). Default 0. */\n min?: number;\n /** Maximum accepted value (inclusive). Omit = unbounded. */\n max?: number;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /** A one-of-N choice rendered as a compact segmented row. */\n type: \"select\";\n id: string;\n label: string;\n description?: string;\n options: { value: string; label: string }[];\n defaultValue: string;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n };\n\n/** A resolved map of setting id → current value, passed to print(). */\nexport type PrintSettings = Record<string, boolean | string | number>;\n\n/**\n * The self-contained print capability that each block can export.\n * Host code only ever calls print(data, variantId, settings) — it never inspects internals.\n */\nexport interface BlockPrinter {\n /** Tooltip / aria-label for the print button in the block header */\n label: string;\n\n /**\n * Variants offered to the user before printing.\n * Empty array = no options dialog shown; prints immediately with defaults.\n */\n variants: PrintVariant[];\n\n /**\n * Optional user-configurable settings shown below the variant picker.\n * Omit entirely if the block has no configurable settings.\n */\n settings?: PrintSetting[];\n\n /**\n * Execute the print.\n * data — the block's structured data (serverData or parsed content)\n * variantId — which variant was chosen (undefined = default / no variants)\n * settings — resolved setting values chosen by the user\n *\n * MAY resolve to a PrintOutcome (\"opened\" | \"downloaded\") so the host can\n * tell the user when the popup-blocked download fallback fired; `void`\n * stays legal for printers that don't go through a print window.\n */\n print: (\n data: unknown,\n variantId?: string,\n settings?: PrintSettings,\n ) => void | PrintOutcome | Promise<void | PrintOutcome>;\n\n /**\n * The block as it appears INSIDE a composed print (a message's Print / Full\n * Print, a document export): the same adapter the block's own print uses,\n * returning body HTML or a picture. A frame block (an HTML page) answers\n * with an image — a composed print window runs no author scripts — or an\n * honest `{ notice }` when no picture can be made. Absent ⇒ the composer\n * prints the block's source.\n */\n toPrintHtml?: (data: unknown, context: PrintBlockContext) => PrintBlockOutput | null | Promise<PrintBlockOutput | null>;\n}\n\n/** Where a block sits when a composer asks for its print form. */\nexport interface PrintBlockContext {\n /** The registry key the block was found under (artifact type or `__kind`). */\n readonly type: string;\n /** The block's raw source text, exactly as it sits in the message. */\n readonly raw: string;\n /** The block's title, when the wire form carries one. */\n readonly title?: string | undefined;\n /** The wire form's attributes (an `<artifact>`'s `id`, `version`) — how a printer finds the block's record. */\n readonly attributes?: Readonly<Record<string, string>> | undefined;\n}\n\n/**\n * A block's print form inside a composed print. `html` is trusted body HTML\n * from a printer (escape what you interpolate); `image` is a picture (a\n * captured page, a drawn diagram); `notice` is an honest line printed in the\n * block's place when neither exists.\n */\nexport type PrintBlockOutput =\n | {\n readonly html: string;\n /**\n * The block's stylesheet. A composed print sanitizes block HTML (no\n * `<style>`, no inline styles, only `matrx-*` / `print-*` classes), so a\n * printer that styles its layout passes its CSS here: the composer puts\n * each distinct sheet once in the document head. Scope it under your own\n * `matrx-…` class.\n */\n readonly css?: string | undefined;\n }\n | { readonly image: { readonly src: string; readonly alt?: string | undefined; readonly caption?: string | undefined } }\n | { readonly notice: string };\n\n// ─── The ONE block-printer registry ───────────────────────────────────────────\n//\n// Keyed by artifact type / `__kind`. A block's own print button, the canvas\n// tab's Print and the message composer all read the same entry, so a block\n// has one print adapter with every caller. Lives on globalThis under a\n// Symbol.for key so duplicated bundles (ESM + CJS) share one registry.\n\nconst PRINTER_REGISTRY = Symbol.for(\"ai-matrx.print.block-printers\");\n\nfunction printerRegistry(): Map<string, BlockPrinter> {\n const g = globalThis as unknown as { [PRINTER_REGISTRY]?: Map<string, BlockPrinter> };\n g[PRINTER_REGISTRY] ??= new Map();\n return g[PRINTER_REGISTRY];\n}\n\n/** Registers `printer` under each key (artifact type and/or `__kind`). Returns the unregister. */\nexport function registerBlockPrinter(keys: string | readonly string[], printer: BlockPrinter): () => void {\n const map = printerRegistry();\n const list = (typeof keys === \"string\" ? [keys] : keys).map((key) => key.toLowerCase());\n for (const key of list) map.set(key, printer);\n return () => {\n for (const key of list) if (map.get(key) === printer) map.delete(key);\n };\n}\n\n/** The printer registered for an artifact type or `__kind` (case-insensitive). */\nexport function getBlockPrinter(key: string | null | undefined): BlockPrinter | undefined {\n return key ? printerRegistry().get(key.toLowerCase()) : undefined;\n}\n\n// ─── Lazy printer loaders ─────────────────────────────────────────────────────\n//\n// A host may load its printers lazily (they carry KaTeX and every structured\n// layout, so they stay out of a page's first load). A lookup that is allowed to\n// wait — a message print, which already awaits its own imports — calls\n// `whenBlockPrintersReady()` first, so a print clicked before the host's\n// background load finishes still finds every printer instead of silently\n// printing a block as its source. Shared on globalThis like the registry.\n\nconst PRINTER_LOADERS = Symbol.for(\"ai-matrx.print.block-printer-loaders\");\n\nfunction printerLoaders(): Set<() => Promise<unknown>> {\n const g = globalThis as unknown as { [PRINTER_LOADERS]?: Set<() => Promise<unknown>> };\n g[PRINTER_LOADERS] ??= new Set();\n return g[PRINTER_LOADERS];\n}\n\n/** Registers a host's lazy printer loader (it must be idempotent). Returns the unregister. */\nexport function registerBlockPrinterLoader(load: () => Promise<unknown>): () => void {\n const loaders = printerLoaders();\n loaders.add(load);\n return () => {\n loaders.delete(load);\n };\n}\n\n/**\n * Resolves once every registered printer loader has finished. A loader that\n * fails is reported, never thrown: the print still goes out, with that block\n * printed as its source.\n */\nexport async function whenBlockPrintersReady(): Promise<void> {\n await Promise.all(\n [...printerLoaders()].map((load) =>\n load().then(\n () => undefined,\n (error: unknown) => {\n console.error(\"[print] a block-printer loader failed; its blocks print as source\", error);\n },\n ),\n ),\n );\n}\n\n/** Every registered key — for guards and diagnostics. */\nexport function listBlockPrinterKeys(): string[] {\n return [...printerRegistry().keys()];\n}\n\n/** The HTML a composer puts in a block's place for a `PrintBlockOutput`. */\nexport function printBlockOutputHtml(output: PrintBlockOutput): string {\n if (\"html\" in output) return `<div class=\"matrx-print-block\">${output.html}</div>`;\n if (\"image\" in output) {\n const { src, alt, caption } = output.image;\n return `<figure class=\"matrx-print-block matrx-print-image\"><img src=\"${escapeHtml(src)}\" alt=\"${escapeHtml(\n alt ?? \"Picture\",\n )}\" style=\"max-width:100%;height:auto\">${caption ? `<figcaption>${escapeHtml(caption)}</figcaption>` : \"\"}</figure>`;\n }\n return `<p class=\"matrx-print-block matrx-print-notice\" style=\"border:1px dashed #94a3b8;border-radius:6px;padding:10px 12px;color:#475569;font-style:italic\">${escapeHtml(\n output.notice,\n )}</p>`;\n}\n\n// ─── Shared Utilities ─────────────────────────────────────────────────────────\n\nconst PRINT_STYLES = `\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;\n font-size: 11pt;\n line-height: 1.6;\n color: #1a1a1a;\n background: #fff;\n max-width: 860px;\n margin: 0 auto;\n padding: 28px 36px;\n }\n\n .print-actions {\n display: flex;\n gap: 10px;\n margin-bottom: 24px;\n padding-bottom: 18px;\n border-bottom: 2px solid #e5e7eb;\n }\n\n .print-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 20px;\n border-radius: 8px;\n font-size: 13px;\n font-family: inherit;\n cursor: pointer;\n border: none;\n font-weight: 600;\n }\n\n .print-btn-primary { background: #2563eb; color: #fff; }\n .print-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }\n\n h1 { font-size: 22pt; font-weight: 700; margin-bottom: 0.5em; }\n h2 { font-size: 16pt; font-weight: 700; margin: 1.2em 0 0.4em; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.2em; }\n h3 { font-size: 13pt; font-weight: 600; margin: 1em 0 0.3em; }\n p { margin-bottom: 0.7em; }\n\n table {\n width: 100%;\n border-collapse: collapse;\n margin: 1em 0;\n font-size: 10pt;\n }\n th {\n background: #1e293b;\n color: #f8fafc;\n font-weight: 600;\n text-align: left;\n padding: 7px 12px;\n border: 1px solid #334155;\n }\n td {\n padding: 6px 12px;\n border: 1px solid #e2e8f0;\n vertical-align: top;\n }\n tr:nth-child(even) td { background: #f8fafc; }\n\n @media print {\n body { max-width: 100%; padding: 0; }\n .print-actions { display: none !important; }\n th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }\n }\n`;\n\n/**\n * Wrap body HTML in a complete, styled print document.\n */\nexport function buildPrintDocument(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${escapeHtml(title)}</title>\n <style>\n${PRINT_STYLES}\n${extraStyles}\n </style>\n</head>\n<body>\n <div class=\"print-actions\">\n <button class=\"print-btn print-btn-primary\" onclick=\"window.print()\">Print / Save as PDF</button>\n <button class=\"print-btn print-btn-secondary\" onclick=\"window.close()\">Close</button>\n </div>\n <div class=\"content\">\n${bodyHtml}\n </div>\n</body>\n</html>`;\n}\n\n/**\n * How a print request actually reached the user.\n *\n * \"opened\" — a print window/tab was opened and the document written into it.\n * \"downloaded\" — the popup was blocked, so the document was downloaded as an\n * .html file instead (the contract's fallback). Hosts SHOULD\n * surface this to the user (e.g. a toast saying the print file\n * was downloaded) — a blocked popup plus a silent download is\n * indistinguishable from \"nothing happened\", which is exactly\n * how QA filed it (2026-08-30, F5/F6/F8).\n */\nexport type PrintOutcome = \"opened\" | \"downloaded\";\n\n/**\n * Open a new browser window and write a complete HTML document into it.\n * Falls back to downloading an .html file if the popup is blocked.\n *\n * Returns which path was taken so callers can tell the user when the\n * fallback fired. `windowFeatures` customizes the popup geometry (the\n * flashcards fixed-geometry variants pass a wider window).\n */\n/**\n * Every printed document leaves through here, so this is where typeset math\n * gets its stylesheet: when the page carries KaTeX output and does not already\n * carry the sheet, it is added to <head>. A page with no math is untouched,\n * byte for byte (the flashcard golden master holds every printer to that).\n */\nexport function withMathStyles(htmlDocument: string): string {\n if (!htmlDocument.includes('class=\"katex\"') || htmlDocument.includes(KATEX_CSS_MARKER)) {\n return htmlDocument;\n }\n const style = `<style data-matrx-katex>${KATEX_CSS}</style>`;\n const head = htmlDocument.search(/<\\/head>/i);\n return head >= 0\n ? htmlDocument.slice(0, head) + style + htmlDocument.slice(head)\n : style + htmlDocument;\n}\n\nexport function openPrintWindow(\n rawDocument: string,\n filename = \"print\",\n windowFeatures = \"width=920,height=720,scrollbars=yes\",\n): PrintOutcome {\n const htmlDocument = withMathStyles(rawDocument);\n const win = openOutbound(\"\", \"_blank\", windowFeatures);\n // Popup blocked — fall back to file download.\n if (!win) return openPrintWindowFallback(htmlDocument, filename);\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n return \"opened\";\n}\n\n/** A print window opened before its document is ready (see `openPendingPrintWindow`). */\nexport interface PendingPrintWindow {\n /** Write the finished document; downloads it when the popup was blocked or closed. */\n write(htmlDocument: string): PrintOutcome;\n}\n\n/**\n * Open the print window NOW — inside the click, where popup blockers allow it —\n * and write the document later, once async preparation (drawing diagrams) is\n * done. The window says \"Preparing…\" meanwhile.\n */\nexport function openPendingPrintWindow(\n filename = \"print\",\n windowFeatures = \"width=920,height=720,scrollbars=yes\",\n): PendingPrintWindow {\n const win = openOutbound(\"\", \"_blank\", windowFeatures);\n if (win) {\n win.document.open();\n win.document.write(\n '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>Preparing…</title></head><body style=\"font:14px -apple-system,BlinkMacSystemFont,sans-serif;color:#555;padding:2rem\">Preparing the document for print…</body></html>',\n );\n win.document.close();\n }\n return {\n write(rawDocument: string): PrintOutcome {\n const htmlDocument = withMathStyles(rawDocument);\n if (!win || win.closed) return openPrintWindowFallback(htmlDocument, filename);\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n return \"opened\";\n },\n };\n}\n\nfunction openPrintWindowFallback(htmlDocument: string, filename: string): PrintOutcome {\n downloadFile(`${filename.replace(/\\s+/g, \"-\").toLowerCase()}.html`, htmlDocument, \"text/html;charset=utf-8\");\n return \"downloaded\";\n}\n\n/**\n * Shorthand: build the document and open it in one call.\n * Returns the same PrintOutcome as openPrintWindow.\n */\nexport function printHtmlContent(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): PrintOutcome {\n const doc = buildPrintDocument(bodyHtml, title, extraStyles);\n return openPrintWindow(doc, title);\n}\n\n/**\n * Build the auto-shrink script that runs INSIDE a printed window: for every\n * element matching each selector, its font-size is reduced (0.5pt steps, 6pt\n * floor) until it fits its parent element's box. This is the flashcards\n * FIT_TEXT behavior verbatim, parameterized by selector so every lane\n * (flashcards card text, Avery text, label captions) shares the ONE loop.\n *\n * Pass the same selectors the original used to get byte-identical behavior:\n * flashcards passes `[\".card-text .card-text-inner\", \".avery-text-wrap .avery-text\"]`.\n */\nexport function buildFitTextScript(selectors: readonly string[]): string {\n const selectorsJson = JSON.stringify([...selectors]);\n return `\n<script>\n(function() {\n function fitText(el, container) {\n // container is the bounding box we must fit inside\n var maxW = container.clientWidth;\n var maxH = container.clientHeight;\n var size = parseFloat(window.getComputedStyle(el).fontSize);\n var minSize = 6;\n // Give the element a chance to reflow at current size before measuring\n el.style.fontSize = size + \"pt\";\n while (size > minSize && (el.scrollWidth > maxW + 1 || el.scrollHeight > maxH + 1)) {\n size -= 0.5;\n el.style.fontSize = size + \"pt\";\n }\n }\n function runAll() {\n var selectors = ${selectorsJson};\n for (var s = 0; s < selectors.length; s++) {\n var els = document.querySelectorAll(selectors[s]);\n for (var i = 0; i < els.length; i++) {\n fitText(els[i], els[i].parentElement);\n }\n }\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", runAll);\n } else {\n // document.write pages may already be loaded\n setTimeout(runAll, 0);\n }\n})();\n</script>`;\n}\n\n\n// ─── DOM print: a rendered body, as it looks ─────────────────────────────────\n\nexport interface PrintElementOptions {\n /** The print window's title (and the download fallback's file name). */\n title?: string;\n /** Open the system print dialog once the window has loaded. Default true. */\n autoPrint?: boolean;\n /** Keep buttons, toolbars and menus in the print (default false: they are chrome, not content). */\n keepInteractive?: boolean;\n}\n\n/**\n * Interactive chrome that is not content: anything marked `data-print-hide`,\n * toolbars and menus, and buttons (\"Chart this\", \"Save to\", icon actions).\n * A button stays when it IS content — marked `data-print-keep`, or a disclosure\n * trigger (`aria-expanded`) whose label is a heading — and so does anything\n * inside a `data-print-keep` region. Removal runs on the print clone only.\n */\nconst PRINT_CHROME_SELECTOR = [\n \"[data-print-hide]\",\n '[role=\"toolbar\"]',\n '[role=\"menu\"]',\n '[role=\"menubar\"]',\n \"button\",\n '[role=\"button\"]',\n 'input[type=\"button\"]',\n 'input[type=\"submit\"]',\n 'input[type=\"reset\"]',\n].join(\",\");\n\nexport function stripInteractiveChrome(root: ParentNode): number {\n let removed = 0;\n root.querySelectorAll(PRINT_CHROME_SELECTOR).forEach((node) => {\n if (!root.contains(node)) return; // already gone with a removed ancestor\n const explicit = node.hasAttribute(\"data-print-hide\");\n if (!explicit) {\n if (node.closest(\"[data-print-keep]\")) return;\n if (node.matches(\"button, [role=\\\"button\\\"]\") && node.hasAttribute(\"aria-expanded\")) return;\n }\n node.remove();\n removed += 1;\n });\n return removed;\n}\n\nconst ELEMENT_PRINT_CSS = `\n html, body { height: auto !important; min-height: 0 !important; overflow: visible !important; background: #fff !important; }\n body { margin: 0; padding: 16px; }\n *, *::before, *::after { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }\n .matrx-print-root, .matrx-print-root * { max-height: none !important; }\n .matrx-print-root [class*=\"overflow-\"], .matrx-print-root [class*=\"h-full\"], .matrx-print-root [class*=\"min-h-0\"] { overflow: visible !important; height: auto !important; }\n .matrx-print-frame-notice { border: 1px dashed #94a3b8; border-radius: 6px; padding: 10px 12px; color: #475569; font: italic 12px sans-serif; }\n @media print { .matrx-print-root * { break-inside: auto; } img, svg, figure, tr { break-inside: avoid; } }\n`;\n\n/**\n * Prints a same-origin DOM body exactly as it is drawn: the element's markup\n * with the page's own stylesheets, light theme, backgrounds kept, scroll\n * containers opened to full height. A `<canvas>` prints as its picture; an\n * `<iframe>` cannot be copied and prints an honest notice in its place (a\n * frame kind declares its own print instead). Opens inside the click.\n */\nexport function printElement(element: HTMLElement, options: PrintElementOptions = {}): PrintOutcome {\n const title = options.title ?? (document.title || \"Print\");\n const head = [...document.querySelectorAll<HTMLLinkElement | HTMLStyleElement>('link[rel=\"stylesheet\"], style')]\n .map((node) => {\n if (node instanceof HTMLLinkElement) return `<link rel=\"stylesheet\" href=\"${escapeHtml(node.href)}\">`;\n return `<style>${node.textContent ?? \"\"}</style>`;\n })\n .join(\"\\n\");\n const clone = element.cloneNode(true) as HTMLElement;\n const liveCanvases = element.querySelectorAll(\"canvas\");\n clone.querySelectorAll(\"canvas\").forEach((canvas, i) => {\n const live = liveCanvases[i];\n const img = document.createElement(\"img\");\n try {\n img.src = live ? live.toDataURL(\"image/png\") : \"\";\n } catch {\n img.alt = \"This drawing cannot be copied for print.\";\n }\n img.style.maxWidth = \"100%\";\n canvas.replaceWith(img);\n });\n clone.querySelectorAll(\"iframe\").forEach((frame) => {\n const note = document.createElement(\"p\");\n note.className = \"matrx-print-frame-notice\";\n note.textContent = \"An embedded page is here — print it from its own Print.\";\n frame.replaceWith(note);\n });\n clone.querySelectorAll(\"script\").forEach((node) => node.remove());\n if (options.keepInteractive !== true) stripInteractiveChrome(clone);\n const htmlClass = document.documentElement.className.replace(/\\bdark\\b/g, \"light\");\n const autoPrint = options.autoPrint ?? true;\n const doc = `<!DOCTYPE html><html lang=\"en\" class=\"${escapeHtml(htmlClass)}\"><head><meta charset=\"UTF-8\"><base href=\"${escapeHtml(\n document.baseURI,\n )}\"><title>${escapeHtml(title)}</title>${head}<style>${ELEMENT_PRINT_CSS}</style></head><body><div class=\"matrx-print-root\">${\n clone.outerHTML\n }</div>${\n autoPrint\n ? '<script>window.addEventListener(\"load\",function(){setTimeout(function(){window.focus();window.print();},250);});</script>'\n : \"\"\n }</body></html>`;\n return openPrintWindow(doc, title);\n}\n","// GENERATED by scripts/gen-katex-css.mjs from katex@0.18.9 — do not edit.\nexport const KATEX_VERSION = \"0.18.9\";\n/** Leads KATEX_CSS; lets openPrintWindow see the sheet is already present. */\nexport const KATEX_CSS_MARKER = \"/* matrx-katex@0.18.9 */\";\nexport const KATEX_CSS = \"/* matrx-katex@0.18.9 */@font-face{font-display:block;font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.ttf) format(\\\"truetype\\\")}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;position:relative;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:\\\"0.18.9\\\"}.katex .katex-mathml{border:0;-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.katex-newline{display:block}.katex .katex-base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .katex-base,.katex .katex-strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .katex-vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .katex-thinbox{display:inline-flex;flex-direction:row;max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline,.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-rule,.katex .katex-underline .underline-line,.katex .mfrac .frac-line{min-height:1px}.katex .mspace{display:inline-block}.katex .katex-smash{display:inline;line-height:0}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.katex-inner,.katex .llap>.katex-inner,.katex .rlap>.katex-inner{position:absolute}.katex .clap>.katex-fix,.katex .llap>.katex-fix,.katex .rlap>.katex-fix{display:inline-block}.katex .llap>.katex-inner{right:0}.katex .clap>.katex-inner,.katex .rlap>.katex-inner{left:0}.katex .clap>.katex-inner>span{margin-left:-50%;margin-right:50%}.katex .katex-rule{border:0 solid;display:inline-block;position:relative}.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.katex-root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .katex-sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .katex-sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .katex-sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .katex-sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .katex-sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .katex-sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .katex-sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .katex-sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .katex-sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .katex-sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .katex-sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .katex-sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .katex-sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .katex-sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .katex-sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .katex-sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .katex-sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .katex-sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .katex-sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .katex-sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .katex-sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .katex-sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .katex-sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .katex-sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .katex-sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .katex-sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .katex-sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .katex-sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .katex-sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .katex-sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .katex-sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .katex-sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .katex-sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .katex-sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .katex-sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .katex-sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .katex-sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .katex-sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .katex-sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .katex-sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .katex-sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .katex-sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .katex-sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .katex-sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .katex-sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .katex-sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .katex-sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .katex-sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .katex-sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .katex-sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .katex-sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .katex-sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .katex-sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .katex-sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .katex-sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .katex-sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .katex-sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .katex-sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .katex-sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .katex-sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .katex-sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .katex-sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .katex-sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .katex-sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .katex-sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .katex-sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .katex-sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .katex-sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .katex-sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .katex-sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .katex-sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .katex-sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .katex-sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .katex-sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .katex-sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .katex-sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .katex-sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .katex-sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .katex-sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .katex-sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .katex-sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .katex-sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .katex-sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .katex-sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .katex-sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .katex-sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .katex-sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .katex-sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .katex-sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .katex-sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .katex-sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .katex-sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .katex-sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .katex-sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .katex-sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .katex-sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .katex-sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .katex-sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .katex-sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .katex-sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .katex-sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .katex-sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .katex-sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .katex-sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .katex-sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .katex-sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .katex-sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .katex-sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .katex-sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .katex-sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .katex-sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .katex-sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .katex-sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .katex-sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .katex-sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .katex-sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .katex-sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .katex-sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .katex-sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .katex-sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .katex-sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .katex-accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .katex-accent .accent-body{position:relative}.katex .katex-accent .accent-body:not(.accent-full){width:0}.katex .katex-overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex svg{fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .katex-stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .katex-stretchy:after,.katex .katex-stretchy:before{content:\\\"\\\"}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .katex-sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .reflectbox{display:inline-block;transform:scaleX(-1)}.katex .eqn-num:before{content:\\\"(\\\" counter(katexEqnNo) \\\")\\\";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:\\\"(\\\" counter(mmlEqnNo) \\\")\\\";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.katex-tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.katex-tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}\\n\";\n","/**\n * Plain text with math → safe HTML: THE one way a printer turns authored text\n * (a flashcard face, an exam prompt, a glossary definition) into markup.\n *\n * Every non-markdown printer used to `escapeHtml()` its text, so `\\(\\text{IO}_3^-\\)`\n * on a flashcard printed as that literal source (Arman, 2026-09-27). What is math\n * is decided by the shared tokenizer (`extractMath` — the same rule the screen,\n * the markdown lane and the /document pipeline use: code and `$5` prices are\n * never math); everything else is escaped exactly as before.\n *\n * Typeset output is KaTeX's laid-out HTML plus MathML, chemistry (mhchem)\n * loaded. KaTeX runs with `trust: false`, which refuses every command that could\n * inject HTML (\\href, \\url, \\htmlClass, \\htmlId, \\htmlStyle, \\htmlData,\n * \\includegraphics). The stylesheet is added by `openPrintWindow` whenever a\n * document contains math, so no printer can forget it.\n */\nimport katex from \"katex\";\n// Registers \\ce{…} and \\pu{…} on the same katex instance.\nimport \"katex/contrib/mhchem\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\nimport { extractMath, MATH_TOKEN_RE } from \"./document/math\";\n\n/** Typeset one TeX span — the ONE KaTeX configuration every print lane uses. */\nexport function typesetTex(tex: string, display: boolean): string {\n return katex.renderToString(tex, {\n displayMode: display,\n output: \"htmlAndMathml\",\n throwOnError: false,\n trust: false,\n strict: \"ignore\",\n });\n}\n\n/**\n * Escape `text` for HTML, with every math span typeset. Newlines are kept as\n * `\\n` in the prose so callers that turn them into `<br>` keep doing so; a\n * newline inside a formula is folded to a space (TeX treats it as one).\n */\nexport function mathTextHtml(text: string | null | undefined): string {\n // The tokenizer's own markers can never come from an author.\n const source = (text ?? \"\").replace(/[\\uE100\\uE101]/g, \"\");\n if (!/[$\\\\]/.test(source)) return escapeHtml(source);\n const { text: prose, spans } = extractMath(source);\n if (spans.length === 0) return escapeHtml(source);\n return escapeHtml(prose).replace(MATH_TOKEN_RE, (_m: string, i: string) => {\n const span = spans[Number(i)];\n return span ? typesetTex(span.tex.replace(/\\s*\\n\\s*/g, \" \"), span.display) : \"\";\n });\n}\n\n/**\n * A field that IS a formula (a cheat-sheet \"formula\" slot): delimited math is\n * typeset as usual; undelimited TeX (it has a command, `^` or `_`) is typeset\n * whole as inline math; anything else is escaped text.\n */\nexport function formulaHtml(formula: string | null | undefined): string {\n const source = (formula ?? \"\").trim();\n const withMath = mathTextHtml(source);\n if (withMath.includes('class=\"katex\"')) return withMath;\n return /\\\\[a-zA-Z]|[\\^_]/.test(source) ? typesetTex(source, false) : withMath;\n}\n","/**\n * Math in documents — ONE math dialect, typeset in every format.\n *\n * What is math is decided ONLY by the shared tokenizer (`@ai-matrx/content-ir/\n * source`): its fence-nesting rule finds code, its pairing finds `$$…$$`,\n * `$…$` (math, not currency), `\\[…\\]` and `\\(…\\)`. Math is lifted out\n * BEFORE markdown parsing, so a TeX `\\,` is never eaten as a markdown escape\n * and `_`/`*` inside a formula never become emphasis.\n *\n * Typesetting: MathJax (TeX → its internal tree) produces\n * - SVG for PDF display math (drawn as vector paths; allow-list sanitized),\n * - MathML for HTML and EPUB (rendered natively by browsers and readers),\n * - MathML that the Word writer turns into native OMML equations and the\n * PDF writer turns into inline text with real super/subscripts.\n */\n\nimport { listIslands, tokenizeSource } from \"@ai-matrx/content-ir/source\";\nimport { sanitizeMathml, sanitizeMathSvg } from \"../safe-html\";\n\nexport const MATH_OPEN = \"\\uE100\";\nexport const MATH_CLOSE = \"\\uE101\";\nexport const MATH_TOKEN_RE = /\\uE100(\\d+)\\uE101/g;\n\nexport interface MathSpan {\n tex: string;\n display: boolean;\n}\n\nconst DELIMITERS: Record<string, { open: number; close: number; display: boolean }> = {\n $$: { open: 2, close: 2, display: true },\n \"\\\\[\": { open: 2, close: 2, display: true },\n $: { open: 1, close: 1, display: false },\n \"\\\\(\": { open: 2, close: 2, display: false },\n};\n\n/**\n * Replace every math span with a private-use token; returns the spans.\n *\n * Print keeps NO fence, code-span or math detection of its own: the spans are\n * the shared tokenizer's complete `math_block` / `math_inline` islands\n * (`tokenizeSource` + `listIslands` from `@ai-matrx/content-ir/source` — the\n * one fence-nesting rule and the one math pairing), so print, the renderer and\n * the editor can never disagree about what is math. The delimiter decides the\n * form: `$$`/`\\[` display, `$`/`\\(` inline.\n */\nexport function extractMath(body: string): { text: string; spans: MathSpan[] } {\n const spans: MathSpan[] = [];\n if (!/[$\\\\]/.test(body)) return { text: body, spans };\n const islands = listIslands(tokenizeSource(body))\n .filter((isl) => isl.complete && (isl.islandType === \"math_block\" || isl.islandType === \"math_inline\"))\n .sort((a, b) => a.start - b.start);\n let out = \"\";\n let at = 0;\n for (const isl of islands) {\n const d = DELIMITERS[String(isl.meta.delimiter ?? \"\")];\n if (!d || isl.start < at) continue;\n out += body.slice(at, isl.start);\n spans.push({ tex: body.slice(isl.start + d.open, isl.end - d.close).trim(), display: d.display });\n out += `${MATH_OPEN}${spans.length - 1}${MATH_CLOSE}`;\n at = isl.end;\n }\n return { text: out + body.slice(at), spans };\n}\n\n// ── typesetting ────────────────────────────────────────────────────────────\n\nexport interface TypesetMath {\n /** MathML (<math …>…</math>) for HTML/EPUB and the Word/PDF-inline writers. */\n mathml: string;\n /** SVG for PDF display math; widths in ex (1ex ≈ 0.442em in MathJax). */\n svg?: string | undefined;\n widthEx?: number | undefined;\n heightEx?: number | undefined;\n /** Error message when the TeX could not be typeset. */\n error?: string | undefined;\n}\n\nexport const mathKey = (tex: string, display: boolean): string => `${display ? \"D\" : \"I\"}:${tex}`;\n\ntype MathJaxKit = import(\"../mathjax\").MathJaxKit;\n\nconst MATHJAX = Symbol.for(\"ai-matrx.print.document.mathjax\");\n\nasync function loadMathJax(): Promise<MathJaxKit> {\n const g = globalThis as unknown as Record<symbol, Promise<MathJaxKit> | undefined>;\n if (g[MATHJAX]) return g[MATHJAX] as Promise<MathJaxKit>;\n // The self-referencing specifier keeps this a real lazy edge in dist (see src/mathjax.ts).\n const pending = import(\"@ai-matrx/print/mathjax\").then((m) => m.createMathJaxKit());\n pending.catch(() => {\n if (g[MATHJAX] === pending) g[MATHJAX] = undefined;\n });\n g[MATHJAX] = pending;\n return pending;\n}\n\n/** Typeset every math span once; keyed by `mathKey`. */\nexport async function typesetMathSpans(spans: MathSpan[]): Promise<Map<string, TypesetMath>> {\n const out = new Map<string, TypesetMath>();\n if (spans.length === 0) return out;\n const mj = await loadMathJax();\n for (const s of spans) {\n const key = mathKey(s.tex, s.display);\n if (out.has(key)) continue;\n try {\n const mathml = mj.mml(s.tex, s.display);\n const err = /<merror[\\s\\S]*?<mtext[^>]*>([\\s\\S]*?)<\\/mtext>/.exec(mathml);\n if (err) {\n out.set(key, { mathml: sanitizeMathml(mathml), error: decodeXml(err[1] as string) });\n continue;\n }\n // TeX is author input: `\\href`, `\\style`, `\\bbox[…]`, `\\mmlToken` reach\n // MathML attributes, so the typeset MathML goes through the allow-list.\n const entry: TypesetMath = { mathml: sanitizeMathml(mathml.replace(/\\n\\s*/g, \"\")) };\n if (s.display) {\n // The same author TeX reaches the SVG as <a href>, style, class and id\n // (LIST T26): only the drawing survives.\n const svg = sanitizeMathSvg(mj.svg(s.tex, true));\n entry.svg = svg;\n entry.widthEx = Number(/width=\"([\\d.]+)ex\"/.exec(svg)?.[1] ?? 0);\n entry.heightEx = Number(/height=\"([\\d.]+)ex\"/.exec(svg)?.[1] ?? 0);\n }\n out.set(key, entry);\n } catch (e) {\n out.set(key, { mathml: \"\", error: (e as Error).message });\n }\n }\n return out;\n}\n\n// ── a small MathML reader (MathJax's serializer output is well-formed XML) ──\n\nexport interface MNode {\n tag: string;\n attrs: Record<string, string>;\n children: MNode[];\n text: string;\n}\n\nexport function decodeXml(s: string): string {\n return s.replace(/&(#x[0-9a-f]+|#\\d+|amp|lt|gt|quot|apos);/gi, (m, e: string) => {\n if (e[0] === \"#\") return String.fromCodePoint(e[1] === \"x\" || e[1] === \"X\" ? parseInt(e.slice(2), 16) : Number(e.slice(1)));\n return ({ amp: \"&\", lt: \"<\", gt: \">\", quot: '\"', apos: \"'\" } as Record<string, string>)[e.toLowerCase()] ?? m;\n });\n}\n\nexport function parseMathML(xml: string): MNode {\n const root: MNode = { tag: \"#root\", attrs: {}, children: [], text: \"\" };\n const stack: MNode[] = [root];\n for (const m of xml.matchAll(/<(\\/?)([\\w:-]+)((?:\\s+[\\w:-]+=\"[^\"]*\")*)\\s*(\\/?)>|([^<]+)/g)) {\n const top = stack[stack.length - 1] as MNode;\n if (m[5] !== undefined) {\n const t = decodeXml(m[5]);\n if (t.trim() || top.tag === \"mtext\" || top.tag === \"mo\") top.text += t;\n continue;\n }\n if (m[1] === \"/\") {\n if (stack.length > 1) stack.pop();\n continue;\n }\n const attrs: Record<string, string> = {};\n for (const a of (m[3] ?? \"\").matchAll(/([\\w:-]+)=\"([^\"]*)\"/g)) attrs[a[1] as string] = decodeXml(a[2] as string);\n const node: MNode = { tag: (m[2] as string).replace(/^m:/, \"\"), attrs, children: [], text: \"\" };\n top.children.push(node);\n if (m[4] !== \"/\") stack.push(node);\n }\n return root.children[0] ?? root;\n}\n\n/** Flatten to plain text (the linear reading of a formula). */\nexport function mathText(n: MNode): string {\n if (n.children.length === 0) return n.text;\n return n.children.map(mathText).join(\"\");\n}\n","/**\n * @ai-matrx/print/exam — the practice-test lane (question booklet, bubble\n * answer form, answer key, explanations).\n *\n * ── THE ANSWER-SEPARATION LAW ───────────────────────────────────────────────\n * The education research (PRINT-EDUCATION-RESEARCH.md, P0 row \"Practice test\n * booklets + bubble answer sheets\", modeled on Barron's / Princeton Review AP\n * and Kaplan) is explicit: the question booklet, the scannable bubble form and\n * the answer key are SEPARATE printable units. That is not a styling\n * preference — it is what makes a practice test re-usable: a student prints\n * ONE booklet and a fresh blank bubble form per attempt, and the key never\n * travels with the questions. So `question-booklet` emits no answers and no\n * explanations at all (there is a test asserting their ABSENCE), and every\n * variant here is an independently printable document.\n *\n * ── WHY THERE IS NO \"SHUFFLE\" SETTING ───────────────────────────────────────\n * Question order and choice order are DATA, not presentation. A printer that\n * shuffled would desynchronize the three units above — a bubble form or key\n * printed in a different session would no longer match the booklet, silently.\n * Shuffling belongs to whatever generates the PracticeTestData (where the\n * shuffled order can be persisted once and reused by all three units).\n *\n * Zero dependencies; `window` is touched only inside `print`, at call time, so\n * the module stays import-inert and RSC-safe.\n */\n\nimport {\n buildPrintDocument,\n openPrintWindow,\n type BlockPrinter,\n type PrintOutcome,\n type PrintSettings,\n} from \"./core\";\nimport { mathTextHtml } from \"./math-text\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface ExamChoice {\n /** Optional explicit label. Omitted = auto-lettered A, B, C, … by position. */\n label?: string;\n text: string;\n}\n\nexport interface ExamQuestion {\n prompt: string;\n /** Omitted or empty = free-response (ruled work space instead of bubbles). */\n choices?: ExamChoice[];\n /** The correct choice label (or, for free response, the model answer text). */\n answer?: string;\n explanation?: string;\n points?: number;\n /** Ruled lines to print under a free-response question in the booklet. */\n workSpaceLines?: number;\n}\n\nexport interface PracticeTestData {\n title: string;\n instructions?: string;\n questions: ExamQuestion[];\n}\n\nexport type PracticeTestVariant =\n | \"question-booklet\"\n | \"bubble-sheet\"\n | \"answer-key\"\n | \"explanations\";\n\n// ─── Geometry constants (bubble sheet) ───────────────────────────────────────\n//\n// Letter portrait, 0.5in page margin. 25 rows per column is the scanner-form\n// convention (Scantron 882-E and the AP answer sheets both group in 25s) and\n// it also keeps a 5-choice row inside a 1.75in column at a 0.28in pitch.\n// Everything below is INCH-exact so a physical ruler laid on the print\n// matches: circles are 0.16in across, drawn as CSS borders (never images, and\n// never a background fill that a \"save ink\" print mode could drop).\n\n/** Rows per bubble column — the scanner-form convention. */\nconst BUBBLE_ROWS_PER_COLUMN = 25;\n/** Columns printed side by side before a page break. */\nconst BUBBLE_COLUMNS_PER_PAGE = 4;\n/** Max bubbles a row can carry (A–J) before we stop lettering. */\nconst CHOICE_LETTERS = \"ABCDEFGHIJ\";\n\n/** Auto-letter for choice index i; falls back to the number past the alphabet. */\nfunction letterFor(index: number): string {\n return CHOICE_LETTERS[index] ?? String(index + 1);\n}\n\n/** The label a choice prints under (explicit label wins over the auto letter). */\nfunction choiceLabel(choice: ExamChoice, index: number): string {\n const explicit = choice.label?.trim();\n return explicit && explicit.length > 0 ? explicit : letterFor(index);\n}\n\nfunction isFreeResponse(question: ExamQuestion): boolean {\n return !question.choices || question.choices.length === 0;\n}\n\n// ─── Data extraction ─────────────────────────────────────────────────────────\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0\n ? value.trim()\n : undefined;\n}\n\nfunction asNumber(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\nfunction parseChoice(raw: unknown): ExamChoice | null {\n if (typeof raw === \"string\") {\n return raw.trim().length > 0 ? { text: raw } : null;\n }\n if (!raw || typeof raw !== \"object\") return null;\n const obj = raw as Record<string, unknown>;\n const text = asString(obj[\"text\"]);\n if (text === undefined) return null;\n const label = asString(obj[\"label\"]);\n // exactOptionalPropertyTypes: only spread the key when it actually has a value.\n return label === undefined ? { text } : { label, text };\n}\n\nfunction parseQuestion(raw: unknown): ExamQuestion | null {\n if (!raw || typeof raw !== \"object\") return null;\n const obj = raw as Record<string, unknown>;\n const prompt = asString(obj[\"prompt\"]) ?? asString(obj[\"question\"]);\n if (prompt === undefined) return null;\n\n const question: ExamQuestion = { prompt };\n\n const rawChoices = obj[\"choices\"] ?? obj[\"options\"];\n if (Array.isArray(rawChoices)) {\n const choices = rawChoices\n .map((c) => parseChoice(c))\n .filter((c): c is ExamChoice => c !== null);\n if (choices.length > 0) question.choices = choices;\n }\n const answer = asString(obj[\"answer\"]);\n if (answer !== undefined) question.answer = answer;\n const explanation = asString(obj[\"explanation\"]);\n if (explanation !== undefined) question.explanation = explanation;\n const points = asNumber(obj[\"points\"]);\n if (points !== undefined) question.points = points;\n const workSpaceLines = asNumber(obj[\"workSpaceLines\"]);\n if (workSpaceLines !== undefined) {\n question.workSpaceLines = Math.max(0, Math.floor(workSpaceLines));\n }\n return question;\n}\n\n/**\n * Accept the canonical shape, a bare questions array, or a wrapper object that\n * carries one — the same tolerance the flashcards lane offers its hosts.\n */\nexport function extractPracticeTest(data: unknown): PracticeTestData {\n const empty: PracticeTestData = { title: \"Practice Test\", questions: [] };\n if (!data) return empty;\n\n if (Array.isArray(data)) {\n return {\n title: \"Practice Test\",\n questions: data\n .map(parseQuestion)\n .filter((q): q is ExamQuestion => q !== null),\n };\n }\n if (typeof data !== \"object\") return empty;\n\n const obj = data as Record<string, unknown>;\n const rawQuestions = obj[\"questions\"] ?? obj[\"items\"];\n const questions = Array.isArray(rawQuestions)\n ? rawQuestions.map(parseQuestion).filter((q): q is ExamQuestion => q !== null)\n : [];\n\n const test: PracticeTestData = {\n title: asString(obj[\"title\"]) ?? \"Practice Test\",\n questions,\n };\n const instructions = asString(obj[\"instructions\"]);\n if (instructions !== undefined) test.instructions = instructions;\n return test;\n}\n\n// ─── Settings ────────────────────────────────────────────────────────────────\n\ninterface ResolvedExamSettings {\n /** 1-based inclusive question range; both 0 = whole test. */\n from: number;\n to: number;\n /** 0 = derive the bubble count from each question's own choices. */\n bubbleLetters: number;\n showPointsColumn: boolean;\n}\n\nfunction resolveSettings(raw?: PrintSettings): ResolvedExamSettings {\n const num = (key: string): number => {\n const v = raw?.[key];\n return typeof v === \"number\" && Number.isFinite(v) && v > 0\n ? Math.floor(v)\n : 0;\n };\n return {\n from: num(\"fromQuestion\"),\n to: num(\"toQuestion\"),\n bubbleLetters: Math.min(num(\"bubbleLetters\"), CHOICE_LETTERS.length),\n showPointsColumn: raw?.[\"showPointsColumn\"] === true,\n };\n}\n\n/**\n * Slice by the range setting. Returns the questions AND the 0-based offset of\n * the first one, so every variant keeps the ORIGINAL question numbers (a key\n * for questions 26–50 must say \"26.\", never \"1.\").\n */\nfunction applyRange(\n questions: readonly ExamQuestion[],\n s: ResolvedExamSettings,\n): { questions: ExamQuestion[]; startIndex: number } {\n if (questions.length === 0) return { questions: [], startIndex: 0 };\n const start = s.from > 0 ? Math.min(s.from, questions.length) : 1;\n const end = s.to > 0 ? Math.min(s.to, questions.length) : questions.length;\n const [lo, hi] = start <= end ? [start, end] : [end, start];\n return { questions: questions.slice(lo - 1, hi), startIndex: lo - 1 };\n}\n\n// ─── Shared styles ───────────────────────────────────────────────────────────\n\nconst EXAM_BASE_STYLES = `\n .ex-title { font-size: 18pt; font-weight: 700; margin-bottom: 2px; }\n .ex-subtitle { font-size: 10pt; color: #475569; margin-bottom: 14px; }\n .ex-instructions {\n border: 2px solid #1e293b;\n border-radius: 6px;\n padding: 10px 14px;\n margin: 0 0 18px;\n font-size: 10.5pt;\n line-height: 1.5;\n background: #f8fafc;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n .ex-instructions-heading {\n font-size: 9pt;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: #1e293b;\n margin-bottom: 4px;\n }\n`;\n\n// ─── Variant: question booklet ───────────────────────────────────────────────\n\nconst BOOKLET_VARIANT_STYLES = `\n ${EXAM_BASE_STYLES}\n .ex-q {\n /* A question never splits across a page mid-choice-list — a student\n reading choice (C) at the top of the next page misreads the item. */\n break-inside: avoid;\n page-break-inside: avoid;\n margin: 0 0 16px;\n }\n .ex-q-head { display: flex; gap: 8px; align-items: baseline; }\n .ex-q-num { font-weight: 700; min-width: 1.9em; }\n .ex-q-prompt { flex: 1; font-size: 11pt; line-height: 1.5; }\n .ex-q-points {\n font-size: 8.5pt;\n color: #475569;\n white-space: nowrap;\n font-weight: 600;\n }\n .ex-choices { list-style: none; margin: 6px 0 0 1.9em; padding: 0; }\n .ex-choice { display: flex; gap: 7px; font-size: 10.5pt; line-height: 1.45; margin-bottom: 3px; }\n .ex-choice-label { font-weight: 600; min-width: 1.3em; }\n .ex-work { margin: 8px 0 0 1.9em; }\n .ex-work-line {\n border-bottom: 1px solid #94a3b8;\n height: 0.32in;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n`;\n\nfunction renderPoints(question: ExamQuestion): string {\n if (question.points === undefined) return \"\";\n const unit = question.points === 1 ? \"pt\" : \"pts\";\n return `<span class=\"ex-q-points\">(${escapeHtml(String(question.points))} ${unit})</span>`;\n}\n\nfunction renderQuestionBooklet(\n test: PracticeTestData,\n questions: readonly ExamQuestion[],\n startIndex: number,\n): string {\n const instructions = test.instructions\n ? `<div class=\"ex-instructions\">\n <div class=\"ex-instructions-heading\">Instructions</div>\n ${mathTextHtml(test.instructions)}\n </div>`\n : \"\";\n\n const items = questions\n .map((q, i) => {\n const number = startIndex + i + 1;\n const choices = (q.choices ?? [])\n .map(\n (c, ci) =>\n `<li class=\"ex-choice\"><span class=\"ex-choice-label\">${escapeHtml(\n choiceLabel(c, ci),\n )}.</span><span>${mathTextHtml(c.text)}</span></li>`,\n )\n .join(\"\");\n const choiceList =\n choices.length > 0 ? `<ul class=\"ex-choices\">${choices}</ul>` : \"\";\n const lineCount = isFreeResponse(q) ? (q.workSpaceLines ?? 0) : 0;\n const work =\n lineCount > 0\n ? `<div class=\"ex-work\">${'<div class=\"ex-work-line\"></div>'.repeat(lineCount)}</div>`\n : \"\";\n return `<div class=\"ex-q\" data-question=\"${number}\">\n <div class=\"ex-q-head\">\n <span class=\"ex-q-num\">${number}.</span>\n <span class=\"ex-q-prompt\">${mathTextHtml(q.prompt)}</span>\n ${renderPoints(q)}\n </div>\n ${choiceList}\n ${work}\n</div>`;\n })\n .join(\"\\n\");\n\n // NO answers, NO explanations here — the answer-separation law.\n return `<div class=\"ex-title\">${mathTextHtml(test.title)}</div>\n<div class=\"ex-subtitle\">Question booklet — mark your answers on the separate answer sheet</div>\n${instructions}\n${items}`;\n}\n\n// ─── Variant: bubble sheet ───────────────────────────────────────────────────\n\nconst BUBBLE_STYLES = `\n @page { size: 8.5in 11in; margin: 0.5in; }\n\n .bs-header {\n border: 2px solid #111;\n padding: 0.12in 0.16in;\n margin-bottom: 0.22in;\n break-inside: avoid;\n page-break-inside: avoid;\n }\n .bs-header-title { font-size: 13pt; font-weight: 700; margin-bottom: 0.06in; }\n .bs-header-note { font-size: 8pt; color: #334155; margin-bottom: 0.1in; }\n .bs-fields { display: flex; gap: 0.2in; }\n .bs-field { flex: 1; font-size: 8.5pt; }\n .bs-field-score { flex: 0 0 1.2in; }\n .bs-field-rule {\n border-bottom: 1px solid #111;\n height: 0.26in;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n .bs-field-label {\n font-size: 7.5pt;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: #334155;\n }\n\n .bs-columns {\n display: flex;\n gap: 0.3in;\n break-after: page;\n page-break-after: always;\n }\n .bs-columns:last-of-type { break-after: auto; page-break-after: auto; }\n\n .bs-column { flex: 1; }\n\n .bs-row {\n display: flex;\n align-items: center;\n height: 0.28in; /* the pitch — every row identical */\n break-inside: avoid;\n page-break-inside: avoid;\n }\n .bs-row-num {\n width: 0.3in;\n font-size: 8.5pt;\n text-align: right;\n padding-right: 0.06in;\n font-variant-numeric: tabular-nums;\n }\n .bs-bubble {\n width: 0.16in;\n height: 0.16in;\n border: 1.25px solid #111;\n border-radius: 50%;\n margin-right: 0.055in;\n font-size: 5.5pt;\n line-height: 0.16in;\n text-align: center;\n color: #111;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n .bs-blank-line {\n flex: 1;\n border-bottom: 1px solid #111;\n height: 0.16in;\n margin-right: 0.06in;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n .bs-points {\n width: 0.34in;\n font-size: 7.5pt;\n color: #334155;\n text-align: right;\n }\n`;\n\n/** How many bubbles row `q` prints: the override, else the question's choices. */\nfunction bubbleCountFor(\n question: ExamQuestion,\n s: ResolvedExamSettings,\n): number {\n if (isFreeResponse(question)) return s.bubbleLetters > 0 ? s.bubbleLetters : 0;\n const own = question.choices?.length ?? 0;\n const n = s.bubbleLetters > 0 ? s.bubbleLetters : own;\n return Math.min(n, CHOICE_LETTERS.length);\n}\n\nfunction renderBubbleRow(\n question: ExamQuestion,\n number: number,\n s: ResolvedExamSettings,\n): string {\n const count = bubbleCountFor(question, s);\n const cells =\n count > 0\n ? Array.from(\n { length: count },\n (_, i) =>\n `<span class=\"bs-bubble\" data-letter=\"${escapeHtml(letterFor(i))}\">${escapeHtml(\n letterFor(i),\n )}</span>`,\n ).join(\"\")\n : // Free response: a short rule to write the answer on, never bubbles.\n `<span class=\"bs-blank-line\"></span>`;\n const points =\n s.showPointsColumn && question.points !== undefined\n ? `<span class=\"bs-points\">${escapeHtml(String(question.points))}</span>`\n : \"\";\n return `<div class=\"bs-row\" data-question=\"${number}\" data-bubbles=\"${count}\">\n <span class=\"bs-row-num\">${number}.</span>${cells}${points}\n </div>`;\n}\n\nfunction renderBubbleSheet(\n test: PracticeTestData,\n questions: readonly ExamQuestion[],\n startIndex: number,\n s: ResolvedExamSettings,\n): string {\n const rows = questions.map((q, i) =>\n renderBubbleRow(q, startIndex + i + 1, s),\n );\n\n // Chunk into fixed 25-row columns, then group columns into page-sized sets.\n const columns: string[] = [];\n for (let i = 0; i < rows.length; i += BUBBLE_ROWS_PER_COLUMN) {\n columns.push(\n `<div class=\"bs-column\">${rows\n .slice(i, i + BUBBLE_ROWS_PER_COLUMN)\n .join(\"\")}</div>`,\n );\n }\n const sets: string[] = [];\n for (let i = 0; i < columns.length; i += BUBBLE_COLUMNS_PER_PAGE) {\n // Pad the last set with empty columns so the surviving columns keep their\n // width — a 1-column final page must not stretch to the full 7.5in.\n const group = columns.slice(i, i + BUBBLE_COLUMNS_PER_PAGE);\n while (group.length < BUBBLE_COLUMNS_PER_PAGE) {\n group.push(`<div class=\"bs-column\"></div>`);\n }\n sets.push(`<div class=\"bs-columns\">${group.join(\"\")}</div>`);\n }\n\n const header = `<div class=\"bs-header\">\n <div class=\"bs-header-title\">${mathTextHtml(test.title)} — Answer Sheet</div>\n <div class=\"bs-header-note\">Fill the bubble completely with a No. 2 pencil. Erase changes fully. Do not write outside the boxes.</div>\n <div class=\"bs-fields\">\n <div class=\"bs-field\"><div class=\"bs-field-rule\"></div><div class=\"bs-field-label\">Name</div></div>\n <div class=\"bs-field\"><div class=\"bs-field-rule\"></div><div class=\"bs-field-label\">Date</div></div>\n <div class=\"bs-field bs-field-score\"><div class=\"bs-field-rule\"></div><div class=\"bs-field-label\">Score</div></div>\n </div>\n</div>`;\n\n return `${header}\n${sets.join(\"\\n\")}`;\n}\n\n// ─── Variant: answer key ─────────────────────────────────────────────────────\n\nconst KEY_STYLES = `\n ${EXAM_BASE_STYLES}\n .ak-grid { column-count: 4; column-gap: 0.3in; }\n .ak-row {\n break-inside: avoid;\n page-break-inside: avoid;\n font-size: 10pt;\n line-height: 1.5;\n display: flex;\n gap: 6px;\n }\n .ak-num { font-weight: 600; min-width: 2em; text-align: right; }\n .ak-answer { font-weight: 700; }\n .ak-points { color: #475569; font-size: 8.5pt; margin-left: auto; }\n .ak-free { column-count: 1; }\n .ak-free .ak-answer { font-weight: 500; }\n`;\n\n/** The printed answer for a question — \"—\" when the data carries none. */\nfunction answerText(question: ExamQuestion): string {\n return question.answer ?? \"—\";\n}\n\nfunction renderAnswerKey(\n test: PracticeTestData,\n questions: readonly ExamQuestion[],\n startIndex: number,\n): string {\n const anyPoints = questions.some((q) => q.points !== undefined);\n // Free-response answers are model-answer PROSE — they can't ride a 4-column\n // grid without becoming unreadable, so they get their own single-column list.\n const compact: string[] = [];\n const prose: string[] = [];\n questions.forEach((q, i) => {\n const number = startIndex + i + 1;\n const points =\n anyPoints && q.points !== undefined\n ? `<span class=\"ak-points\">${escapeHtml(String(q.points))}</span>`\n : \"\";\n const row = `<div class=\"ak-row\" data-question=\"${number}\"><span class=\"ak-num\">${number}.</span><span class=\"ak-answer\">${mathTextHtml(\n answerText(q),\n )}</span>${points}</div>`;\n (isFreeResponse(q) ? prose : compact).push(row);\n });\n\n const compactHtml =\n compact.length > 0 ? `<div class=\"ak-grid\">${compact.join(\"\")}</div>` : \"\";\n const proseHtml =\n prose.length > 0\n ? `<h3>Free-response model answers</h3><div class=\"ak-grid ak-free\">${prose.join(\n \"\",\n )}</div>`\n : \"\";\n\n return `<div class=\"ex-title\">${mathTextHtml(test.title)}</div>\n<div class=\"ex-subtitle\">Answer key — keep separate from the question booklet</div>\n${compactHtml}\n${proseHtml}`;\n}\n\n// ─── Variant: explanations ───────────────────────────────────────────────────\n\nconst EXPLANATIONS_STYLES = `\n ${EXAM_BASE_STYLES}\n .exp-row {\n break-inside: avoid;\n page-break-inside: avoid;\n border-bottom: 1px solid #e2e8f0;\n padding: 6px 0;\n }\n .exp-head { display: flex; gap: 6px; font-size: 10pt; }\n .exp-num { font-weight: 700; min-width: 2em; }\n .exp-answer { font-weight: 700; }\n .exp-prompt { color: #334155; }\n .exp-body { font-size: 9.5pt; line-height: 1.45; margin: 3px 0 0 2em; color: #1f2937; }\n`;\n\nfunction renderExplanations(\n test: PracticeTestData,\n questions: readonly ExamQuestion[],\n startIndex: number,\n): string {\n const rows = questions\n .map((q, i) => {\n const number = startIndex + i + 1;\n const explanation = q.explanation\n ? `<div class=\"exp-body\">${mathTextHtml(q.explanation)}</div>`\n : \"\";\n return `<div class=\"exp-row\" data-question=\"${number}\">\n <div class=\"exp-head\">\n <span class=\"exp-num\">${number}.</span>\n <span class=\"exp-prompt\">${mathTextHtml(q.prompt)}</span>\n </div>\n <div class=\"exp-body\"><span class=\"exp-answer\">Answer: ${mathTextHtml(\n answerText(q),\n )}</span></div>\n ${explanation}\n</div>`;\n })\n .join(\"\\n\");\n\n return `<div class=\"ex-title\">${mathTextHtml(test.title)}</div>\n<div class=\"ex-subtitle\">Answers &amp; explanations — study sheet</div>\n${rows}`;\n}\n\n// ─── The printer ─────────────────────────────────────────────────────────────\n\nconst EMPTY_MESSAGE = \"No practice-test data available to print.\";\n\n/**\n * The practice-test printer. Four variants, each an INDEPENDENT printable unit\n * (see the answer-separation law at the top of this file).\n */\nexport const practiceTestPrinter: BlockPrinter = {\n label: \"Print practice test\",\n variants: [\n {\n id: \"question-booklet\",\n label: \"Question booklet (recommended)\",\n description:\n \"Numbered questions with lettered choices and ruled work space. Contains no answers — print it once and re-use it with a fresh answer sheet each attempt\",\n },\n {\n id: \"bubble-sheet\",\n label: \"Bubble answer sheet\",\n description:\n \"The scannable answer form: name/date/score header and a fixed grid of bubble rows, 25 per column. Free-response items get a write-in line\",\n },\n {\n id: \"answer-key\",\n label: \"Answer key\",\n description:\n \"Compact multi-column key for grading. Keep it away from the booklet\",\n },\n {\n id: \"explanations\",\n label: \"Answers & explanations\",\n description:\n \"Every question with its answer and explanation, at study-sheet density\",\n },\n ],\n settings: [\n {\n type: \"range\" as const,\n id: \"questionRange\",\n label: \"Questions to print\",\n description:\n \"Leave blank to print the whole test. Question numbers stay the original ones\",\n fromId: \"fromQuestion\",\n toId: \"toQuestion\",\n defaultFrom: 0,\n defaultTo: 0,\n min: 1,\n fromPlaceholder: \"1\",\n toPlaceholder: \"end\",\n },\n {\n type: \"number\" as const,\n id: \"bubbleLetters\",\n label: \"Bubbles per row (override)\",\n description:\n \"0 = derive from each question's own choices (recommended). Set 4 or 5 to force a uniform grid\",\n defaultValue: 0,\n min: 0,\n max: CHOICE_LETTERS.length,\n appliesTo: [\"bubble-sheet\"],\n },\n {\n type: \"boolean\" as const,\n id: \"showPointsColumn\",\n label: \"Show points column\",\n description: \"Prints each question's point value beside its bubble row\",\n defaultValue: false,\n appliesTo: [\"bubble-sheet\"],\n },\n ],\n\n print(\n data: unknown,\n variantId: string = \"question-booklet\",\n rawSettings?: PrintSettings,\n ): PrintOutcome {\n const test = extractPracticeTest(data);\n const s = resolveSettings(rawSettings);\n\n if (test.questions.length === 0) {\n return openPrintWindow(\n buildPrintDocument(`<p>${EMPTY_MESSAGE}</p>`, test.title, EXAM_BASE_STYLES),\n \"practice-test\",\n );\n }\n\n const { questions, startIndex } = applyRange(test.questions, s);\n\n switch (variantId as PracticeTestVariant) {\n case \"bubble-sheet\":\n return openPrintWindow(\n buildPrintDocument(\n renderBubbleSheet(test, questions, startIndex, s),\n `${test.title} — Answer Sheet`,\n BUBBLE_STYLES,\n ),\n `${test.title}-answer-sheet`,\n );\n case \"answer-key\":\n return openPrintWindow(\n buildPrintDocument(\n renderAnswerKey(test, questions, startIndex),\n `${test.title} — Answer Key`,\n KEY_STYLES,\n ),\n `${test.title}-answer-key`,\n );\n case \"explanations\":\n return openPrintWindow(\n buildPrintDocument(\n renderExplanations(test, questions, startIndex),\n `${test.title} — Explanations`,\n EXPLANATIONS_STYLES,\n ),\n `${test.title}-explanations`,\n );\n case \"question-booklet\":\n default:\n return openPrintWindow(\n buildPrintDocument(\n renderQuestionBooklet(test, questions, startIndex),\n test.title,\n BOOKLET_VARIANT_STYLES,\n ),\n `${test.title}-questions`,\n );\n }\n },\n};\n\n/** Geometry the bubble form is built on — exported so hosts/tests can assert it. */\nexport const BUBBLE_SHEET_GEOMETRY = {\n rowsPerColumn: BUBBLE_ROWS_PER_COLUMN,\n columnsPerPage: BUBBLE_COLUMNS_PER_PAGE,\n bubbleDiameterInches: 0.16,\n rowPitchInches: 0.28,\n} as const;\n"],"mappings":"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","names":["escapeHtml","escapeHtml","escapeHtml"]}