@ai-matrx/print 0.12.16 → 0.12.18
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/CHANGELOG.md +6 -0
- package/dist/pdf.cjs +9 -0
- package/dist/pdf.cjs.map +1 -1
- package/dist/pdf.js +9 -0
- package/dist/pdf.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.18
|
|
4
|
+
|
|
5
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
|
+
`git diff npm/print/v0.12.17..npm/print/v0.12.18 -- apps/shared/print`).
|
|
7
|
+
No source changes intended and no consumer action required.
|
|
8
|
+
|
|
3
9
|
## 0.12.16
|
|
4
10
|
|
|
5
11
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
package/dist/pdf.cjs
CHANGED
|
@@ -87,6 +87,13 @@ function flattenFlips3d(doc, root) {
|
|
|
87
87
|
stacks.set(parent, list);
|
|
88
88
|
}
|
|
89
89
|
for (const [stack, stackFaces] of stacks) {
|
|
90
|
+
for (const box of [stack, stack.parentElement]) {
|
|
91
|
+
if (!box || box === root) continue;
|
|
92
|
+
const height = win.getComputedStyle(box).height;
|
|
93
|
+
if (height.endsWith("px")) box.style.setProperty("min-height", height);
|
|
94
|
+
box.style.setProperty("height", "auto");
|
|
95
|
+
box.style.setProperty("max-height", "none");
|
|
96
|
+
}
|
|
90
97
|
if (stackFaces.length > 1) {
|
|
91
98
|
stack.style.setProperty("display", "grid");
|
|
92
99
|
stack.style.setProperty("grid-template-columns", `repeat(${stackFaces.length}, minmax(0, 1fr))`);
|
|
@@ -95,6 +102,8 @@ function flattenFlips3d(doc, root) {
|
|
|
95
102
|
for (const face of stackFaces) {
|
|
96
103
|
face.style.setProperty("position", "relative");
|
|
97
104
|
face.style.setProperty("inset", "auto");
|
|
105
|
+
face.style.setProperty("height", "auto");
|
|
106
|
+
face.style.setProperty("overflow", "visible");
|
|
98
107
|
face.style.setProperty("order", facing.get(face) ? "0" : "1");
|
|
99
108
|
}
|
|
100
109
|
}
|
package/dist/pdf.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/pdf.ts","../src/directives.ts","../src/flatten-3d.ts"],"sourcesContent":["/**\n * DOM-Capture Print Utilities — Tier 2 Full-Message Print\n *\n * Captures a rendered DOM element using html2canvas, then pipes it into jsPDF.\n * This handles ALL block types because it screenshots the actual rendered output.\n *\n * Problem: Tailwind CSS 4 uses modern CSS color functions (oklch, lab, color(display-p3))\n * that html2canvas cannot parse. It calls console.error for each one, which gets\n * captured by AdminDebugContextCollector and shows up as false errors.\n *\n * Solution: Patch window.getComputedStyle during the capture so html2canvas always\n * receives safe hex fallbacks instead of unparseable color function strings.\n *\n * ---\n *\n * This module is the packaged union of three source files that always shipped\n * together:\n * • dom-capture-print-utils — the capture/PDF renderers (above),\n * • dom-capture-block-printer — the BlockPrinter factory + captureBlockElement,\n * • markdown-pdf — markdownToPdfBlob.\n *\n * Package-level seams (the only deliberate departures from the originals):\n * • `markdownToPdfBlob` takes its markdown→HTML converter and its stylesheet\n * loader INJECTED (`MarkdownPdfDeps`) instead of dynamically importing app\n * modules. The package cannot reach into an application's feature folders.\n * • `sanitizeForCapture` is a package-added export (it was module-private in\n * the original) so the sanitizer can be tested on its own.\n * • `captureBlockElement` calls `captureToPDF` directly rather than through a\n * dynamic import — html2canvas and jspdf are still lazily imported inside\n * the render functions, which is the laziness that actually matters.\n *\n * html2canvas and jspdf stay behind call-time dynamic imports: importing this\n * module pulls in neither, and the module touches `window`/`document` only when\n * one of its functions is called.\n */\n\nimport { copyImage } from \"@ai-matrx/kit/clipboard\";\nimport type { BlockPrinter } from \"./core\";\nimport type { MarkdownSkin } from \"./markdown\";\nimport { PAGE_BREAK_CLASS } from \"./directives\";\nimport { flattenFlips3d } from \"./flatten-3d\";\n\n/** Type-only view of the lazily imported defaults module. */\ntype MarkdownModule = typeof import(\"./markdown\");\n\nexport interface DomCaptureOptions {\n /** Paper size — 'a4' (default) or 'letter' */\n paperSize?: \"a4\" | \"letter\";\n /** Orientation */\n orientation?: \"portrait\" | \"landscape\";\n /** Scale factor for html2canvas (higher = sharper, slower). Default: 2 */\n scale?: number;\n /** Background colour for the capture. Default: '#ffffff' */\n background?: string;\n /** Filename for the downloaded PDF (without extension). Default: 'ai-response' */\n filename?: string;\n /** Image encoding inside the PDF. PNG is lossless; JPEG is much smaller for reports. */\n imageFormat?: \"png\" | \"jpeg\";\n /** JPEG quality from 0 to 1. Ignored for PNG. Default: 0.92. */\n imageQuality?: number;\n /** Called after each page is captured, with current page / total estimate */\n onProgress?: (page: number, total: number) => void;\n /**\n * Color-fallback theme for unparseable (oklch/lab) computed colors.\n * \"auto\" (default) follows the app's dark class — right for capturing\n * on-screen UI. Pass \"light\" for always-light offscreen document renders\n * so dark-mode fallbacks never bleed into a white-background PDF.\n */\n theme?: \"auto\" | \"light\";\n}\n\nconst PAGE_DIMS = {\n a4: { w: 210, h: 297 },\n letter: { w: 215.9, h: 279.4 },\n} as const;\n\n// Matches any modern CSS color function html2canvas cannot parse\nconst UNSAFE_COLOR_RE = /\\b(oklch|oklab|lab|lch|color)\\s*\\(/i;\n\nfunction isSafeColor(value: string): boolean {\n return !UNSAFE_COLOR_RE.test(value);\n}\n\n/**\n * Replaces modern CSS color functions in a single value string with a\n * context-appropriate hex fallback.\n */\nfunction safenColor(value: string, prop: string, isDark: boolean): string {\n if (isSafeColor(value)) return value;\n const lp = prop.toLowerCase();\n const fallback = lp.includes(\"background\")\n ? isDark ? \"#1f2937\" : \"#ffffff\"\n : lp === \"color\"\n ? isDark ? \"#f3f4f6\" : \"#111827\"\n : lp.includes(\"border\") || lp.includes(\"outline\")\n ? isDark ? \"#4b5563\" : \"#d1d5db\"\n : lp === \"fill\"\n ? isDark ? \"#9ca3af\" : \"#374151\"\n : \"#6b7280\";\n // Replace each colour FUNCTION in place, never the whole value: a gradient\n // (`background-image: linear-gradient(oklch(…), …)`) answered as a bare hex\n // is a value html2canvas cannot parse as an image (\"Unsupported image type 5\"\n // — Full Print died on every answer that carried one, 2026-10-08).\n return value.replace(UNSAFE_COLOR_FN_RE, fallback);\n}\n\n/** One modern colour function call, one level of nested parentheses allowed (`oklch(from var(--x) l c h)`). */\nconst UNSAFE_COLOR_FN_RE = /\\b(?:oklch|oklab|lab|lch|color)\\s*\\((?:[^()]|\\([^()]*\\))*\\)/gi;\n\n/**\n * Patch window.getComputedStyle so that any call during the capture returns\n * safe hex values instead of oklch/lab/etc.\n *\n * html2canvas calls getComputedStyle extensively — this is the authoritative\n * intercept point. Returns a restore function.\n */\nfunction patchGetComputedStyle(isDark: boolean, views: readonly Window[] = [window]): () => void {\n const restores = [...new Set(views)].map((view) => patchOneWindow(view, isDark));\n return () => restores.forEach((restore) => restore());\n}\n\nfunction patchOneWindow(window: Window, isDark: boolean): () => void {\n // Two references on purpose. `original` is the UNBOUND function that was\n // installed before us and is what the restore puts back — restoring a bound\n // wrapper instead would leave one more `.bind` layer installed after every\n // capture, so the real function identity would never come back. `callOriginal`\n // is the bound form we invoke, because getComputedStyle needs `window` as its\n // receiver.\n const original = window.getComputedStyle;\n const callOriginal = original.bind(window);\n\n window.getComputedStyle = function patchedGetComputedStyle(\n elt: Element,\n pseudoElt?: string | null,\n ): CSSStyleDeclaration {\n const computed = callOriginal(elt, pseudoElt);\n\n return new Proxy(computed, {\n get(target, prop) {\n // CSSStyleDeclaration accessors require the native object as their\n // receiver. Using the Proxy as the receiver throws \"Illegal\n // invocation\" while html2canvas clones inline styles.\n const value: unknown = Reflect.get(target, prop, target);\n\n // Intercept string property accesses (color values)\n if (typeof prop === \"string\" && typeof value === \"string\" && value) {\n if (!isSafeColor(value)) {\n return safenColor(value, prop, isDark);\n }\n }\n\n // Intercept getPropertyValue\n if (prop === \"getPropertyValue\") {\n return (propName: string) => {\n const raw = target.getPropertyValue(propName);\n if (raw && !isSafeColor(raw)) {\n return safenColor(raw, propName, isDark);\n }\n return raw;\n };\n }\n\n if (typeof value === \"function\") {\n const fn = value as (...args: unknown[]) => unknown;\n return fn.bind(target);\n }\n\n return value;\n },\n });\n };\n\n return () => {\n window.getComputedStyle = original;\n };\n}\n\n/**\n * Capture a DOM element and export it as a multi-page PDF download.\n */\nexport async function captureToPDF(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n): Promise<void> {\n const pdf = await renderElementToPdf(element, options);\n pdf.save(`${options.filename ?? \"ai-response\"}.pdf`);\n}\n\n/**\n * Capture a DOM element and return the multi-page PDF as a Blob — for\n * callers that upload/persist the PDF instead of downloading it (e.g. the\n * chat \"Save as → PDF Document\" action saving into the user's Files).\n */\nexport async function captureToPDFBlob(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n): Promise<Blob> {\n const pdf = await renderElementToPdf(element, options);\n return pdf.output(\"blob\");\n}\n\n/**\n * Capture deliberately paginated DOM elements as one PDF page each.\n *\n * Use this for designed reports whose page boundaries are part of the layout.\n * The ordinary capture helpers intentionally slice one continuous canvas; this\n * variant preserves explicit report pages and therefore cannot cut a section\n * in half at an arbitrary paper boundary.\n */\nexport async function captureElementsToPDF(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n): Promise<void> {\n const pdf = await renderElementsToPdf(elements, options);\n pdf.save(`${options.filename ?? \"ai-response\"}.pdf`);\n}\n\n/** Blob form of captureElementsToPDF for durable file-system uploads. */\nexport async function captureElementsToPDFBlob(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n): Promise<Blob> {\n const pdf = await renderElementsToPdf(elements, options);\n return pdf.output(\"blob\");\n}\n\nasync function renderElementsToPdf(\n elements: HTMLElement[],\n options: DomCaptureOptions,\n) {\n if (elements.length === 0) {\n throw new Error(\"No report pages are available to export.\");\n }\n\n const {\n paperSize = \"a4\",\n orientation = \"portrait\",\n scale = 2,\n background = \"#ffffff\",\n imageFormat = \"png\",\n imageQuality = 0.92,\n onProgress,\n } = options;\n const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([\n import(\"html2canvas\"),\n import(\"jspdf\"),\n ]);\n const dims = PAGE_DIMS[paperSize];\n const pageW = orientation === \"portrait\" ? dims.w : dims.h;\n const pageH = orientation === \"portrait\" ? dims.h : dims.w;\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n const restoreGetComputedStyle = patchGetComputedStyle(false, [window, ...elements.map((e) => e.ownerDocument.defaultView ?? window)]);\n\n try {\n for (const [index, element] of elements.entries()) {\n onProgress?.(index, elements.length);\n const canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: background,\n logging: false,\n windowWidth: element.scrollWidth,\n windowHeight: element.scrollHeight,\n onclone: (clonedDoc, clonedEl) => flattenFlips3d(clonedDoc, clonedEl),\n });\n if (index > 0) pdf.addPage();\n const isJpeg = imageFormat === \"jpeg\";\n pdf.addImage(\n canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n ),\n isJpeg ? \"JPEG\" : \"PNG\",\n 0,\n 0,\n pageW,\n pageH,\n );\n onProgress?.(index + 1, elements.length);\n }\n } finally {\n restoreGetComputedStyle();\n }\n\n return pdf;\n}\n\nasync function renderElementToPdf(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n) {\n return renderSectionsToPdf([element], options);\n}\n\n/**\n * Capture one or more continuous sections into one PDF. Each section is\n * sliced across as many pages as it needs, and every section after the first\n * STARTS ON A NEW PAGE — that is how a markdown page break survives the\n * canvas lane, where CSS `break-after` means nothing.\n */\nasync function renderSectionsToPdf(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n) {\n const {\n paperSize = \"a4\",\n orientation = \"portrait\",\n scale = 2,\n background = \"#ffffff\",\n imageFormat = \"png\",\n imageQuality = 0.92,\n onProgress,\n } = options;\n\n const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([\n import(\"html2canvas\"),\n import(\"jspdf\"),\n ]);\n\n const dims = PAGE_DIMS[paperSize];\n const pageW = orientation === \"portrait\" ? dims.w : dims.h;\n const pageH = orientation === \"portrait\" ? dims.h : dims.w;\n\n onProgress?.(0, 1);\n\n const isDark =\n options.theme === \"light\"\n ? false\n : document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n const bgColor = background === \"#ffffff\" && isDark ? \"#111827\" : background;\n\n // The element may live in its own frame (a message copied alone for capture): patch that window too.\n const restoreGetComputedStyle = patchGetComputedStyle(isDark, [window, ...elements.map((e) => e.ownerDocument.defaultView ?? window)]);\n\n // ONE PAGE AT A TIME. Each section is drawn in page-tall windows, never as\n // one canvas the height of the whole section: a long answer at scale 2 is a\n // canvas past what a browser will encode, `toDataURL` answers \"data:,\" and\n // jsPDF throws \"Unsupported image type\" — Full Print of a 25-page answer\n // never produced a file (2026-10-08). A window per page also stops every\n // page from embedding the whole section's image.\n const isJpeg = imageFormat === \"jpeg\";\n const imgFormat = isJpeg ? \"JPEG\" : \"PNG\";\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n\n // Half a millimetre of tolerance: a section that is an exact multiple of the\n // page height must not grow a blank trailing page from float rounding.\n const EPSILON_MM = 0.5;\n const sections = elements.map((element) => {\n const cssW = Math.max(1, element.scrollWidth);\n const cssH = Math.max(1, element.scrollHeight);\n const pagePx = (cssW * pageH) / pageW;\n const pages = Math.max(1, Math.ceil(((cssH * pageW) / cssW - EPSILON_MM) / pageH));\n return { element, cssW, cssH, pagePx, pages };\n });\n const totalPages = sections.reduce((a, s) => a + s.pages, 0);\n let page = 0;\n\n try {\n for (const { element, cssW, cssH, pagePx, pages } of sections) {\n const view = element.ownerDocument.defaultView ?? window;\n const box = element.getBoundingClientRect();\n const left = box.left + view.scrollX;\n const top = box.top + view.scrollY;\n for (let index = 0; index < pages; index += 1) {\n const offset = index * pagePx;\n const height = Math.max(1, Math.min(pagePx, cssH - offset));\n const canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: bgColor,\n logging: false,\n windowWidth: cssW,\n windowHeight: cssH,\n x: left,\n y: top + offset,\n width: cssW,\n height,\n onclone: (clonedDoc, clonedEl) => flattenFlips3d(clonedDoc, clonedEl),\n });\n const imgData = canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n );\n if (!imgData.startsWith(\"data:image/\")) {\n throw new Error(`Page ${page + 1} could not be drawn (the browser refused a ${canvas.width}×${canvas.height} image).`);\n }\n if (page > 0) pdf.addPage();\n pdf.addImage(imgData, imgFormat, 0, 0, pageW, (height * pageW) / cssW);\n page += 1;\n onProgress?.(page, totalPages);\n }\n }\n } finally {\n restoreGetComputedStyle();\n }\n\n return pdf;\n}\n\n/**\n * Capture a DOM element and copy it to the clipboard as a PNG, through THE one image door (kit\n * `copyImage`). `true` only when the image is on the clipboard — gate any \"Copied\" UI on it. A\n * refused copy answers `false` and changes nothing (it used to download `capture.png` silently and\n * resolve as if it had copied); the caller says so and may offer a download itself.\n */\nexport async function captureToClipboardImage(\n element: HTMLElement,\n): Promise<boolean> {\n const isDark =\n document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n const canvas = await captureCanvas(element, {\n scale: 2,\n backgroundColor: isDark ? \"#111827\" : \"#ffffff\",\n safeColors: { dark: isDark },\n });\n const blob = await new Promise<Blob | null>((resolve) =>\n canvas.toBlob(resolve, \"image/png\"),\n );\n if (!blob) {\n console.error(\"Failed to copy image: the capture could not be encoded\");\n return false;\n }\n return copyImage(blob);\n}\n\n// ─── THE one DOM-capture + canvas-to-PDF engine (AP-2 engine ruling) ─────────\n// Print sits BELOW alchemy, so the one html2canvas + jsPDF engine lives here and alchemy's\n// `/operate/capture` (`captureElement`, `pagesToPdf`, `elementsToPdf`) loads these lazily.\n\n/** Options for `captureCanvas` — html2canvas's, plus the colour-safety patch. */\nexport type CanvasCaptureOptions = {\n /** Device-pixel scale of the capture (default 2 — legible when scaled in a deck or print). */\n scale?: number;\n /** Canvas background; null keeps transparency. */\n backgroundColor?: string | null;\n useCORS?: boolean;\n allowTaint?: boolean;\n logging?: boolean;\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n scrollX?: number;\n scrollY?: number;\n windowWidth?: number;\n windowHeight?: number;\n ignoreElements?(element: Element): boolean;\n onclone?(document: Document, element: HTMLElement): void | Promise<void>;\n /** Replace unparseable modern colour functions while capturing (`{ dark }` picks the palette; `true` follows the page). */\n safeColors?: boolean | { dark: boolean };\n};\n\nconst prefersDark = () =>\n typeof document !== \"undefined\" &&\n (document.documentElement.classList.contains(\"dark\") ||\n (typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches));\n\n/** Rasterise a live element to a canvas (what the person sees, at `scale`). html2canvas loads on first use. */\nexport async function captureCanvas(\n element: HTMLElement,\n options: CanvasCaptureOptions = {},\n): Promise<HTMLCanvasElement> {\n const { safeColors, ...rest } = options;\n const { default: html2canvas } = await import(\"html2canvas\");\n const restore = safeColors\n ? patchGetComputedStyle(\n typeof safeColors === \"object\" ? safeColors.dark : prefersDark(),\n )\n : () => {};\n try {\n const callerClone = rest.onclone;\n return await html2canvas(element, {\n scale: 2,\n logging: false,\n useCORS: true,\n ...rest,\n onclone: async (clonedDoc: Document, clonedEl: HTMLElement) => {\n flattenFlips3d(clonedDoc, clonedEl);\n await callerClone?.(clonedDoc, clonedEl);\n },\n } as Parameters<typeof html2canvas>[1]);\n } finally {\n restore();\n }\n}\n\nexport type PdfMargins = { top: number; right: number; bottom: number; left: number };\nexport type PdfPage = {\n image: HTMLCanvasElement;\n /** \"JPEG\" (default 0.92 quality) keeps a flat chart an order of magnitude smaller than a PNG embed. */\n imageType?: \"PNG\" | \"JPEG\";\n quality?: number;\n compression?: \"NONE\" | \"FAST\" | \"MEDIUM\" | \"SLOW\";\n /** \"fill\": the image covers the page. Margins: the image is scaled to fit inside them, never cropped. */\n placement?: \"fill\" | PdfMargins;\n /** A line of text drawn on the page (e.g. the as-of date a chart is true of). */\n header?: { text: string; x: number; y: number; fontSize?: number };\n};\nexport type PdfDocumentOptions = {\n orientation?: \"portrait\" | \"landscape\";\n unit?: \"pt\" | \"mm\" | \"cm\" | \"in\" | \"px\";\n format?: string | [number, number];\n};\n\n/** Lay canvases out as PDF pages (one page each) and answer the PDF Blob. jsPDF loads on first use. */\nexport async function pagesToPdf(\n pages: readonly PdfPage[],\n options: PdfDocumentOptions = {},\n): Promise<Blob> {\n const { jsPDF } = await import(\"jspdf\");\n const doc = new jsPDF({\n orientation: options.orientation ?? \"portrait\",\n unit: options.unit ?? \"pt\",\n format: options.format ?? \"a4\",\n });\n pages.forEach((page, index) => {\n if (index > 0) doc.addPage();\n const pageW = doc.internal.pageSize.getWidth();\n const pageH = doc.internal.pageSize.getHeight();\n if (page.header) {\n doc.setFontSize(page.header.fontSize ?? 12);\n doc.text(page.header.text, page.header.x, page.header.y);\n }\n const type = page.imageType ?? \"PNG\";\n const data =\n type === \"JPEG\"\n ? page.image.toDataURL(\"image/jpeg\", page.quality ?? 0.92)\n : page.image.toDataURL(\"image/png\");\n const placement = page.placement ?? \"fill\";\n let x = 0,\n y = 0,\n w = pageW,\n h = pageH;\n if (placement !== \"fill\") {\n const availW = pageW - placement.left - placement.right;\n const availH = pageH - placement.top - placement.bottom;\n const scale = Math.min(availW / page.image.width, availH / page.image.height);\n x = placement.left;\n y = placement.top;\n w = page.image.width * scale;\n h = page.image.height * scale;\n }\n if (page.compression) doc.addImage(data, type, x, y, w, h, undefined, page.compression);\n else doc.addImage(data, type, x, y, w, h);\n });\n return doc.output(\"blob\");\n}\n\n/**\n * DOM-Capture BlockPrinter factory\n *\n * Creates a BlockPrinter that captures a provided DOM element and exports it as PDF.\n * Used by visual blocks (Timeline, ComparisonTable, DecisionTree, Diagram) where\n * the rendered DOM is the most faithful representation.\n *\n * Usage inside a block component:\n * const { captureRef, printAction } = useBlockDomCapture('Timeline');\n * // attach captureRef to the block's root element\n * // add printAction to customActions or controls\n */\n\n/**\n * Create a BlockPrinter backed by DOM capture.\n * The printer's `print(data)` call is a no-op — actual printing is triggered\n * imperatively via `captureElement(el)`.\n *\n * This exists so the block can register itself in the BlockPrinter contract\n * while keeping the DOM reference local to the component.\n */\nexport function createDomCapturePrinter(blockLabel: string): BlockPrinter {\n return {\n label: `Print ${blockLabel}`,\n variants: [],\n // No-op: DOM capture blocks use captureElement() directly\n print: () => {},\n };\n}\n\n/**\n * Capture a DOM element as PDF.\n * Called directly by block components when their print button is clicked.\n *\n * @param element - The DOM element to capture\n * @param filename - Output filename (without extension)\n * @param orientation - \"landscape\" (default) for wide visual blocks; \"portrait\" for tall/list content\n */\nexport async function captureBlockElement(\n element: HTMLElement,\n filename: string,\n orientation: \"landscape\" | \"portrait\" = \"landscape\",\n): Promise<void> {\n await captureToPDF(element, {\n filename,\n scale: 2,\n orientation,\n });\n}\n\n/**\n * markdownToPdfBlob — render a markdown string to a styled, multi-page PDF\n * Blob without any visible UI.\n *\n * Pipeline: markdown → WordPress-styled HTML (the same converter the\n * publish/copy-HTML flows use) → sanitized offscreen DOM host → html2canvas +\n * jsPDF via `captureToPDFBlob`. Everything heavy stays lazy-loaded behind\n * this module.\n *\n * Used by the chat \"Save as → PDF Document\" action, which uploads the blob\n * into the user's Files. Reuse this for any \"content → PDF file\" need — do\n * not fork another markdown-to-pdf path.\n */\n\n/**\n * OVERRIDES for markdownToPdfBlob — every field is optional.\n *\n * Both ports ship a working default in `@ai-matrx/print/markdown`\n * (`markdownToHtml` + `getMarkdownStylesheet`), so `markdownToPdfBlob(md)`\n * alone produces a styled PDF in a fresh app. Supplying a field replaces that\n * default; supplying neither is the normal case. Until 0.3.0 both fields were\n * REQUIRED, which forced every host to build the hard half itself.\n */\nexport interface MarkdownPdfDeps {\n /** markdown → HTML body string. Default: the package's `markdownToHtml`. */\n convertToHtml?: (markdown: string) => string;\n /** the stylesheet the rendered HTML expects. Default: the \"document\" skin. */\n loadCss?: () => Promise<string> | string;\n /** Which shipped look to use when `convertToHtml`/`loadCss` are not given. */\n skin?: MarkdownSkin;\n /** Host brand token overrides appended after the package's default values. */\n tokens?: string;\n /**\n * Where degraded conversion and unreachable images are announced. Nothing\n * fails silently: without a sink these still surface as `console.warn`.\n */\n onNotice?: (notice: MarkdownPdfNotice) => void;\n /**\n * How long to wait for images to decode before capturing, in ms.\n * Default 8000. html2canvas paints whatever has loaded, so an un-awaited\n * image is a blank rectangle in the PDF with no error anywhere.\n */\n imageTimeoutMs?: number;\n}\n\n/** A degradation the PDF lane detected, with the remedy. Never swallowed. */\nexport interface MarkdownPdfNotice {\n code:\n | \"flashcard-malformed\"\n | \"empty-output\"\n | \"html-stripped\"\n | \"image-unreachable\"\n | \"image-timeout\";\n message: string;\n remedy: string;\n}\n\n/**\n * Markdown can carry raw HTML; we are about to attach it to the live\n * document, so strip anything executable. html2canvas ignores scripts, but\n * inline handlers (`onerror`) and javascript: URLs would fire on attach.\n */\nexport function sanitizeForCapture(html: string): DocumentFragment {\n const template = document.createElement(\"template\");\n template.innerHTML = html;\n const fragment = template.content;\n\n fragment\n .querySelectorAll(\"script, iframe, object, embed, link, meta\")\n .forEach((el) => el.remove());\n fragment.querySelectorAll(\"*\").forEach((el) => {\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase();\n if (name.startsWith(\"on\")) el.removeAttribute(attr.name);\n if (\n (name === \"href\" || name === \"src\") &&\n attr.value.trim().toLowerCase().startsWith(\"javascript:\")\n ) {\n el.removeAttribute(attr.name);\n }\n }\n });\n return fragment;\n}\n\n/**\n * Wait for every `<img>` inside a host to finish decoding, and REPORT the ones\n * that never will.\n *\n * html2canvas captures whatever the browser has painted: an image still in\n * flight becomes a blank rectangle and an image that 404s becomes a blank\n * rectangle, both without a single error anywhere. This is the difference\n * between a PDF that is missing a figure and a PDF that is missing a figure\n * AND says so.\n */\nasync function settleImages(\n host: HTMLElement,\n timeoutMs: number,\n notify: (notice: MarkdownPdfNotice) => void,\n): Promise<void> {\n const images = Array.from(host.querySelectorAll(\"img\"));\n if (images.length === 0) return;\n\n const timedOut: string[] = [];\n await Promise.all(\n images.map(\n (img) =>\n new Promise<void>((resolve) => {\n if (img.complete) {\n if (img.naturalWidth === 0) {\n notify({\n code: \"image-unreachable\",\n message: `An image could not be loaded and will be blank in the PDF: ${img.src}`,\n remedy:\n \"Use a durable, publicly reachable https URL — a print render is an unauthenticated document and cannot use session-signed or expiring links.\",\n });\n }\n resolve();\n return;\n }\n const timer = setTimeout(() => {\n timedOut.push(img.src);\n resolve();\n }, timeoutMs);\n const done = (ok: boolean): void => {\n clearTimeout(timer);\n if (!ok) {\n notify({\n code: \"image-unreachable\",\n message: `An image could not be loaded and will be blank in the PDF: ${img.src}`,\n remedy:\n \"Use a durable, publicly reachable https URL — a print render is an unauthenticated document and cannot use session-signed or expiring links.\",\n });\n }\n resolve();\n };\n img.addEventListener(\"load\", () => done(true), { once: true });\n img.addEventListener(\"error\", () => done(false), { once: true });\n }),\n ),\n );\n\n if (timedOut.length > 0) {\n notify({\n code: \"image-timeout\",\n message: `${timedOut.length} image(s) did not finish loading in ${timeoutMs}ms and may be blank in the PDF: ${timedOut.join(\", \")}`,\n remedy:\n \"Raise `imageTimeoutMs`, or serve the images from a faster/CDN origin before exporting.\",\n });\n }\n}\n\nexport async function markdownToPdfBlob(\n markdown: string,\n deps: MarkdownPdfDeps = {},\n): Promise<Blob> {\n if (!markdown.trim()) {\n throw new Error(\"Nothing to convert — the content is empty.\");\n }\n\n // A caller with no sink still gets told. Silence is not an option the API\n // offers, because a silently degraded PDF looks exactly like a correct one.\n const notify = (notice: MarkdownPdfNotice): void => {\n if (deps.onNotice) {\n deps.onNotice(notice);\n return;\n }\n console.warn(`[@ai-matrx/print] ${notice.message} → ${notice.remedy}`);\n };\n\n // The defaults live behind a lazy import: a consumer that injects its own\n // converter and stylesheet never pulls the ~20KB of package CSS into its\n // bundle, and neither does one that imports /pdf for DOM capture only.\n //\n // The specifier is the package's OWN public name, not a relative path: tsup\n // runs with `splitting: false`, so `import(\"./markdown\")` gets inlined into\n // this chunk and the lazy edge silently disappears (measured — dist/pdf.js\n // grew by the full stylesheet). A self-reference is external to the bundle\n // and resolves through the export map at runtime. Same pattern as\n // @ai-matrx/associations `./react/lazy`.\n const needsDefaults = !deps.convertToHtml || !deps.loadCss;\n const markdownModule = needsDefaults\n ? ((await import(\"@ai-matrx/print/markdown\")) as MarkdownModule)\n : undefined;\n\n const bodyHtml = deps.convertToHtml\n ? deps.convertToHtml(markdown)\n : (markdownModule as MarkdownModule).markdownToHtmlForSkin(\n markdown,\n deps.skin ?? \"document\",\n { onNotice: notify },\n );\n\n const css = deps.loadCss\n ? await deps.loadCss()\n : (markdownModule as MarkdownModule).getMarkdownStylesheet({\n skin: deps.skin ?? \"document\",\n ...(deps.tokens === undefined ? {} : { tokens: deps.tokens }),\n });\n\n if (!bodyHtml.trim()) {\n throw new Error(\n \"The markdown converted to an empty document — nothing would be printed. \" +\n \"Check the content is more than whitespace or reasoning blocks.\",\n );\n }\n\n // Offscreen host — must be in the document for html2canvas to compute\n // styles, but never visible and never intercepting input.\n const host = document.createElement(\"div\");\n host.style.cssText =\n \"position:fixed;left:-10000px;top:0;width:800px;background:#ffffff;color:#111827;pointer-events:none;\";\n host.setAttribute(\"aria-hidden\", \"true\");\n\n const style = document.createElement(\"style\");\n style.textContent = css;\n host.appendChild(style);\n\n const contentEl = document.createElement(\"div\");\n // The document stylesheet is class-scoped (.matrx-*) and expects this\n // container; the article sheet styles bare tags under `.content`.\n contentEl.className =\n (deps.skin ?? \"document\") === \"article\" ? \"content\" : \"matrx-content-container\";\n contentEl.style.cssText = \"padding:32px;background:#ffffff;\";\n contentEl.appendChild(sanitizeForCapture(bodyHtml));\n host.appendChild(contentEl);\n\n const hosts = splitHostAtPageBreaks(host, contentEl);\n for (const h of hosts) document.body.appendChild(h);\n try {\n for (const h of hosts) await settleImages(h, deps.imageTimeoutMs ?? 8000, notify);\n // Always-light document render: \"#fff\" (not \"#ffffff\") bypasses the\n // dark-mode background flip, and theme:\"light\" keeps the oklch-fallback\n // patch from substituting dark text colors while the app is in dark mode.\n const pdf = await renderSectionsToPdf(hosts, { background: \"#fff\", theme: \"light\" });\n return pdf.output(\"blob\");\n } catch (error) {\n // The raw html2canvas/jsPDF failure is meaningless to a user and to a host\n // author. Name the stage and the remedy here so no host writes a catch\n // block to reinterpret it (C22).\n throw new Error(\n `The PDF could not be rendered from this content: ${\n error instanceof Error ? error.message : String(error)\n }. Try again; if it repeats, the content likely contains an element the ` +\n \"renderer cannot rasterize (a cross-origin image without CORS headers, \" +\n \"or an embedded iframe/canvas).\",\n error instanceof Error ? { cause: error } : undefined,\n );\n } finally {\n for (const h of hosts) h.remove();\n }\n}\n\n/**\n * Split an offscreen capture host at its top-level page breaks into one host\n * per printed section (same stylesheet, same container shells), so each\n * section starts on a fresh PDF page. No breaks → the host itself.\n */\nfunction splitHostAtPageBreaks(host: HTMLElement, contentEl: HTMLElement): HTMLElement[] {\n const inner = contentEl.firstElementChild;\n const flow =\n inner instanceof HTMLElement && inner.classList.contains(\"matrx-content-container\")\n ? inner\n : contentEl;\n const hasBreak = Array.from(flow.children).some((c) => c.classList.contains(PAGE_BREAK_CLASS));\n if (!hasBreak) return [host];\n\n const style = host.querySelector(\"style\");\n const shell = (): { h: HTMLElement; target: HTMLElement } => {\n const h = host.cloneNode(false) as HTMLElement;\n if (style) h.appendChild(style.cloneNode(true));\n const c = contentEl.cloneNode(false) as HTMLElement;\n h.appendChild(c);\n if (flow === contentEl) return { h, target: c };\n const f = flow.cloneNode(false) as HTMLElement;\n c.appendChild(f);\n return { h, target: f };\n };\n\n const sections: HTMLElement[] = [];\n let current = shell();\n let hasContent = false;\n for (const node of Array.from(flow.childNodes)) {\n if (node instanceof HTMLElement && node.classList.contains(PAGE_BREAK_CLASS)) {\n if (hasContent) {\n sections.push(current.h);\n current = shell();\n hasContent = false;\n }\n continue;\n }\n if (node.nodeType === 1 || (node.textContent ?? \"\").trim()) hasContent = true;\n current.target.appendChild(node);\n }\n if (hasContent || sections.length === 0) sections.push(current.h);\n return sections;\n}\n","/**\n * @ai-matrx/print/directives — the ONE definition of the print directives\n * markdown can carry, shared by every surface that reads markdown.\n *\n * Why a separate, zero-dependency entry: the directive syntax has three\n * readers — this package's two converters (print window / PDF / HTML page),\n * the chat renderer's remark plugin (the on-screen preview), and any editor\n * that inserts one. If each re-derived the regex they would drift the first\n * time someone added an alias. `/markdown` carries ~20KB of stylesheet, so the\n * chat bundle imports the syntax from here instead and pays nothing.\n *\n * ── Page break ────────────────────────────────────────────────────────────\n * A line that is exactly one of these (case-insensitive, surrounding\n * whitespace ignored) forces the next content onto a new printed page:\n *\n * <!-- pagebreak --> ← canonical; invisible in any other renderer\n * <!-- page-break --> <!-- page break --> <!-- newpage -->\n * \\pagebreak \\newpage ← LaTeX / pandoc habit\n * <div style=\"page-break-after: always\"></div> ← Typora / VS Code habit\n * (also `page-break-before`, `break-after: page`, `break-before: page`,\n * and `<div class=\"page-break\"></div>` / `class=\"pagebreak\"`)\n *\n * On screen every surface draws the same idea: a dashed rule labelled\n * \"Page break\" with generous padding. In print it is a zero-height element\n * with `break-after: page`.\n */\n\n/** The form editors insert and docs recommend. */\nexport const PAGE_BREAK_MARKDOWN = \"<!-- pagebreak -->\";\n\n/** The class every surface puts on a rendered page break. */\nexport const PAGE_BREAK_CLASS = \"matrx-page-break\";\n\n/** Screen label for a rendered page break. */\nexport const PAGE_BREAK_LABEL = \"Page break\";\n\nconst COMMENT_FORM = /^<!--\\s*(?:page[\\s_-]?break|new[\\s_-]?page)\\s*-->$/i;\nconst LATEX_FORM = /^\\\\(?:pagebreak|newpage|clearpage)\\s*(?:\\{\\s*\\})?$/i;\nconst DIV_FORM = /^<div\\b([^>]*)>\\s*<\\/div>$|^<div\\b([^>]*)\\/>$/i;\nconst DIV_BREAK_ATTR =\n /(?:page-break-(?:after|before)\\s*:\\s*always|break-(?:after|before)\\s*:\\s*page|class\\s*=\\s*[\"'](?:[^\"']*\\s)?page-?break(?:\\s[^\"']*)?[\"'])/i;\n\n/** Columns of leading indentation (tab = 4, NBSP counts as a space). */\nfunction indentColumns(line: string): number {\n let cols = 0;\n for (const ch of line) {\n if (ch === \" \" || ch === \"\\u00A0\") cols += 1;\n else if (ch === \"\\t\") cols += 4;\n else break;\n }\n return cols;\n}\n\n/**\n * Is this single line a page-break directive? Indented four or more columns\n * it is not — in markdown that is a code block, and every surface must agree.\n */\nexport function isPageBreakLine(line: string): boolean {\n if (indentColumns(line) >= 4) return false;\n const t = line.trim();\n if (t.length < 8 || t.length > 200) return false;\n if (COMMENT_FORM.test(t) || LATEX_FORM.test(t)) return true;\n const div = DIV_FORM.exec(t);\n if (!div) return false;\n return DIV_BREAK_ATTR.test(div[1] ?? div[2] ?? \"\");\n}\n\n// ── Section break ─────────────────────────────────────────────────────────\n//\n// A section break is a page break that can also change the page layout for\n// everything after it (Word's \"Section break (next page)\"):\n//\n// <!-- section --> ← new page, same layout\n// <!-- section: landscape --> ← new page, landscape from here on\n// <!-- section landscape columns=2 --> ← landscape AND two columns\n// <!-- section portrait columns=1 --> ← back to the default layout\n//\n// Recognised words: `landscape` / `portrait`, `columns=N` (or `cols=N`,\n// `N columns`, 1–3). Unknown words are ignored, never an error — the break\n// still happens. Every page-break reader treats a section break as a break.\n\n/** The form editors insert for a plain section break. */\nexport const SECTION_BREAK_MARKDOWN = \"<!-- section -->\";\n\nexport interface SectionBreakDirective {\n /** Absent = inherit the document's orientation. */\n orientation?: \"portrait\" | \"landscape\";\n /** Absent = inherit the document's column count. 1–3. */\n columns?: 1 | 2 | 3;\n}\n\nconst SECTION_FORM = /^<!--\\s*section(?:[\\s_-]?break)?\\b\\s*:?\\s*([^>]*?)\\s*-->$/i;\n\n/** Parse a section-break line; `null` when the line is not one. */\nexport function parseSectionBreakLine(line: string): SectionBreakDirective | null {\n if (indentColumns(line) >= 4) return null;\n const m = SECTION_FORM.exec(line.trim());\n if (!m) return null;\n const words = (m[1] ?? \"\").toLowerCase();\n const out: SectionBreakDirective = {};\n if (/\\blandscape\\b/.test(words)) out.orientation = \"landscape\";\n else if (/\\bportrait\\b/.test(words)) out.orientation = \"portrait\";\n const cols = /\\b(?:columns?|cols?)\\s*[=:]\\s*(\\d)\\b/.exec(words) ?? /\\b(\\d)\\s*[-\\s]?columns?\\b/.exec(words);\n if (cols) {\n const n = Math.min(3, Math.max(1, Number(cols[1])));\n out.columns = n as 1 | 2 | 3;\n }\n return out;\n}\n\n// ── Table of contents / bibliography placement ───────────────────────────\n//\n// `[[toc]]`, `[toc]`, `[TOC]`, `${toc}`, `<!-- toc -->`, `\\tableofcontents`\n// put the table of contents at that spot (otherwise frontmatter `toc: true`\n// puts it at the start). `<!-- bibliography -->`, `[[bibliography]]`,\n// `\\printbibliography` put the reference list there (otherwise it closes the\n// document whenever something is cited).\n\nexport const TOC_MARKDOWN = \"[[toc]]\";\nexport const BIBLIOGRAPHY_MARKDOWN = \"<!-- bibliography -->\";\n\nconst TOC_FORM = /^(?:\\[\\[\\s*toc\\s*\\]\\]|\\[toc\\]|\\$\\{toc\\}|<!--\\s*toc\\s*-->|\\\\tableofcontents)$/i;\nconst BIBLIOGRAPHY_FORM =\n /^(?:\\[\\[\\s*(?:bibliography|references)\\s*\\]\\]|<!--\\s*(?:bibliography|references)\\s*-->|\\\\printbibliography|\\\\bibliography\\{[^}]*\\})$/i;\n\nexport function isTocLine(line: string): boolean {\n return indentColumns(line) < 4 && TOC_FORM.test(line.trim());\n}\n\nexport function isBibliographyLine(line: string): boolean {\n return indentColumns(line) < 4 && BIBLIOGRAPHY_FORM.test(line.trim());\n}\n\n/** Any directive that ends the current printed page (page or section break). */\nexport function isBreakLine(line: string): boolean {\n return isPageBreakLine(line) || parseSectionBreakLine(line) !== null;\n}\n\n/**\n * Split markdown into the pieces that belong on separate pages. Fence-aware:\n * a directive inside a ``` / ~~~ code block is code, not a break. Always\n * returns at least one piece; empty pieces (two breaks in a row, a break at\n * the very start or end) are kept out so no blank page is produced.\n */\nexport function splitAtPageBreaks(markdown: string): string[] {\n const lines = markdown.split(/\\r?\\n/);\n const pieces: string[] = [];\n let current: string[] = [];\n let fence: string | null = null;\n\n for (const line of lines) {\n // Any indent — the converters accept list-nested fences, so must this.\n const fenceMatch = /^\\s*(`{3,}|~{3,})/.exec(line);\n if (fenceMatch) {\n const marker = fenceMatch[1] as string;\n if (fence === null) fence = marker;\n else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;\n } else if (fence === null && isBreakLine(line)) {\n if (current.join(\"\\n\").trim()) pieces.push(current.join(\"\\n\"));\n current = [];\n continue;\n }\n current.push(line);\n }\n if (current.join(\"\\n\").trim() || pieces.length === 0) pieces.push(current.join(\"\\n\"));\n return pieces;\n}\n","/**\n * Flatten CSS 3D flips for a canvas capture.\n *\n * html2canvas has no 3D: it ignores `backface-visibility` and `transform-style:\n * preserve-3d`, so a flip card (two faces stacked, the back turned with\n * `rotateY(180deg)`) is drawn with BOTH faces on top of each other and the back\n * face mirrored. This runs on the capture's cloned document and turns every such\n * stack into a flat row: the faces sit side by side, the face turned toward the\n * viewer first, every face un-mirrored and shown — so a printed card carries its\n * answer as well as its question. Plain 2D transforms are never touched.\n */\n\nconst IDENTITY = \"none\";\n\ntype FrameWindow = Window & typeof globalThis;\n\nfunction matrixOf(win: FrameWindow, cs: CSSStyleDeclaration): DOMMatrix {\n const t = cs.transform;\n if (!t || t === IDENTITY) return new win.DOMMatrix();\n try {\n return new win.DOMMatrix(t);\n } catch {\n return new win.DOMMatrix();\n }\n}\n\n/** True when the face, after every ancestor transform up to `root`, points toward the viewer. */\nfunction facesViewer(win: FrameWindow, face: HTMLElement, root: HTMLElement): boolean {\n let m = new win.DOMMatrix();\n for (let node: HTMLElement | null = face; node; node = node.parentElement) {\n m = matrixOf(win, win.getComputedStyle(node)).multiply(m);\n if (node === root) break;\n }\n // m33 is the z-component of the face normal after rotation: > 0 faces the viewer.\n return m.m33 >= 0;\n}\n\nfunction is3d(cs: CSSStyleDeclaration): boolean {\n return (\n cs.transformStyle === \"preserve-3d\" ||\n cs.backfaceVisibility === \"hidden\" ||\n cs.perspective !== \"none\" ||\n (cs.transform ?? \"\").startsWith(\"matrix3d\")\n );\n}\n\n/** Make every 3D flip in `root` html2canvas-safe. Idempotent; a no-op when there is no 3D. */\nexport function flattenFlips3d(doc: Document, root: HTMLElement): void {\n const win = doc.defaultView as FrameWindow | null;\n if (!win) return;\n const all = [root, ...Array.from(root.querySelectorAll<HTMLElement>(\"*\"))];\n const faces = all.filter((el) => win.getComputedStyle(el).backfaceVisibility === \"hidden\");\n if (faces.length === 0 && !all.some((el) => is3d(win.getComputedStyle(el)))) return;\n\n // Decide every face's facing BEFORE any style changes (it reads ancestor transforms).\n const facing = new Map<HTMLElement, boolean>();\n for (const face of faces) facing.set(face, facesViewer(win, face, root));\n\n // Faces grouped by the element that stacks them: that element becomes a flat row.\n const stacks = new Map<HTMLElement, HTMLElement[]>();\n for (const face of faces) {\n const parent = face.parentElement;\n if (!parent) continue;\n const list = stacks.get(parent) ?? [];\n list.push(face);\n stacks.set(parent, list);\n }\n\n for (const [stack, stackFaces] of stacks) {\n if (stackFaces.length > 1) {\n stack.style.setProperty(\"display\", \"grid\");\n stack.style.setProperty(\"grid-template-columns\", `repeat(${stackFaces.length}, minmax(0, 1fr))`);\n stack.style.setProperty(\"gap\", \"8px\");\n }\n for (const face of stackFaces) {\n face.style.setProperty(\"position\", \"relative\");\n face.style.setProperty(\"inset\", \"auto\");\n face.style.setProperty(\"order\", facing.get(face) ? \"0\" : \"1\");\n }\n }\n\n for (const el of all) {\n const cs = win.getComputedStyle(el);\n if (!is3d(cs)) continue;\n el.style.setProperty(\"transform\", \"none\");\n el.style.setProperty(\"transform-style\", \"flat\");\n el.style.setProperty(\"perspective\", \"none\");\n el.style.setProperty(\"backface-visibility\", \"visible\");\n }\n}\n"],"mappings":"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","names":["window"]}
|
|
1
|
+
{"version":3,"sources":["../src/pdf.ts","../src/directives.ts","../src/flatten-3d.ts"],"sourcesContent":["/**\n * DOM-Capture Print Utilities — Tier 2 Full-Message Print\n *\n * Captures a rendered DOM element using html2canvas, then pipes it into jsPDF.\n * This handles ALL block types because it screenshots the actual rendered output.\n *\n * Problem: Tailwind CSS 4 uses modern CSS color functions (oklch, lab, color(display-p3))\n * that html2canvas cannot parse. It calls console.error for each one, which gets\n * captured by AdminDebugContextCollector and shows up as false errors.\n *\n * Solution: Patch window.getComputedStyle during the capture so html2canvas always\n * receives safe hex fallbacks instead of unparseable color function strings.\n *\n * ---\n *\n * This module is the packaged union of three source files that always shipped\n * together:\n * • dom-capture-print-utils — the capture/PDF renderers (above),\n * • dom-capture-block-printer — the BlockPrinter factory + captureBlockElement,\n * • markdown-pdf — markdownToPdfBlob.\n *\n * Package-level seams (the only deliberate departures from the originals):\n * • `markdownToPdfBlob` takes its markdown→HTML converter and its stylesheet\n * loader INJECTED (`MarkdownPdfDeps`) instead of dynamically importing app\n * modules. The package cannot reach into an application's feature folders.\n * • `sanitizeForCapture` is a package-added export (it was module-private in\n * the original) so the sanitizer can be tested on its own.\n * • `captureBlockElement` calls `captureToPDF` directly rather than through a\n * dynamic import — html2canvas and jspdf are still lazily imported inside\n * the render functions, which is the laziness that actually matters.\n *\n * html2canvas and jspdf stay behind call-time dynamic imports: importing this\n * module pulls in neither, and the module touches `window`/`document` only when\n * one of its functions is called.\n */\n\nimport { copyImage } from \"@ai-matrx/kit/clipboard\";\nimport type { BlockPrinter } from \"./core\";\nimport type { MarkdownSkin } from \"./markdown\";\nimport { PAGE_BREAK_CLASS } from \"./directives\";\nimport { flattenFlips3d } from \"./flatten-3d\";\n\n/** Type-only view of the lazily imported defaults module. */\ntype MarkdownModule = typeof import(\"./markdown\");\n\nexport interface DomCaptureOptions {\n /** Paper size — 'a4' (default) or 'letter' */\n paperSize?: \"a4\" | \"letter\";\n /** Orientation */\n orientation?: \"portrait\" | \"landscape\";\n /** Scale factor for html2canvas (higher = sharper, slower). Default: 2 */\n scale?: number;\n /** Background colour for the capture. Default: '#ffffff' */\n background?: string;\n /** Filename for the downloaded PDF (without extension). Default: 'ai-response' */\n filename?: string;\n /** Image encoding inside the PDF. PNG is lossless; JPEG is much smaller for reports. */\n imageFormat?: \"png\" | \"jpeg\";\n /** JPEG quality from 0 to 1. Ignored for PNG. Default: 0.92. */\n imageQuality?: number;\n /** Called after each page is captured, with current page / total estimate */\n onProgress?: (page: number, total: number) => void;\n /**\n * Color-fallback theme for unparseable (oklch/lab) computed colors.\n * \"auto\" (default) follows the app's dark class — right for capturing\n * on-screen UI. Pass \"light\" for always-light offscreen document renders\n * so dark-mode fallbacks never bleed into a white-background PDF.\n */\n theme?: \"auto\" | \"light\";\n}\n\nconst PAGE_DIMS = {\n a4: { w: 210, h: 297 },\n letter: { w: 215.9, h: 279.4 },\n} as const;\n\n// Matches any modern CSS color function html2canvas cannot parse\nconst UNSAFE_COLOR_RE = /\\b(oklch|oklab|lab|lch|color)\\s*\\(/i;\n\nfunction isSafeColor(value: string): boolean {\n return !UNSAFE_COLOR_RE.test(value);\n}\n\n/**\n * Replaces modern CSS color functions in a single value string with a\n * context-appropriate hex fallback.\n */\nfunction safenColor(value: string, prop: string, isDark: boolean): string {\n if (isSafeColor(value)) return value;\n const lp = prop.toLowerCase();\n const fallback = lp.includes(\"background\")\n ? isDark ? \"#1f2937\" : \"#ffffff\"\n : lp === \"color\"\n ? isDark ? \"#f3f4f6\" : \"#111827\"\n : lp.includes(\"border\") || lp.includes(\"outline\")\n ? isDark ? \"#4b5563\" : \"#d1d5db\"\n : lp === \"fill\"\n ? isDark ? \"#9ca3af\" : \"#374151\"\n : \"#6b7280\";\n // Replace each colour FUNCTION in place, never the whole value: a gradient\n // (`background-image: linear-gradient(oklch(…), …)`) answered as a bare hex\n // is a value html2canvas cannot parse as an image (\"Unsupported image type 5\"\n // — Full Print died on every answer that carried one, 2026-10-08).\n return value.replace(UNSAFE_COLOR_FN_RE, fallback);\n}\n\n/** One modern colour function call, one level of nested parentheses allowed (`oklch(from var(--x) l c h)`). */\nconst UNSAFE_COLOR_FN_RE = /\\b(?:oklch|oklab|lab|lch|color)\\s*\\((?:[^()]|\\([^()]*\\))*\\)/gi;\n\n/**\n * Patch window.getComputedStyle so that any call during the capture returns\n * safe hex values instead of oklch/lab/etc.\n *\n * html2canvas calls getComputedStyle extensively — this is the authoritative\n * intercept point. Returns a restore function.\n */\nfunction patchGetComputedStyle(isDark: boolean, views: readonly Window[] = [window]): () => void {\n const restores = [...new Set(views)].map((view) => patchOneWindow(view, isDark));\n return () => restores.forEach((restore) => restore());\n}\n\nfunction patchOneWindow(window: Window, isDark: boolean): () => void {\n // Two references on purpose. `original` is the UNBOUND function that was\n // installed before us and is what the restore puts back — restoring a bound\n // wrapper instead would leave one more `.bind` layer installed after every\n // capture, so the real function identity would never come back. `callOriginal`\n // is the bound form we invoke, because getComputedStyle needs `window` as its\n // receiver.\n const original = window.getComputedStyle;\n const callOriginal = original.bind(window);\n\n window.getComputedStyle = function patchedGetComputedStyle(\n elt: Element,\n pseudoElt?: string | null,\n ): CSSStyleDeclaration {\n const computed = callOriginal(elt, pseudoElt);\n\n return new Proxy(computed, {\n get(target, prop) {\n // CSSStyleDeclaration accessors require the native object as their\n // receiver. Using the Proxy as the receiver throws \"Illegal\n // invocation\" while html2canvas clones inline styles.\n const value: unknown = Reflect.get(target, prop, target);\n\n // Intercept string property accesses (color values)\n if (typeof prop === \"string\" && typeof value === \"string\" && value) {\n if (!isSafeColor(value)) {\n return safenColor(value, prop, isDark);\n }\n }\n\n // Intercept getPropertyValue\n if (prop === \"getPropertyValue\") {\n return (propName: string) => {\n const raw = target.getPropertyValue(propName);\n if (raw && !isSafeColor(raw)) {\n return safenColor(raw, propName, isDark);\n }\n return raw;\n };\n }\n\n if (typeof value === \"function\") {\n const fn = value as (...args: unknown[]) => unknown;\n return fn.bind(target);\n }\n\n return value;\n },\n });\n };\n\n return () => {\n window.getComputedStyle = original;\n };\n}\n\n/**\n * Capture a DOM element and export it as a multi-page PDF download.\n */\nexport async function captureToPDF(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n): Promise<void> {\n const pdf = await renderElementToPdf(element, options);\n pdf.save(`${options.filename ?? \"ai-response\"}.pdf`);\n}\n\n/**\n * Capture a DOM element and return the multi-page PDF as a Blob — for\n * callers that upload/persist the PDF instead of downloading it (e.g. the\n * chat \"Save as → PDF Document\" action saving into the user's Files).\n */\nexport async function captureToPDFBlob(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n): Promise<Blob> {\n const pdf = await renderElementToPdf(element, options);\n return pdf.output(\"blob\");\n}\n\n/**\n * Capture deliberately paginated DOM elements as one PDF page each.\n *\n * Use this for designed reports whose page boundaries are part of the layout.\n * The ordinary capture helpers intentionally slice one continuous canvas; this\n * variant preserves explicit report pages and therefore cannot cut a section\n * in half at an arbitrary paper boundary.\n */\nexport async function captureElementsToPDF(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n): Promise<void> {\n const pdf = await renderElementsToPdf(elements, options);\n pdf.save(`${options.filename ?? \"ai-response\"}.pdf`);\n}\n\n/** Blob form of captureElementsToPDF for durable file-system uploads. */\nexport async function captureElementsToPDFBlob(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n): Promise<Blob> {\n const pdf = await renderElementsToPdf(elements, options);\n return pdf.output(\"blob\");\n}\n\nasync function renderElementsToPdf(\n elements: HTMLElement[],\n options: DomCaptureOptions,\n) {\n if (elements.length === 0) {\n throw new Error(\"No report pages are available to export.\");\n }\n\n const {\n paperSize = \"a4\",\n orientation = \"portrait\",\n scale = 2,\n background = \"#ffffff\",\n imageFormat = \"png\",\n imageQuality = 0.92,\n onProgress,\n } = options;\n const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([\n import(\"html2canvas\"),\n import(\"jspdf\"),\n ]);\n const dims = PAGE_DIMS[paperSize];\n const pageW = orientation === \"portrait\" ? dims.w : dims.h;\n const pageH = orientation === \"portrait\" ? dims.h : dims.w;\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n const restoreGetComputedStyle = patchGetComputedStyle(false, [window, ...elements.map((e) => e.ownerDocument.defaultView ?? window)]);\n\n try {\n for (const [index, element] of elements.entries()) {\n onProgress?.(index, elements.length);\n const canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: background,\n logging: false,\n windowWidth: element.scrollWidth,\n windowHeight: element.scrollHeight,\n onclone: (clonedDoc, clonedEl) => flattenFlips3d(clonedDoc, clonedEl),\n });\n if (index > 0) pdf.addPage();\n const isJpeg = imageFormat === \"jpeg\";\n pdf.addImage(\n canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n ),\n isJpeg ? \"JPEG\" : \"PNG\",\n 0,\n 0,\n pageW,\n pageH,\n );\n onProgress?.(index + 1, elements.length);\n }\n } finally {\n restoreGetComputedStyle();\n }\n\n return pdf;\n}\n\nasync function renderElementToPdf(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n) {\n return renderSectionsToPdf([element], options);\n}\n\n/**\n * Capture one or more continuous sections into one PDF. Each section is\n * sliced across as many pages as it needs, and every section after the first\n * STARTS ON A NEW PAGE — that is how a markdown page break survives the\n * canvas lane, where CSS `break-after` means nothing.\n */\nasync function renderSectionsToPdf(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n) {\n const {\n paperSize = \"a4\",\n orientation = \"portrait\",\n scale = 2,\n background = \"#ffffff\",\n imageFormat = \"png\",\n imageQuality = 0.92,\n onProgress,\n } = options;\n\n const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([\n import(\"html2canvas\"),\n import(\"jspdf\"),\n ]);\n\n const dims = PAGE_DIMS[paperSize];\n const pageW = orientation === \"portrait\" ? dims.w : dims.h;\n const pageH = orientation === \"portrait\" ? dims.h : dims.w;\n\n onProgress?.(0, 1);\n\n const isDark =\n options.theme === \"light\"\n ? false\n : document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n const bgColor = background === \"#ffffff\" && isDark ? \"#111827\" : background;\n\n // The element may live in its own frame (a message copied alone for capture): patch that window too.\n const restoreGetComputedStyle = patchGetComputedStyle(isDark, [window, ...elements.map((e) => e.ownerDocument.defaultView ?? window)]);\n\n // ONE PAGE AT A TIME. Each section is drawn in page-tall windows, never as\n // one canvas the height of the whole section: a long answer at scale 2 is a\n // canvas past what a browser will encode, `toDataURL` answers \"data:,\" and\n // jsPDF throws \"Unsupported image type\" — Full Print of a 25-page answer\n // never produced a file (2026-10-08). A window per page also stops every\n // page from embedding the whole section's image.\n const isJpeg = imageFormat === \"jpeg\";\n const imgFormat = isJpeg ? \"JPEG\" : \"PNG\";\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n\n // Half a millimetre of tolerance: a section that is an exact multiple of the\n // page height must not grow a blank trailing page from float rounding.\n const EPSILON_MM = 0.5;\n const sections = elements.map((element) => {\n const cssW = Math.max(1, element.scrollWidth);\n const cssH = Math.max(1, element.scrollHeight);\n const pagePx = (cssW * pageH) / pageW;\n const pages = Math.max(1, Math.ceil(((cssH * pageW) / cssW - EPSILON_MM) / pageH));\n return { element, cssW, cssH, pagePx, pages };\n });\n const totalPages = sections.reduce((a, s) => a + s.pages, 0);\n let page = 0;\n\n try {\n for (const { element, cssW, cssH, pagePx, pages } of sections) {\n const view = element.ownerDocument.defaultView ?? window;\n const box = element.getBoundingClientRect();\n const left = box.left + view.scrollX;\n const top = box.top + view.scrollY;\n for (let index = 0; index < pages; index += 1) {\n const offset = index * pagePx;\n const height = Math.max(1, Math.min(pagePx, cssH - offset));\n const canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: bgColor,\n logging: false,\n windowWidth: cssW,\n windowHeight: cssH,\n x: left,\n y: top + offset,\n width: cssW,\n height,\n onclone: (clonedDoc, clonedEl) => flattenFlips3d(clonedDoc, clonedEl),\n });\n const imgData = canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n );\n if (!imgData.startsWith(\"data:image/\")) {\n throw new Error(`Page ${page + 1} could not be drawn (the browser refused a ${canvas.width}×${canvas.height} image).`);\n }\n if (page > 0) pdf.addPage();\n pdf.addImage(imgData, imgFormat, 0, 0, pageW, (height * pageW) / cssW);\n page += 1;\n onProgress?.(page, totalPages);\n }\n }\n } finally {\n restoreGetComputedStyle();\n }\n\n return pdf;\n}\n\n/**\n * Capture a DOM element and copy it to the clipboard as a PNG, through THE one image door (kit\n * `copyImage`). `true` only when the image is on the clipboard — gate any \"Copied\" UI on it. A\n * refused copy answers `false` and changes nothing (it used to download `capture.png` silently and\n * resolve as if it had copied); the caller says so and may offer a download itself.\n */\nexport async function captureToClipboardImage(\n element: HTMLElement,\n): Promise<boolean> {\n const isDark =\n document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n const canvas = await captureCanvas(element, {\n scale: 2,\n backgroundColor: isDark ? \"#111827\" : \"#ffffff\",\n safeColors: { dark: isDark },\n });\n const blob = await new Promise<Blob | null>((resolve) =>\n canvas.toBlob(resolve, \"image/png\"),\n );\n if (!blob) {\n console.error(\"Failed to copy image: the capture could not be encoded\");\n return false;\n }\n return copyImage(blob);\n}\n\n// ─── THE one DOM-capture + canvas-to-PDF engine (AP-2 engine ruling) ─────────\n// Print sits BELOW alchemy, so the one html2canvas + jsPDF engine lives here and alchemy's\n// `/operate/capture` (`captureElement`, `pagesToPdf`, `elementsToPdf`) loads these lazily.\n\n/** Options for `captureCanvas` — html2canvas's, plus the colour-safety patch. */\nexport type CanvasCaptureOptions = {\n /** Device-pixel scale of the capture (default 2 — legible when scaled in a deck or print). */\n scale?: number;\n /** Canvas background; null keeps transparency. */\n backgroundColor?: string | null;\n useCORS?: boolean;\n allowTaint?: boolean;\n logging?: boolean;\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n scrollX?: number;\n scrollY?: number;\n windowWidth?: number;\n windowHeight?: number;\n ignoreElements?(element: Element): boolean;\n onclone?(document: Document, element: HTMLElement): void | Promise<void>;\n /** Replace unparseable modern colour functions while capturing (`{ dark }` picks the palette; `true` follows the page). */\n safeColors?: boolean | { dark: boolean };\n};\n\nconst prefersDark = () =>\n typeof document !== \"undefined\" &&\n (document.documentElement.classList.contains(\"dark\") ||\n (typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches));\n\n/** Rasterise a live element to a canvas (what the person sees, at `scale`). html2canvas loads on first use. */\nexport async function captureCanvas(\n element: HTMLElement,\n options: CanvasCaptureOptions = {},\n): Promise<HTMLCanvasElement> {\n const { safeColors, ...rest } = options;\n const { default: html2canvas } = await import(\"html2canvas\");\n const restore = safeColors\n ? patchGetComputedStyle(\n typeof safeColors === \"object\" ? safeColors.dark : prefersDark(),\n )\n : () => {};\n try {\n const callerClone = rest.onclone;\n return await html2canvas(element, {\n scale: 2,\n logging: false,\n useCORS: true,\n ...rest,\n onclone: async (clonedDoc: Document, clonedEl: HTMLElement) => {\n flattenFlips3d(clonedDoc, clonedEl);\n await callerClone?.(clonedDoc, clonedEl);\n },\n } as Parameters<typeof html2canvas>[1]);\n } finally {\n restore();\n }\n}\n\nexport type PdfMargins = { top: number; right: number; bottom: number; left: number };\nexport type PdfPage = {\n image: HTMLCanvasElement;\n /** \"JPEG\" (default 0.92 quality) keeps a flat chart an order of magnitude smaller than a PNG embed. */\n imageType?: \"PNG\" | \"JPEG\";\n quality?: number;\n compression?: \"NONE\" | \"FAST\" | \"MEDIUM\" | \"SLOW\";\n /** \"fill\": the image covers the page. Margins: the image is scaled to fit inside them, never cropped. */\n placement?: \"fill\" | PdfMargins;\n /** A line of text drawn on the page (e.g. the as-of date a chart is true of). */\n header?: { text: string; x: number; y: number; fontSize?: number };\n};\nexport type PdfDocumentOptions = {\n orientation?: \"portrait\" | \"landscape\";\n unit?: \"pt\" | \"mm\" | \"cm\" | \"in\" | \"px\";\n format?: string | [number, number];\n};\n\n/** Lay canvases out as PDF pages (one page each) and answer the PDF Blob. jsPDF loads on first use. */\nexport async function pagesToPdf(\n pages: readonly PdfPage[],\n options: PdfDocumentOptions = {},\n): Promise<Blob> {\n const { jsPDF } = await import(\"jspdf\");\n const doc = new jsPDF({\n orientation: options.orientation ?? \"portrait\",\n unit: options.unit ?? \"pt\",\n format: options.format ?? \"a4\",\n });\n pages.forEach((page, index) => {\n if (index > 0) doc.addPage();\n const pageW = doc.internal.pageSize.getWidth();\n const pageH = doc.internal.pageSize.getHeight();\n if (page.header) {\n doc.setFontSize(page.header.fontSize ?? 12);\n doc.text(page.header.text, page.header.x, page.header.y);\n }\n const type = page.imageType ?? \"PNG\";\n const data =\n type === \"JPEG\"\n ? page.image.toDataURL(\"image/jpeg\", page.quality ?? 0.92)\n : page.image.toDataURL(\"image/png\");\n const placement = page.placement ?? \"fill\";\n let x = 0,\n y = 0,\n w = pageW,\n h = pageH;\n if (placement !== \"fill\") {\n const availW = pageW - placement.left - placement.right;\n const availH = pageH - placement.top - placement.bottom;\n const scale = Math.min(availW / page.image.width, availH / page.image.height);\n x = placement.left;\n y = placement.top;\n w = page.image.width * scale;\n h = page.image.height * scale;\n }\n if (page.compression) doc.addImage(data, type, x, y, w, h, undefined, page.compression);\n else doc.addImage(data, type, x, y, w, h);\n });\n return doc.output(\"blob\");\n}\n\n/**\n * DOM-Capture BlockPrinter factory\n *\n * Creates a BlockPrinter that captures a provided DOM element and exports it as PDF.\n * Used by visual blocks (Timeline, ComparisonTable, DecisionTree, Diagram) where\n * the rendered DOM is the most faithful representation.\n *\n * Usage inside a block component:\n * const { captureRef, printAction } = useBlockDomCapture('Timeline');\n * // attach captureRef to the block's root element\n * // add printAction to customActions or controls\n */\n\n/**\n * Create a BlockPrinter backed by DOM capture.\n * The printer's `print(data)` call is a no-op — actual printing is triggered\n * imperatively via `captureElement(el)`.\n *\n * This exists so the block can register itself in the BlockPrinter contract\n * while keeping the DOM reference local to the component.\n */\nexport function createDomCapturePrinter(blockLabel: string): BlockPrinter {\n return {\n label: `Print ${blockLabel}`,\n variants: [],\n // No-op: DOM capture blocks use captureElement() directly\n print: () => {},\n };\n}\n\n/**\n * Capture a DOM element as PDF.\n * Called directly by block components when their print button is clicked.\n *\n * @param element - The DOM element to capture\n * @param filename - Output filename (without extension)\n * @param orientation - \"landscape\" (default) for wide visual blocks; \"portrait\" for tall/list content\n */\nexport async function captureBlockElement(\n element: HTMLElement,\n filename: string,\n orientation: \"landscape\" | \"portrait\" = \"landscape\",\n): Promise<void> {\n await captureToPDF(element, {\n filename,\n scale: 2,\n orientation,\n });\n}\n\n/**\n * markdownToPdfBlob — render a markdown string to a styled, multi-page PDF\n * Blob without any visible UI.\n *\n * Pipeline: markdown → WordPress-styled HTML (the same converter the\n * publish/copy-HTML flows use) → sanitized offscreen DOM host → html2canvas +\n * jsPDF via `captureToPDFBlob`. Everything heavy stays lazy-loaded behind\n * this module.\n *\n * Used by the chat \"Save as → PDF Document\" action, which uploads the blob\n * into the user's Files. Reuse this for any \"content → PDF file\" need — do\n * not fork another markdown-to-pdf path.\n */\n\n/**\n * OVERRIDES for markdownToPdfBlob — every field is optional.\n *\n * Both ports ship a working default in `@ai-matrx/print/markdown`\n * (`markdownToHtml` + `getMarkdownStylesheet`), so `markdownToPdfBlob(md)`\n * alone produces a styled PDF in a fresh app. Supplying a field replaces that\n * default; supplying neither is the normal case. Until 0.3.0 both fields were\n * REQUIRED, which forced every host to build the hard half itself.\n */\nexport interface MarkdownPdfDeps {\n /** markdown → HTML body string. Default: the package's `markdownToHtml`. */\n convertToHtml?: (markdown: string) => string;\n /** the stylesheet the rendered HTML expects. Default: the \"document\" skin. */\n loadCss?: () => Promise<string> | string;\n /** Which shipped look to use when `convertToHtml`/`loadCss` are not given. */\n skin?: MarkdownSkin;\n /** Host brand token overrides appended after the package's default values. */\n tokens?: string;\n /**\n * Where degraded conversion and unreachable images are announced. Nothing\n * fails silently: without a sink these still surface as `console.warn`.\n */\n onNotice?: (notice: MarkdownPdfNotice) => void;\n /**\n * How long to wait for images to decode before capturing, in ms.\n * Default 8000. html2canvas paints whatever has loaded, so an un-awaited\n * image is a blank rectangle in the PDF with no error anywhere.\n */\n imageTimeoutMs?: number;\n}\n\n/** A degradation the PDF lane detected, with the remedy. Never swallowed. */\nexport interface MarkdownPdfNotice {\n code:\n | \"flashcard-malformed\"\n | \"empty-output\"\n | \"html-stripped\"\n | \"image-unreachable\"\n | \"image-timeout\";\n message: string;\n remedy: string;\n}\n\n/**\n * Markdown can carry raw HTML; we are about to attach it to the live\n * document, so strip anything executable. html2canvas ignores scripts, but\n * inline handlers (`onerror`) and javascript: URLs would fire on attach.\n */\nexport function sanitizeForCapture(html: string): DocumentFragment {\n const template = document.createElement(\"template\");\n template.innerHTML = html;\n const fragment = template.content;\n\n fragment\n .querySelectorAll(\"script, iframe, object, embed, link, meta\")\n .forEach((el) => el.remove());\n fragment.querySelectorAll(\"*\").forEach((el) => {\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase();\n if (name.startsWith(\"on\")) el.removeAttribute(attr.name);\n if (\n (name === \"href\" || name === \"src\") &&\n attr.value.trim().toLowerCase().startsWith(\"javascript:\")\n ) {\n el.removeAttribute(attr.name);\n }\n }\n });\n return fragment;\n}\n\n/**\n * Wait for every `<img>` inside a host to finish decoding, and REPORT the ones\n * that never will.\n *\n * html2canvas captures whatever the browser has painted: an image still in\n * flight becomes a blank rectangle and an image that 404s becomes a blank\n * rectangle, both without a single error anywhere. This is the difference\n * between a PDF that is missing a figure and a PDF that is missing a figure\n * AND says so.\n */\nasync function settleImages(\n host: HTMLElement,\n timeoutMs: number,\n notify: (notice: MarkdownPdfNotice) => void,\n): Promise<void> {\n const images = Array.from(host.querySelectorAll(\"img\"));\n if (images.length === 0) return;\n\n const timedOut: string[] = [];\n await Promise.all(\n images.map(\n (img) =>\n new Promise<void>((resolve) => {\n if (img.complete) {\n if (img.naturalWidth === 0) {\n notify({\n code: \"image-unreachable\",\n message: `An image could not be loaded and will be blank in the PDF: ${img.src}`,\n remedy:\n \"Use a durable, publicly reachable https URL — a print render is an unauthenticated document and cannot use session-signed or expiring links.\",\n });\n }\n resolve();\n return;\n }\n const timer = setTimeout(() => {\n timedOut.push(img.src);\n resolve();\n }, timeoutMs);\n const done = (ok: boolean): void => {\n clearTimeout(timer);\n if (!ok) {\n notify({\n code: \"image-unreachable\",\n message: `An image could not be loaded and will be blank in the PDF: ${img.src}`,\n remedy:\n \"Use a durable, publicly reachable https URL — a print render is an unauthenticated document and cannot use session-signed or expiring links.\",\n });\n }\n resolve();\n };\n img.addEventListener(\"load\", () => done(true), { once: true });\n img.addEventListener(\"error\", () => done(false), { once: true });\n }),\n ),\n );\n\n if (timedOut.length > 0) {\n notify({\n code: \"image-timeout\",\n message: `${timedOut.length} image(s) did not finish loading in ${timeoutMs}ms and may be blank in the PDF: ${timedOut.join(\", \")}`,\n remedy:\n \"Raise `imageTimeoutMs`, or serve the images from a faster/CDN origin before exporting.\",\n });\n }\n}\n\nexport async function markdownToPdfBlob(\n markdown: string,\n deps: MarkdownPdfDeps = {},\n): Promise<Blob> {\n if (!markdown.trim()) {\n throw new Error(\"Nothing to convert — the content is empty.\");\n }\n\n // A caller with no sink still gets told. Silence is not an option the API\n // offers, because a silently degraded PDF looks exactly like a correct one.\n const notify = (notice: MarkdownPdfNotice): void => {\n if (deps.onNotice) {\n deps.onNotice(notice);\n return;\n }\n console.warn(`[@ai-matrx/print] ${notice.message} → ${notice.remedy}`);\n };\n\n // The defaults live behind a lazy import: a consumer that injects its own\n // converter and stylesheet never pulls the ~20KB of package CSS into its\n // bundle, and neither does one that imports /pdf for DOM capture only.\n //\n // The specifier is the package's OWN public name, not a relative path: tsup\n // runs with `splitting: false`, so `import(\"./markdown\")` gets inlined into\n // this chunk and the lazy edge silently disappears (measured — dist/pdf.js\n // grew by the full stylesheet). A self-reference is external to the bundle\n // and resolves through the export map at runtime. Same pattern as\n // @ai-matrx/associations `./react/lazy`.\n const needsDefaults = !deps.convertToHtml || !deps.loadCss;\n const markdownModule = needsDefaults\n ? ((await import(\"@ai-matrx/print/markdown\")) as MarkdownModule)\n : undefined;\n\n const bodyHtml = deps.convertToHtml\n ? deps.convertToHtml(markdown)\n : (markdownModule as MarkdownModule).markdownToHtmlForSkin(\n markdown,\n deps.skin ?? \"document\",\n { onNotice: notify },\n );\n\n const css = deps.loadCss\n ? await deps.loadCss()\n : (markdownModule as MarkdownModule).getMarkdownStylesheet({\n skin: deps.skin ?? \"document\",\n ...(deps.tokens === undefined ? {} : { tokens: deps.tokens }),\n });\n\n if (!bodyHtml.trim()) {\n throw new Error(\n \"The markdown converted to an empty document — nothing would be printed. \" +\n \"Check the content is more than whitespace or reasoning blocks.\",\n );\n }\n\n // Offscreen host — must be in the document for html2canvas to compute\n // styles, but never visible and never intercepting input.\n const host = document.createElement(\"div\");\n host.style.cssText =\n \"position:fixed;left:-10000px;top:0;width:800px;background:#ffffff;color:#111827;pointer-events:none;\";\n host.setAttribute(\"aria-hidden\", \"true\");\n\n const style = document.createElement(\"style\");\n style.textContent = css;\n host.appendChild(style);\n\n const contentEl = document.createElement(\"div\");\n // The document stylesheet is class-scoped (.matrx-*) and expects this\n // container; the article sheet styles bare tags under `.content`.\n contentEl.className =\n (deps.skin ?? \"document\") === \"article\" ? \"content\" : \"matrx-content-container\";\n contentEl.style.cssText = \"padding:32px;background:#ffffff;\";\n contentEl.appendChild(sanitizeForCapture(bodyHtml));\n host.appendChild(contentEl);\n\n const hosts = splitHostAtPageBreaks(host, contentEl);\n for (const h of hosts) document.body.appendChild(h);\n try {\n for (const h of hosts) await settleImages(h, deps.imageTimeoutMs ?? 8000, notify);\n // Always-light document render: \"#fff\" (not \"#ffffff\") bypasses the\n // dark-mode background flip, and theme:\"light\" keeps the oklch-fallback\n // patch from substituting dark text colors while the app is in dark mode.\n const pdf = await renderSectionsToPdf(hosts, { background: \"#fff\", theme: \"light\" });\n return pdf.output(\"blob\");\n } catch (error) {\n // The raw html2canvas/jsPDF failure is meaningless to a user and to a host\n // author. Name the stage and the remedy here so no host writes a catch\n // block to reinterpret it (C22).\n throw new Error(\n `The PDF could not be rendered from this content: ${\n error instanceof Error ? error.message : String(error)\n }. Try again; if it repeats, the content likely contains an element the ` +\n \"renderer cannot rasterize (a cross-origin image without CORS headers, \" +\n \"or an embedded iframe/canvas).\",\n error instanceof Error ? { cause: error } : undefined,\n );\n } finally {\n for (const h of hosts) h.remove();\n }\n}\n\n/**\n * Split an offscreen capture host at its top-level page breaks into one host\n * per printed section (same stylesheet, same container shells), so each\n * section starts on a fresh PDF page. No breaks → the host itself.\n */\nfunction splitHostAtPageBreaks(host: HTMLElement, contentEl: HTMLElement): HTMLElement[] {\n const inner = contentEl.firstElementChild;\n const flow =\n inner instanceof HTMLElement && inner.classList.contains(\"matrx-content-container\")\n ? inner\n : contentEl;\n const hasBreak = Array.from(flow.children).some((c) => c.classList.contains(PAGE_BREAK_CLASS));\n if (!hasBreak) return [host];\n\n const style = host.querySelector(\"style\");\n const shell = (): { h: HTMLElement; target: HTMLElement } => {\n const h = host.cloneNode(false) as HTMLElement;\n if (style) h.appendChild(style.cloneNode(true));\n const c = contentEl.cloneNode(false) as HTMLElement;\n h.appendChild(c);\n if (flow === contentEl) return { h, target: c };\n const f = flow.cloneNode(false) as HTMLElement;\n c.appendChild(f);\n return { h, target: f };\n };\n\n const sections: HTMLElement[] = [];\n let current = shell();\n let hasContent = false;\n for (const node of Array.from(flow.childNodes)) {\n if (node instanceof HTMLElement && node.classList.contains(PAGE_BREAK_CLASS)) {\n if (hasContent) {\n sections.push(current.h);\n current = shell();\n hasContent = false;\n }\n continue;\n }\n if (node.nodeType === 1 || (node.textContent ?? \"\").trim()) hasContent = true;\n current.target.appendChild(node);\n }\n if (hasContent || sections.length === 0) sections.push(current.h);\n return sections;\n}\n","/**\n * @ai-matrx/print/directives — the ONE definition of the print directives\n * markdown can carry, shared by every surface that reads markdown.\n *\n * Why a separate, zero-dependency entry: the directive syntax has three\n * readers — this package's two converters (print window / PDF / HTML page),\n * the chat renderer's remark plugin (the on-screen preview), and any editor\n * that inserts one. If each re-derived the regex they would drift the first\n * time someone added an alias. `/markdown` carries ~20KB of stylesheet, so the\n * chat bundle imports the syntax from here instead and pays nothing.\n *\n * ── Page break ────────────────────────────────────────────────────────────\n * A line that is exactly one of these (case-insensitive, surrounding\n * whitespace ignored) forces the next content onto a new printed page:\n *\n * <!-- pagebreak --> ← canonical; invisible in any other renderer\n * <!-- page-break --> <!-- page break --> <!-- newpage -->\n * \\pagebreak \\newpage ← LaTeX / pandoc habit\n * <div style=\"page-break-after: always\"></div> ← Typora / VS Code habit\n * (also `page-break-before`, `break-after: page`, `break-before: page`,\n * and `<div class=\"page-break\"></div>` / `class=\"pagebreak\"`)\n *\n * On screen every surface draws the same idea: a dashed rule labelled\n * \"Page break\" with generous padding. In print it is a zero-height element\n * with `break-after: page`.\n */\n\n/** The form editors insert and docs recommend. */\nexport const PAGE_BREAK_MARKDOWN = \"<!-- pagebreak -->\";\n\n/** The class every surface puts on a rendered page break. */\nexport const PAGE_BREAK_CLASS = \"matrx-page-break\";\n\n/** Screen label for a rendered page break. */\nexport const PAGE_BREAK_LABEL = \"Page break\";\n\nconst COMMENT_FORM = /^<!--\\s*(?:page[\\s_-]?break|new[\\s_-]?page)\\s*-->$/i;\nconst LATEX_FORM = /^\\\\(?:pagebreak|newpage|clearpage)\\s*(?:\\{\\s*\\})?$/i;\nconst DIV_FORM = /^<div\\b([^>]*)>\\s*<\\/div>$|^<div\\b([^>]*)\\/>$/i;\nconst DIV_BREAK_ATTR =\n /(?:page-break-(?:after|before)\\s*:\\s*always|break-(?:after|before)\\s*:\\s*page|class\\s*=\\s*[\"'](?:[^\"']*\\s)?page-?break(?:\\s[^\"']*)?[\"'])/i;\n\n/** Columns of leading indentation (tab = 4, NBSP counts as a space). */\nfunction indentColumns(line: string): number {\n let cols = 0;\n for (const ch of line) {\n if (ch === \" \" || ch === \"\\u00A0\") cols += 1;\n else if (ch === \"\\t\") cols += 4;\n else break;\n }\n return cols;\n}\n\n/**\n * Is this single line a page-break directive? Indented four or more columns\n * it is not — in markdown that is a code block, and every surface must agree.\n */\nexport function isPageBreakLine(line: string): boolean {\n if (indentColumns(line) >= 4) return false;\n const t = line.trim();\n if (t.length < 8 || t.length > 200) return false;\n if (COMMENT_FORM.test(t) || LATEX_FORM.test(t)) return true;\n const div = DIV_FORM.exec(t);\n if (!div) return false;\n return DIV_BREAK_ATTR.test(div[1] ?? div[2] ?? \"\");\n}\n\n// ── Section break ─────────────────────────────────────────────────────────\n//\n// A section break is a page break that can also change the page layout for\n// everything after it (Word's \"Section break (next page)\"):\n//\n// <!-- section --> ← new page, same layout\n// <!-- section: landscape --> ← new page, landscape from here on\n// <!-- section landscape columns=2 --> ← landscape AND two columns\n// <!-- section portrait columns=1 --> ← back to the default layout\n//\n// Recognised words: `landscape` / `portrait`, `columns=N` (or `cols=N`,\n// `N columns`, 1–3). Unknown words are ignored, never an error — the break\n// still happens. Every page-break reader treats a section break as a break.\n\n/** The form editors insert for a plain section break. */\nexport const SECTION_BREAK_MARKDOWN = \"<!-- section -->\";\n\nexport interface SectionBreakDirective {\n /** Absent = inherit the document's orientation. */\n orientation?: \"portrait\" | \"landscape\";\n /** Absent = inherit the document's column count. 1–3. */\n columns?: 1 | 2 | 3;\n}\n\nconst SECTION_FORM = /^<!--\\s*section(?:[\\s_-]?break)?\\b\\s*:?\\s*([^>]*?)\\s*-->$/i;\n\n/** Parse a section-break line; `null` when the line is not one. */\nexport function parseSectionBreakLine(line: string): SectionBreakDirective | null {\n if (indentColumns(line) >= 4) return null;\n const m = SECTION_FORM.exec(line.trim());\n if (!m) return null;\n const words = (m[1] ?? \"\").toLowerCase();\n const out: SectionBreakDirective = {};\n if (/\\blandscape\\b/.test(words)) out.orientation = \"landscape\";\n else if (/\\bportrait\\b/.test(words)) out.orientation = \"portrait\";\n const cols = /\\b(?:columns?|cols?)\\s*[=:]\\s*(\\d)\\b/.exec(words) ?? /\\b(\\d)\\s*[-\\s]?columns?\\b/.exec(words);\n if (cols) {\n const n = Math.min(3, Math.max(1, Number(cols[1])));\n out.columns = n as 1 | 2 | 3;\n }\n return out;\n}\n\n// ── Table of contents / bibliography placement ───────────────────────────\n//\n// `[[toc]]`, `[toc]`, `[TOC]`, `${toc}`, `<!-- toc -->`, `\\tableofcontents`\n// put the table of contents at that spot (otherwise frontmatter `toc: true`\n// puts it at the start). `<!-- bibliography -->`, `[[bibliography]]`,\n// `\\printbibliography` put the reference list there (otherwise it closes the\n// document whenever something is cited).\n\nexport const TOC_MARKDOWN = \"[[toc]]\";\nexport const BIBLIOGRAPHY_MARKDOWN = \"<!-- bibliography -->\";\n\nconst TOC_FORM = /^(?:\\[\\[\\s*toc\\s*\\]\\]|\\[toc\\]|\\$\\{toc\\}|<!--\\s*toc\\s*-->|\\\\tableofcontents)$/i;\nconst BIBLIOGRAPHY_FORM =\n /^(?:\\[\\[\\s*(?:bibliography|references)\\s*\\]\\]|<!--\\s*(?:bibliography|references)\\s*-->|\\\\printbibliography|\\\\bibliography\\{[^}]*\\})$/i;\n\nexport function isTocLine(line: string): boolean {\n return indentColumns(line) < 4 && TOC_FORM.test(line.trim());\n}\n\nexport function isBibliographyLine(line: string): boolean {\n return indentColumns(line) < 4 && BIBLIOGRAPHY_FORM.test(line.trim());\n}\n\n/** Any directive that ends the current printed page (page or section break). */\nexport function isBreakLine(line: string): boolean {\n return isPageBreakLine(line) || parseSectionBreakLine(line) !== null;\n}\n\n/**\n * Split markdown into the pieces that belong on separate pages. Fence-aware:\n * a directive inside a ``` / ~~~ code block is code, not a break. Always\n * returns at least one piece; empty pieces (two breaks in a row, a break at\n * the very start or end) are kept out so no blank page is produced.\n */\nexport function splitAtPageBreaks(markdown: string): string[] {\n const lines = markdown.split(/\\r?\\n/);\n const pieces: string[] = [];\n let current: string[] = [];\n let fence: string | null = null;\n\n for (const line of lines) {\n // Any indent — the converters accept list-nested fences, so must this.\n const fenceMatch = /^\\s*(`{3,}|~{3,})/.exec(line);\n if (fenceMatch) {\n const marker = fenceMatch[1] as string;\n if (fence === null) fence = marker;\n else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;\n } else if (fence === null && isBreakLine(line)) {\n if (current.join(\"\\n\").trim()) pieces.push(current.join(\"\\n\"));\n current = [];\n continue;\n }\n current.push(line);\n }\n if (current.join(\"\\n\").trim() || pieces.length === 0) pieces.push(current.join(\"\\n\"));\n return pieces;\n}\n","/**\n * Flatten CSS 3D flips for a canvas capture.\n *\n * html2canvas has no 3D: it ignores `backface-visibility` and `transform-style:\n * preserve-3d`, so a flip card (two faces stacked, the back turned with\n * `rotateY(180deg)`) is drawn with BOTH faces on top of each other and the back\n * face mirrored. This runs on the capture's cloned document and turns every such\n * stack into a flat row: the faces sit side by side, the face turned toward the\n * viewer first, every face un-mirrored and shown — so a printed card carries its\n * answer as well as its question. Plain 2D transforms are never touched.\n */\n\nconst IDENTITY = \"none\";\n\ntype FrameWindow = Window & typeof globalThis;\n\nfunction matrixOf(win: FrameWindow, cs: CSSStyleDeclaration): DOMMatrix {\n const t = cs.transform;\n if (!t || t === IDENTITY) return new win.DOMMatrix();\n try {\n return new win.DOMMatrix(t);\n } catch {\n return new win.DOMMatrix();\n }\n}\n\n/** True when the face, after every ancestor transform up to `root`, points toward the viewer. */\nfunction facesViewer(win: FrameWindow, face: HTMLElement, root: HTMLElement): boolean {\n let m = new win.DOMMatrix();\n for (let node: HTMLElement | null = face; node; node = node.parentElement) {\n m = matrixOf(win, win.getComputedStyle(node)).multiply(m);\n if (node === root) break;\n }\n // m33 is the z-component of the face normal after rotation: > 0 faces the viewer.\n return m.m33 >= 0;\n}\n\nfunction is3d(cs: CSSStyleDeclaration): boolean {\n return (\n cs.transformStyle === \"preserve-3d\" ||\n cs.backfaceVisibility === \"hidden\" ||\n cs.perspective !== \"none\" ||\n (cs.transform ?? \"\").startsWith(\"matrix3d\")\n );\n}\n\n/** Make every 3D flip in `root` html2canvas-safe. Idempotent; a no-op when there is no 3D. */\nexport function flattenFlips3d(doc: Document, root: HTMLElement): void {\n const win = doc.defaultView as FrameWindow | null;\n if (!win) return;\n const all = [root, ...Array.from(root.querySelectorAll<HTMLElement>(\"*\"))];\n const faces = all.filter((el) => win.getComputedStyle(el).backfaceVisibility === \"hidden\");\n if (faces.length === 0 && !all.some((el) => is3d(win.getComputedStyle(el)))) return;\n\n // Decide every face's facing BEFORE any style changes (it reads ancestor transforms).\n const facing = new Map<HTMLElement, boolean>();\n for (const face of faces) facing.set(face, facesViewer(win, face, root));\n\n // Faces grouped by the element that stacks them: that element becomes a flat row.\n const stacks = new Map<HTMLElement, HTMLElement[]>();\n for (const face of faces) {\n const parent = face.parentElement;\n if (!parent) continue;\n const list = stacks.get(parent) ?? [];\n list.push(face);\n stacks.set(parent, list);\n }\n\n for (const [stack, stackFaces] of stacks) {\n // A card is a fixed-height box with clipped faces; a printed face is wider-than-tall\n // text that must not be cut mid-line. Keep the box as tall as it was, let it grow.\n for (const box of [stack, stack.parentElement]) {\n if (!box || box === root) continue;\n const height = win.getComputedStyle(box).height;\n if (height.endsWith(\"px\")) box.style.setProperty(\"min-height\", height);\n box.style.setProperty(\"height\", \"auto\");\n box.style.setProperty(\"max-height\", \"none\");\n }\n if (stackFaces.length > 1) {\n stack.style.setProperty(\"display\", \"grid\");\n stack.style.setProperty(\"grid-template-columns\", `repeat(${stackFaces.length}, minmax(0, 1fr))`);\n stack.style.setProperty(\"gap\", \"8px\");\n }\n for (const face of stackFaces) {\n face.style.setProperty(\"position\", \"relative\");\n face.style.setProperty(\"inset\", \"auto\");\n face.style.setProperty(\"height\", \"auto\");\n face.style.setProperty(\"overflow\", \"visible\");\n face.style.setProperty(\"order\", facing.get(face) ? \"0\" : \"1\");\n }\n }\n\n for (const el of all) {\n const cs = win.getComputedStyle(el);\n if (!is3d(cs)) continue;\n el.style.setProperty(\"transform\", \"none\");\n el.style.setProperty(\"transform-style\", \"flat\");\n el.style.setProperty(\"perspective\", \"none\");\n el.style.setProperty(\"backface-visibility\", \"visible\");\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoCA,uBAA0B;;;ACLnB,IAAM,mBAAmB;;;ACnBhC,IAAM,WAAW;AAIjB,SAAS,SAAS,KAAkB,IAAoC;AACtE,QAAM,IAAI,GAAG;AACb,MAAI,CAAC,KAAK,MAAM,SAAU,QAAO,IAAI,IAAI,UAAU;AACnD,MAAI;AACF,WAAO,IAAI,IAAI,UAAU,CAAC;AAAA,EAC5B,QAAQ;AACN,WAAO,IAAI,IAAI,UAAU;AAAA,EAC3B;AACF;AAGA,SAAS,YAAY,KAAkB,MAAmB,MAA4B;AACpF,MAAI,IAAI,IAAI,IAAI,UAAU;AAC1B,WAAS,OAA2B,MAAM,MAAM,OAAO,KAAK,eAAe;AACzE,QAAI,SAAS,KAAK,IAAI,iBAAiB,IAAI,CAAC,EAAE,SAAS,CAAC;AACxD,QAAI,SAAS,KAAM;AAAA,EACrB;AAEA,SAAO,EAAE,OAAO;AAClB;AAEA,SAAS,KAAK,IAAkC;AAC9C,SACE,GAAG,mBAAmB,iBACtB,GAAG,uBAAuB,YAC1B,GAAG,gBAAgB,WAClB,GAAG,aAAa,IAAI,WAAW,UAAU;AAE9C;AAGO,SAAS,eAAe,KAAe,MAAyB;AACrE,QAAM,MAAM,IAAI;AAChB,MAAI,CAAC,IAAK;AACV,QAAM,MAAM,CAAC,MAAM,GAAG,MAAM,KAAK,KAAK,iBAA8B,GAAG,CAAC,CAAC;AACzE,QAAM,QAAQ,IAAI,OAAO,CAAC,OAAO,IAAI,iBAAiB,EAAE,EAAE,uBAAuB,QAAQ;AACzF,MAAI,MAAM,WAAW,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,iBAAiB,EAAE,CAAC,CAAC,EAAG;AAG7E,QAAM,SAAS,oBAAI,IAA0B;AAC7C,aAAW,QAAQ,MAAO,QAAO,IAAI,MAAM,YAAY,KAAK,MAAM,IAAI,CAAC;AAGvE,QAAM,SAAS,oBAAI,IAAgC;AACnD,aAAW,QAAQ,OAAO;AACxB,UAAM,SAAS,KAAK;AACpB,QAAI,CAAC,OAAQ;AACb,UAAM,OAAO,OAAO,IAAI,MAAM,KAAK,CAAC;AACpC,SAAK,KAAK,IAAI;AACd,WAAO,IAAI,QAAQ,IAAI;AAAA,EACzB;AAEA,aAAW,CAAC,OAAO,UAAU,KAAK,QAAQ;AAGxC,eAAW,OAAO,CAAC,OAAO,MAAM,aAAa,GAAG;AAC9C,UAAI,CAAC,OAAO,QAAQ,KAAM;AAC1B,YAAM,SAAS,IAAI,iBAAiB,GAAG,EAAE;AACzC,UAAI,OAAO,SAAS,IAAI,EAAG,KAAI,MAAM,YAAY,cAAc,MAAM;AACrE,UAAI,MAAM,YAAY,UAAU,MAAM;AACtC,UAAI,MAAM,YAAY,cAAc,MAAM;AAAA,IAC5C;AACA,QAAI,WAAW,SAAS,GAAG;AACzB,YAAM,MAAM,YAAY,WAAW,MAAM;AACzC,YAAM,MAAM,YAAY,yBAAyB,UAAU,WAAW,MAAM,mBAAmB;AAC/F,YAAM,MAAM,YAAY,OAAO,KAAK;AAAA,IACtC;AACA,eAAW,QAAQ,YAAY;AAC7B,WAAK,MAAM,YAAY,YAAY,UAAU;AAC7C,WAAK,MAAM,YAAY,SAAS,MAAM;AACtC,WAAK,MAAM,YAAY,UAAU,MAAM;AACvC,WAAK,MAAM,YAAY,YAAY,SAAS;AAC5C,WAAK,MAAM,YAAY,SAAS,OAAO,IAAI,IAAI,IAAI,MAAM,GAAG;AAAA,IAC9D;AAAA,EACF;AAEA,aAAW,MAAM,KAAK;AACpB,UAAM,KAAK,IAAI,iBAAiB,EAAE;AAClC,QAAI,CAAC,KAAK,EAAE,EAAG;AACf,OAAG,MAAM,YAAY,aAAa,MAAM;AACxC,OAAG,MAAM,YAAY,mBAAmB,MAAM;AAC9C,OAAG,MAAM,YAAY,eAAe,MAAM;AAC1C,OAAG,MAAM,YAAY,uBAAuB,SAAS;AAAA,EACvD;AACF;;;AF7BA,IAAM,YAAY;AAAA,EAChB,IAAI,EAAE,GAAG,KAAK,GAAG,IAAI;AAAA,EACrB,QAAQ,EAAE,GAAG,OAAO,GAAG,MAAM;AAC/B;AAGA,IAAM,kBAAkB;AAExB,SAAS,YAAY,OAAwB;AAC3C,SAAO,CAAC,gBAAgB,KAAK,KAAK;AACpC;AAMA,SAAS,WAAW,OAAe,MAAc,QAAyB;AACxE,MAAI,YAAY,KAAK,EAAG,QAAO;AAC/B,QAAM,KAAK,KAAK,YAAY;AAC5B,QAAM,WAAW,GAAG,SAAS,YAAY,IACrC,SAAS,YAAY,YACrB,OAAO,UACL,SAAS,YAAY,YACrB,GAAG,SAAS,QAAQ,KAAK,GAAG,SAAS,SAAS,IAC5C,SAAS,YAAY,YACrB,OAAO,SACL,SAAS,YAAY,YACrB;AAKV,SAAO,MAAM,QAAQ,oBAAoB,QAAQ;AACnD;AAGA,IAAM,qBAAqB;AAS3B,SAAS,sBAAsB,QAAiB,QAA2B,CAAC,MAAM,GAAe;AAC/F,QAAM,WAAW,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,EAAE,IAAI,CAAC,SAAS,eAAe,MAAM,MAAM,CAAC;AAC/E,SAAO,MAAM,SAAS,QAAQ,CAAC,YAAY,QAAQ,CAAC;AACtD;AAEA,SAAS,eAAeA,SAAgB,QAA6B;AAOnE,QAAM,WAAWA,QAAO;AACxB,QAAM,eAAe,SAAS,KAAKA,OAAM;AAEzC,EAAAA,QAAO,mBAAmB,SAAS,wBACjC,KACA,WACqB;AACrB,UAAM,WAAW,aAAa,KAAK,SAAS;AAE5C,WAAO,IAAI,MAAM,UAAU;AAAA,MACzB,IAAI,QAAQ,MAAM;AAIhB,cAAM,QAAiB,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAGvD,YAAI,OAAO,SAAS,YAAY,OAAO,UAAU,YAAY,OAAO;AAClE,cAAI,CAAC,YAAY,KAAK,GAAG;AACvB,mBAAO,WAAW,OAAO,MAAM,MAAM;AAAA,UACvC;AAAA,QACF;AAGA,YAAI,SAAS,oBAAoB;AAC/B,iBAAO,CAAC,aAAqB;AAC3B,kBAAM,MAAM,OAAO,iBAAiB,QAAQ;AAC5C,gBAAI,OAAO,CAAC,YAAY,GAAG,GAAG;AAC5B,qBAAO,WAAW,KAAK,UAAU,MAAM;AAAA,YACzC;AACA,mBAAO;AAAA,UACT;AAAA,QACF;AAEA,YAAI,OAAO,UAAU,YAAY;AAC/B,gBAAM,KAAK;AACX,iBAAO,GAAG,KAAK,MAAM;AAAA,QACvB;AAEA,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO,MAAM;AACX,IAAAA,QAAO,mBAAmB;AAAA,EAC5B;AACF;AAKA,eAAsB,aACpB,SACA,UAA6B,CAAC,GACf;AACf,QAAM,MAAM,MAAM,mBAAmB,SAAS,OAAO;AACrD,MAAI,KAAK,GAAG,QAAQ,YAAY,aAAa,MAAM;AACrD;AAOA,eAAsB,iBACpB,SACA,UAA6B,CAAC,GACf;AACf,QAAM,MAAM,MAAM,mBAAmB,SAAS,OAAO;AACrD,SAAO,IAAI,OAAO,MAAM;AAC1B;AAUA,eAAsB,qBACpB,UACA,UAA6B,CAAC,GACf;AACf,QAAM,MAAM,MAAM,oBAAoB,UAAU,OAAO;AACvD,MAAI,KAAK,GAAG,QAAQ,YAAY,aAAa,MAAM;AACrD;AAGA,eAAsB,yBACpB,UACA,UAA6B,CAAC,GACf;AACf,QAAM,MAAM,MAAM,oBAAoB,UAAU,OAAO;AACvD,SAAO,IAAI,OAAO,MAAM;AAC1B;AAEA,eAAe,oBACb,UACA,SACA;AACA,MAAI,SAAS,WAAW,GAAG;AACzB,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AAEA,QAAM;AAAA,IACJ,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,IACf;AAAA,EACF,IAAI;AACJ,QAAM,CAAC,EAAE,SAAS,YAAY,GAAG,EAAE,SAAS,MAAM,CAAC,IAAI,MAAM,QAAQ,IAAI;AAAA,IACvE,OAAO,aAAa;AAAA,IACpB,OAAO,OAAO;AAAA,EAChB,CAAC;AACD,QAAM,OAAO,UAAU,SAAS;AAChC,QAAM,QAAQ,gBAAgB,aAAa,KAAK,IAAI,KAAK;AACzD,QAAM,QAAQ,gBAAgB,aAAa,KAAK,IAAI,KAAK;AACzD,QAAM,MAAM,IAAI,MAAM,EAAE,aAAa,MAAM,MAAM,QAAQ,UAAU,CAAC;AACpE,QAAM,0BAA0B,sBAAsB,OAAO,CAAC,QAAQ,GAAG,SAAS,IAAI,CAAC,MAAM,EAAE,cAAc,eAAe,MAAM,CAAC,CAAC;AAEpI,MAAI;AACF,eAAW,CAAC,OAAO,OAAO,KAAK,SAAS,QAAQ,GAAG;AACjD,mBAAa,OAAO,SAAS,MAAM;AACnC,YAAM,SAAS,MAAM,YAAY,SAAS;AAAA,QACxC;AAAA,QACA,SAAS;AAAA,QACT,iBAAiB;AAAA,QACjB,SAAS;AAAA,QACT,aAAa,QAAQ;AAAA,QACrB,cAAc,QAAQ;AAAA,QACtB,SAAS,CAAC,WAAW,aAAa,eAAe,WAAW,QAAQ;AAAA,MACtE,CAAC;AACD,UAAI,QAAQ,EAAG,KAAI,QAAQ;AAC3B,YAAM,SAAS,gBAAgB;AAC/B,UAAI;AAAA,QACF,OAAO;AAAA,UACL,SAAS,eAAe;AAAA,UACxB,SAAS,eAAe;AAAA,QAC1B;AAAA,QACA,SAAS,SAAS;AAAA,QAClB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,mBAAa,QAAQ,GAAG,SAAS,MAAM;AAAA,IACzC;AAAA,EACF,UAAE;AACA,4BAAwB;AAAA,EAC1B;AAEA,SAAO;AACT;AAEA,eAAe,mBACb,SACA,UAA6B,CAAC,GAC9B;AACA,SAAO,oBAAoB,CAAC,OAAO,GAAG,OAAO;AAC/C;AAQA,eAAe,oBACb,UACA,UAA6B,CAAC,GAC9B;AACA,QAAM;AAAA,IACJ,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,IACf;AAAA,EACF,IAAI;AAEJ,QAAM,CAAC,EAAE,SAAS,YAAY,GAAG,EAAE,SAAS,MAAM,CAAC,IAAI,MAAM,QAAQ,IAAI;AAAA,IACvE,OAAO,aAAa;AAAA,IACpB,OAAO,OAAO;AAAA,EAChB,CAAC;AAED,QAAM,OAAO,UAAU,SAAS;AAChC,QAAM,QAAQ,gBAAgB,aAAa,KAAK,IAAI,KAAK;AACzD,QAAM,QAAQ,gBAAgB,aAAa,KAAK,IAAI,KAAK;AAEzD,eAAa,GAAG,CAAC;AAEjB,QAAM,SACJ,QAAQ,UAAU,UACd,QACA,SAAS,gBAAgB,UAAU,SAAS,MAAM,KAClD,OAAO,WAAW,8BAA8B,EAAE;AAExD,QAAM,UAAU,eAAe,aAAa,SAAS,YAAY;AAGjE,QAAM,0BAA0B,sBAAsB,QAAQ,CAAC,QAAQ,GAAG,SAAS,IAAI,CAAC,MAAM,EAAE,cAAc,eAAe,MAAM,CAAC,CAAC;AAQrI,QAAM,SAAS,gBAAgB;AAC/B,QAAM,YAAY,SAAS,SAAS;AACpC,QAAM,MAAM,IAAI,MAAM,EAAE,aAAa,MAAM,MAAM,QAAQ,UAAU,CAAC;AAIpE,QAAM,aAAa;AACnB,QAAM,WAAW,SAAS,IAAI,CAAC,YAAY;AACzC,UAAM,OAAO,KAAK,IAAI,GAAG,QAAQ,WAAW;AAC5C,UAAM,OAAO,KAAK,IAAI,GAAG,QAAQ,YAAY;AAC7C,UAAM,SAAU,OAAO,QAAS;AAChC,UAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAO,OAAO,QAAS,OAAO,cAAc,KAAK,CAAC;AACjF,WAAO,EAAE,SAAS,MAAM,MAAM,QAAQ,MAAM;AAAA,EAC9C,CAAC;AACD,QAAM,aAAa,SAAS,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,OAAO,CAAC;AAC3D,MAAI,OAAO;AAEX,MAAI;AACF,eAAW,EAAE,SAAS,MAAM,MAAM,QAAQ,MAAM,KAAK,UAAU;AAC7D,YAAM,OAAO,QAAQ,cAAc,eAAe;AAClD,YAAM,MAAM,QAAQ,sBAAsB;AAC1C,YAAM,OAAO,IAAI,OAAO,KAAK;AAC7B,YAAM,MAAM,IAAI,MAAM,KAAK;AAC3B,eAAS,QAAQ,GAAG,QAAQ,OAAO,SAAS,GAAG;AAC7C,cAAM,SAAS,QAAQ;AACvB,cAAM,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,OAAO,MAAM,CAAC;AAC1D,cAAM,SAAS,MAAM,YAAY,SAAS;AAAA,UACxC;AAAA,UACA,SAAS;AAAA,UACT,iBAAiB;AAAA,UACjB,SAAS;AAAA,UACT,aAAa;AAAA,UACb,cAAc;AAAA,UACd,GAAG;AAAA,UACH,GAAG,MAAM;AAAA,UACT,OAAO;AAAA,UACP;AAAA,UACA,SAAS,CAAC,WAAW,aAAa,eAAe,WAAW,QAAQ;AAAA,QACtE,CAAC;AACD,cAAM,UAAU,OAAO;AAAA,UACrB,SAAS,eAAe;AAAA,UACxB,SAAS,eAAe;AAAA,QAC1B;AACA,YAAI,CAAC,QAAQ,WAAW,aAAa,GAAG;AACtC,gBAAM,IAAI,MAAM,QAAQ,OAAO,CAAC,8CAA8C,OAAO,KAAK,OAAI,OAAO,MAAM,UAAU;AAAA,QACvH;AACA,YAAI,OAAO,EAAG,KAAI,QAAQ;AAC1B,YAAI,SAAS,SAAS,WAAW,GAAG,GAAG,OAAQ,SAAS,QAAS,IAAI;AACrE,gBAAQ;AACR,qBAAa,MAAM,UAAU;AAAA,MAC/B;AAAA,IACF;AAAA,EACF,UAAE;AACA,4BAAwB;AAAA,EAC1B;AAEA,SAAO;AACT;AAQA,eAAsB,wBACpB,SACkB;AAClB,QAAM,SACJ,SAAS,gBAAgB,UAAU,SAAS,MAAM,KAClD,OAAO,WAAW,8BAA8B,EAAE;AACpD,QAAM,SAAS,MAAM,cAAc,SAAS;AAAA,IAC1C,OAAO;AAAA,IACP,iBAAiB,SAAS,YAAY;AAAA,IACtC,YAAY,EAAE,MAAM,OAAO;AAAA,EAC7B,CAAC;AACD,QAAM,OAAO,MAAM,IAAI;AAAA,IAAqB,CAAC,YAC3C,OAAO,OAAO,SAAS,WAAW;AAAA,EACpC;AACA,MAAI,CAAC,MAAM;AACT,YAAQ,MAAM,wDAAwD;AACtE,WAAO;AAAA,EACT;AACA,aAAO,4BAAU,IAAI;AACvB;AA6BA,IAAM,cAAc,MAClB,OAAO,aAAa,gBACnB,SAAS,gBAAgB,UAAU,SAAS,MAAM,KAChD,OAAO,OAAO,eAAe,cAC5B,OAAO,WAAW,8BAA8B,EAAE;AAGxD,eAAsB,cACpB,SACA,UAAgC,CAAC,GACL;AAC5B,QAAM,EAAE,YAAY,GAAG,KAAK,IAAI;AAChC,QAAM,EAAE,SAAS,YAAY,IAAI,MAAM,OAAO,aAAa;AAC3D,QAAM,UAAU,aACZ;AAAA,IACE,OAAO,eAAe,WAAW,WAAW,OAAO,YAAY;AAAA,EACjE,IACA,MAAM;AAAA,EAAC;AACX,MAAI;AACF,UAAM,cAAc,KAAK;AACzB,WAAO,MAAM,YAAY,SAAS;AAAA,MAChC,OAAO;AAAA,MACP,SAAS;AAAA,MACT,SAAS;AAAA,MACT,GAAG;AAAA,MACH,SAAS,OAAO,WAAqB,aAA0B;AAC7D,uBAAe,WAAW,QAAQ;AAClC,cAAM,cAAc,WAAW,QAAQ;AAAA,MACzC;AAAA,IACF,CAAsC;AAAA,EACxC,UAAE;AACA,YAAQ;AAAA,EACV;AACF;AAqBA,eAAsB,WACpB,OACA,UAA8B,CAAC,GAChB;AACf,QAAM,EAAE,MAAM,IAAI,MAAM,OAAO,OAAO;AACtC,QAAM,MAAM,IAAI,MAAM;AAAA,IACpB,aAAa,QAAQ,eAAe;AAAA,IACpC,MAAM,QAAQ,QAAQ;AAAA,IACtB,QAAQ,QAAQ,UAAU;AAAA,EAC5B,CAAC;AACD,QAAM,QAAQ,CAAC,MAAM,UAAU;AAC7B,QAAI,QAAQ,EAAG,KAAI,QAAQ;AAC3B,UAAM,QAAQ,IAAI,SAAS,SAAS,SAAS;AAC7C,UAAM,QAAQ,IAAI,SAAS,SAAS,UAAU;AAC9C,QAAI,KAAK,QAAQ;AACf,UAAI,YAAY,KAAK,OAAO,YAAY,EAAE;AAC1C,UAAI,KAAK,KAAK,OAAO,MAAM,KAAK,OAAO,GAAG,KAAK,OAAO,CAAC;AAAA,IACzD;AACA,UAAM,OAAO,KAAK,aAAa;AAC/B,UAAM,OACJ,SAAS,SACL,KAAK,MAAM,UAAU,cAAc,KAAK,WAAW,IAAI,IACvD,KAAK,MAAM,UAAU,WAAW;AACtC,UAAM,YAAY,KAAK,aAAa;AACpC,QAAI,IAAI,GACN,IAAI,GACJ,IAAI,OACJ,IAAI;AACN,QAAI,cAAc,QAAQ;AACxB,YAAM,SAAS,QAAQ,UAAU,OAAO,UAAU;AAClD,YAAM,SAAS,QAAQ,UAAU,MAAM,UAAU;AACjD,YAAM,QAAQ,KAAK,IAAI,SAAS,KAAK,MAAM,OAAO,SAAS,KAAK,MAAM,MAAM;AAC5E,UAAI,UAAU;AACd,UAAI,UAAU;AACd,UAAI,KAAK,MAAM,QAAQ;AACvB,UAAI,KAAK,MAAM,SAAS;AAAA,IAC1B;AACA,QAAI,KAAK,YAAa,KAAI,SAAS,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,QAAW,KAAK,WAAW;AAAA,QACjF,KAAI,SAAS,MAAM,MAAM,GAAG,GAAG,GAAG,CAAC;AAAA,EAC1C,CAAC;AACD,SAAO,IAAI,OAAO,MAAM;AAC1B;AAuBO,SAAS,wBAAwB,YAAkC;AACtE,SAAO;AAAA,IACH,OAAO,SAAS,UAAU;AAAA,IAC1B,UAAU,CAAC;AAAA;AAAA,IAEX,OAAO,MAAM;AAAA,IAAC;AAAA,EAClB;AACJ;AAUA,eAAsB,oBAClB,SACA,UACA,cAAwC,aAC3B;AACb,QAAM,aAAa,SAAS;AAAA,IACxB;AAAA,IACA,OAAO;AAAA,IACP;AAAA,EACJ,CAAC;AACL;AAgEO,SAAS,mBAAmB,MAAgC;AACjE,QAAM,WAAW,SAAS,cAAc,UAAU;AAClD,WAAS,YAAY;AACrB,QAAM,WAAW,SAAS;AAE1B,WACG,iBAAiB,2CAA2C,EAC5D,QAAQ,CAAC,OAAO,GAAG,OAAO,CAAC;AAC9B,WAAS,iBAAiB,GAAG,EAAE,QAAQ,CAAC,OAAO;AAC7C,eAAW,QAAQ,MAAM,KAAK,GAAG,UAAU,GAAG;AAC5C,YAAM,OAAO,KAAK,KAAK,YAAY;AACnC,UAAI,KAAK,WAAW,IAAI,EAAG,IAAG,gBAAgB,KAAK,IAAI;AACvD,WACG,SAAS,UAAU,SAAS,UAC7B,KAAK,MAAM,KAAK,EAAE,YAAY,EAAE,WAAW,aAAa,GACxD;AACA,WAAG,gBAAgB,KAAK,IAAI;AAAA,MAC9B;AAAA,IACF;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAYA,eAAe,aACb,MACA,WACA,QACe;AACf,QAAM,SAAS,MAAM,KAAK,KAAK,iBAAiB,KAAK,CAAC;AACtD,MAAI,OAAO,WAAW,EAAG;AAEzB,QAAM,WAAqB,CAAC;AAC5B,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL,CAAC,QACC,IAAI,QAAc,CAAC,YAAY;AAC7B,YAAI,IAAI,UAAU;AAChB,cAAI,IAAI,iBAAiB,GAAG;AAC1B,mBAAO;AAAA,cACL,MAAM;AAAA,cACN,SAAS,8DAA8D,IAAI,GAAG;AAAA,cAC9E,QACE;AAAA,YACJ,CAAC;AAAA,UACH;AACA,kBAAQ;AACR;AAAA,QACF;AACA,cAAM,QAAQ,WAAW,MAAM;AAC7B,mBAAS,KAAK,IAAI,GAAG;AACrB,kBAAQ;AAAA,QACV,GAAG,SAAS;AACZ,cAAM,OAAO,CAAC,OAAsB;AAClC,uBAAa,KAAK;AAClB,cAAI,CAAC,IAAI;AACP,mBAAO;AAAA,cACL,MAAM;AAAA,cACN,SAAS,8DAA8D,IAAI,GAAG;AAAA,cAC9E,QACE;AAAA,YACJ,CAAC;AAAA,UACH;AACA,kBAAQ;AAAA,QACV;AACA,YAAI,iBAAiB,QAAQ,MAAM,KAAK,IAAI,GAAG,EAAE,MAAM,KAAK,CAAC;AAC7D,YAAI,iBAAiB,SAAS,MAAM,KAAK,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AAAA,MACjE,CAAC;AAAA,IACL;AAAA,EACF;AAEA,MAAI,SAAS,SAAS,GAAG;AACvB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,GAAG,SAAS,MAAM,uCAAuC,SAAS,mCAAmC,SAAS,KAAK,IAAI,CAAC;AAAA,MACjI,QACE;AAAA,IACJ,CAAC;AAAA,EACH;AACF;AAEA,eAAsB,kBACpB,UACA,OAAwB,CAAC,GACV;AACf,MAAI,CAAC,SAAS,KAAK,GAAG;AACpB,UAAM,IAAI,MAAM,iDAA4C;AAAA,EAC9D;AAIA,QAAM,SAAS,CAAC,WAAoC;AAClD,QAAI,KAAK,UAAU;AACjB,WAAK,SAAS,MAAM;AACpB;AAAA,IACF;AACA,YAAQ,KAAK,qBAAqB,OAAO,OAAO,WAAM,OAAO,MAAM,EAAE;AAAA,EACvE;AAYA,QAAM,gBAAgB,CAAC,KAAK,iBAAiB,CAAC,KAAK;AACnD,QAAM,iBAAiB,gBACjB,MAAM,OAAO,0BAA0B,IACzC;AAEJ,QAAM,WAAW,KAAK,gBAClB,KAAK,cAAc,QAAQ,IAC1B,eAAkC;AAAA,IACjC;AAAA,IACA,KAAK,QAAQ;AAAA,IACb,EAAE,UAAU,OAAO;AAAA,EACrB;AAEJ,QAAM,MAAM,KAAK,UACb,MAAM,KAAK,QAAQ,IAClB,eAAkC,sBAAsB;AAAA,IACvD,MAAM,KAAK,QAAQ;AAAA,IACnB,GAAI,KAAK,WAAW,SAAY,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO;AAAA,EAC7D,CAAC;AAEL,MAAI,CAAC,SAAS,KAAK,GAAG;AACpB,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAIA,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,MAAM,UACT;AACF,OAAK,aAAa,eAAe,MAAM;AAEvC,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,cAAc;AACpB,OAAK,YAAY,KAAK;AAEtB,QAAM,YAAY,SAAS,cAAc,KAAK;AAG9C,YAAU,aACP,KAAK,QAAQ,gBAAgB,YAAY,YAAY;AACxD,YAAU,MAAM,UAAU;AAC1B,YAAU,YAAY,mBAAmB,QAAQ,CAAC;AAClD,OAAK,YAAY,SAAS;AAE1B,QAAM,QAAQ,sBAAsB,MAAM,SAAS;AACnD,aAAW,KAAK,MAAO,UAAS,KAAK,YAAY,CAAC;AAClD,MAAI;AACF,eAAW,KAAK,MAAO,OAAM,aAAa,GAAG,KAAK,kBAAkB,KAAM,MAAM;AAIhF,UAAM,MAAM,MAAM,oBAAoB,OAAO,EAAE,YAAY,QAAQ,OAAO,QAAQ,CAAC;AACnF,WAAO,IAAI,OAAO,MAAM;AAAA,EAC1B,SAAS,OAAO;AAId,UAAM,IAAI;AAAA,MACR,oDACE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CACvD;AAAA,MAGA,iBAAiB,QAAQ,EAAE,OAAO,MAAM,IAAI;AAAA,IAC9C;AAAA,EACF,UAAE;AACA,eAAW,KAAK,MAAO,GAAE,OAAO;AAAA,EAClC;AACF;AAOA,SAAS,sBAAsB,MAAmB,WAAuC;AACvF,QAAM,QAAQ,UAAU;AACxB,QAAM,OACJ,iBAAiB,eAAe,MAAM,UAAU,SAAS,yBAAyB,IAC9E,QACA;AACN,QAAM,WAAW,MAAM,KAAK,KAAK,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,SAAS,gBAAgB,CAAC;AAC7F,MAAI,CAAC,SAAU,QAAO,CAAC,IAAI;AAE3B,QAAM,QAAQ,KAAK,cAAc,OAAO;AACxC,QAAM,QAAQ,MAA+C;AAC3D,UAAM,IAAI,KAAK,UAAU,KAAK;AAC9B,QAAI,MAAO,GAAE,YAAY,MAAM,UAAU,IAAI,CAAC;AAC9C,UAAM,IAAI,UAAU,UAAU,KAAK;AACnC,MAAE,YAAY,CAAC;AACf,QAAI,SAAS,UAAW,QAAO,EAAE,GAAG,QAAQ,EAAE;AAC9C,UAAM,IAAI,KAAK,UAAU,KAAK;AAC9B,MAAE,YAAY,CAAC;AACf,WAAO,EAAE,GAAG,QAAQ,EAAE;AAAA,EACxB;AAEA,QAAM,WAA0B,CAAC;AACjC,MAAI,UAAU,MAAM;AACpB,MAAI,aAAa;AACjB,aAAW,QAAQ,MAAM,KAAK,KAAK,UAAU,GAAG;AAC9C,QAAI,gBAAgB,eAAe,KAAK,UAAU,SAAS,gBAAgB,GAAG;AAC5E,UAAI,YAAY;AACd,iBAAS,KAAK,QAAQ,CAAC;AACvB,kBAAU,MAAM;AAChB,qBAAa;AAAA,MACf;AACA;AAAA,IACF;AACA,QAAI,KAAK,aAAa,MAAM,KAAK,eAAe,IAAI,KAAK,EAAG,cAAa;AACzE,YAAQ,OAAO,YAAY,IAAI;AAAA,EACjC;AACA,MAAI,cAAc,SAAS,WAAW,EAAG,UAAS,KAAK,QAAQ,CAAC;AAChE,SAAO;AACT;","names":["window"]}
|
package/dist/pdf.js
CHANGED
|
@@ -43,6 +43,13 @@ function flattenFlips3d(doc, root) {
|
|
|
43
43
|
stacks.set(parent, list);
|
|
44
44
|
}
|
|
45
45
|
for (const [stack, stackFaces] of stacks) {
|
|
46
|
+
for (const box of [stack, stack.parentElement]) {
|
|
47
|
+
if (!box || box === root) continue;
|
|
48
|
+
const height = win.getComputedStyle(box).height;
|
|
49
|
+
if (height.endsWith("px")) box.style.setProperty("min-height", height);
|
|
50
|
+
box.style.setProperty("height", "auto");
|
|
51
|
+
box.style.setProperty("max-height", "none");
|
|
52
|
+
}
|
|
46
53
|
if (stackFaces.length > 1) {
|
|
47
54
|
stack.style.setProperty("display", "grid");
|
|
48
55
|
stack.style.setProperty("grid-template-columns", `repeat(${stackFaces.length}, minmax(0, 1fr))`);
|
|
@@ -51,6 +58,8 @@ function flattenFlips3d(doc, root) {
|
|
|
51
58
|
for (const face of stackFaces) {
|
|
52
59
|
face.style.setProperty("position", "relative");
|
|
53
60
|
face.style.setProperty("inset", "auto");
|
|
61
|
+
face.style.setProperty("height", "auto");
|
|
62
|
+
face.style.setProperty("overflow", "visible");
|
|
54
63
|
face.style.setProperty("order", facing.get(face) ? "0" : "1");
|
|
55
64
|
}
|
|
56
65
|
}
|
package/dist/pdf.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/pdf.ts","../src/directives.ts","../src/flatten-3d.ts"],"sourcesContent":["/**\n * DOM-Capture Print Utilities — Tier 2 Full-Message Print\n *\n * Captures a rendered DOM element using html2canvas, then pipes it into jsPDF.\n * This handles ALL block types because it screenshots the actual rendered output.\n *\n * Problem: Tailwind CSS 4 uses modern CSS color functions (oklch, lab, color(display-p3))\n * that html2canvas cannot parse. It calls console.error for each one, which gets\n * captured by AdminDebugContextCollector and shows up as false errors.\n *\n * Solution: Patch window.getComputedStyle during the capture so html2canvas always\n * receives safe hex fallbacks instead of unparseable color function strings.\n *\n * ---\n *\n * This module is the packaged union of three source files that always shipped\n * together:\n * • dom-capture-print-utils — the capture/PDF renderers (above),\n * • dom-capture-block-printer — the BlockPrinter factory + captureBlockElement,\n * • markdown-pdf — markdownToPdfBlob.\n *\n * Package-level seams (the only deliberate departures from the originals):\n * • `markdownToPdfBlob` takes its markdown→HTML converter and its stylesheet\n * loader INJECTED (`MarkdownPdfDeps`) instead of dynamically importing app\n * modules. The package cannot reach into an application's feature folders.\n * • `sanitizeForCapture` is a package-added export (it was module-private in\n * the original) so the sanitizer can be tested on its own.\n * • `captureBlockElement` calls `captureToPDF` directly rather than through a\n * dynamic import — html2canvas and jspdf are still lazily imported inside\n * the render functions, which is the laziness that actually matters.\n *\n * html2canvas and jspdf stay behind call-time dynamic imports: importing this\n * module pulls in neither, and the module touches `window`/`document` only when\n * one of its functions is called.\n */\n\nimport { copyImage } from \"@ai-matrx/kit/clipboard\";\nimport type { BlockPrinter } from \"./core\";\nimport type { MarkdownSkin } from \"./markdown\";\nimport { PAGE_BREAK_CLASS } from \"./directives\";\nimport { flattenFlips3d } from \"./flatten-3d\";\n\n/** Type-only view of the lazily imported defaults module. */\ntype MarkdownModule = typeof import(\"./markdown\");\n\nexport interface DomCaptureOptions {\n /** Paper size — 'a4' (default) or 'letter' */\n paperSize?: \"a4\" | \"letter\";\n /** Orientation */\n orientation?: \"portrait\" | \"landscape\";\n /** Scale factor for html2canvas (higher = sharper, slower). Default: 2 */\n scale?: number;\n /** Background colour for the capture. Default: '#ffffff' */\n background?: string;\n /** Filename for the downloaded PDF (without extension). Default: 'ai-response' */\n filename?: string;\n /** Image encoding inside the PDF. PNG is lossless; JPEG is much smaller for reports. */\n imageFormat?: \"png\" | \"jpeg\";\n /** JPEG quality from 0 to 1. Ignored for PNG. Default: 0.92. */\n imageQuality?: number;\n /** Called after each page is captured, with current page / total estimate */\n onProgress?: (page: number, total: number) => void;\n /**\n * Color-fallback theme for unparseable (oklch/lab) computed colors.\n * \"auto\" (default) follows the app's dark class — right for capturing\n * on-screen UI. Pass \"light\" for always-light offscreen document renders\n * so dark-mode fallbacks never bleed into a white-background PDF.\n */\n theme?: \"auto\" | \"light\";\n}\n\nconst PAGE_DIMS = {\n a4: { w: 210, h: 297 },\n letter: { w: 215.9, h: 279.4 },\n} as const;\n\n// Matches any modern CSS color function html2canvas cannot parse\nconst UNSAFE_COLOR_RE = /\\b(oklch|oklab|lab|lch|color)\\s*\\(/i;\n\nfunction isSafeColor(value: string): boolean {\n return !UNSAFE_COLOR_RE.test(value);\n}\n\n/**\n * Replaces modern CSS color functions in a single value string with a\n * context-appropriate hex fallback.\n */\nfunction safenColor(value: string, prop: string, isDark: boolean): string {\n if (isSafeColor(value)) return value;\n const lp = prop.toLowerCase();\n const fallback = lp.includes(\"background\")\n ? isDark ? \"#1f2937\" : \"#ffffff\"\n : lp === \"color\"\n ? isDark ? \"#f3f4f6\" : \"#111827\"\n : lp.includes(\"border\") || lp.includes(\"outline\")\n ? isDark ? \"#4b5563\" : \"#d1d5db\"\n : lp === \"fill\"\n ? isDark ? \"#9ca3af\" : \"#374151\"\n : \"#6b7280\";\n // Replace each colour FUNCTION in place, never the whole value: a gradient\n // (`background-image: linear-gradient(oklch(…), …)`) answered as a bare hex\n // is a value html2canvas cannot parse as an image (\"Unsupported image type 5\"\n // — Full Print died on every answer that carried one, 2026-10-08).\n return value.replace(UNSAFE_COLOR_FN_RE, fallback);\n}\n\n/** One modern colour function call, one level of nested parentheses allowed (`oklch(from var(--x) l c h)`). */\nconst UNSAFE_COLOR_FN_RE = /\\b(?:oklch|oklab|lab|lch|color)\\s*\\((?:[^()]|\\([^()]*\\))*\\)/gi;\n\n/**\n * Patch window.getComputedStyle so that any call during the capture returns\n * safe hex values instead of oklch/lab/etc.\n *\n * html2canvas calls getComputedStyle extensively — this is the authoritative\n * intercept point. Returns a restore function.\n */\nfunction patchGetComputedStyle(isDark: boolean, views: readonly Window[] = [window]): () => void {\n const restores = [...new Set(views)].map((view) => patchOneWindow(view, isDark));\n return () => restores.forEach((restore) => restore());\n}\n\nfunction patchOneWindow(window: Window, isDark: boolean): () => void {\n // Two references on purpose. `original` is the UNBOUND function that was\n // installed before us and is what the restore puts back — restoring a bound\n // wrapper instead would leave one more `.bind` layer installed after every\n // capture, so the real function identity would never come back. `callOriginal`\n // is the bound form we invoke, because getComputedStyle needs `window` as its\n // receiver.\n const original = window.getComputedStyle;\n const callOriginal = original.bind(window);\n\n window.getComputedStyle = function patchedGetComputedStyle(\n elt: Element,\n pseudoElt?: string | null,\n ): CSSStyleDeclaration {\n const computed = callOriginal(elt, pseudoElt);\n\n return new Proxy(computed, {\n get(target, prop) {\n // CSSStyleDeclaration accessors require the native object as their\n // receiver. Using the Proxy as the receiver throws \"Illegal\n // invocation\" while html2canvas clones inline styles.\n const value: unknown = Reflect.get(target, prop, target);\n\n // Intercept string property accesses (color values)\n if (typeof prop === \"string\" && typeof value === \"string\" && value) {\n if (!isSafeColor(value)) {\n return safenColor(value, prop, isDark);\n }\n }\n\n // Intercept getPropertyValue\n if (prop === \"getPropertyValue\") {\n return (propName: string) => {\n const raw = target.getPropertyValue(propName);\n if (raw && !isSafeColor(raw)) {\n return safenColor(raw, propName, isDark);\n }\n return raw;\n };\n }\n\n if (typeof value === \"function\") {\n const fn = value as (...args: unknown[]) => unknown;\n return fn.bind(target);\n }\n\n return value;\n },\n });\n };\n\n return () => {\n window.getComputedStyle = original;\n };\n}\n\n/**\n * Capture a DOM element and export it as a multi-page PDF download.\n */\nexport async function captureToPDF(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n): Promise<void> {\n const pdf = await renderElementToPdf(element, options);\n pdf.save(`${options.filename ?? \"ai-response\"}.pdf`);\n}\n\n/**\n * Capture a DOM element and return the multi-page PDF as a Blob — for\n * callers that upload/persist the PDF instead of downloading it (e.g. the\n * chat \"Save as → PDF Document\" action saving into the user's Files).\n */\nexport async function captureToPDFBlob(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n): Promise<Blob> {\n const pdf = await renderElementToPdf(element, options);\n return pdf.output(\"blob\");\n}\n\n/**\n * Capture deliberately paginated DOM elements as one PDF page each.\n *\n * Use this for designed reports whose page boundaries are part of the layout.\n * The ordinary capture helpers intentionally slice one continuous canvas; this\n * variant preserves explicit report pages and therefore cannot cut a section\n * in half at an arbitrary paper boundary.\n */\nexport async function captureElementsToPDF(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n): Promise<void> {\n const pdf = await renderElementsToPdf(elements, options);\n pdf.save(`${options.filename ?? \"ai-response\"}.pdf`);\n}\n\n/** Blob form of captureElementsToPDF for durable file-system uploads. */\nexport async function captureElementsToPDFBlob(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n): Promise<Blob> {\n const pdf = await renderElementsToPdf(elements, options);\n return pdf.output(\"blob\");\n}\n\nasync function renderElementsToPdf(\n elements: HTMLElement[],\n options: DomCaptureOptions,\n) {\n if (elements.length === 0) {\n throw new Error(\"No report pages are available to export.\");\n }\n\n const {\n paperSize = \"a4\",\n orientation = \"portrait\",\n scale = 2,\n background = \"#ffffff\",\n imageFormat = \"png\",\n imageQuality = 0.92,\n onProgress,\n } = options;\n const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([\n import(\"html2canvas\"),\n import(\"jspdf\"),\n ]);\n const dims = PAGE_DIMS[paperSize];\n const pageW = orientation === \"portrait\" ? dims.w : dims.h;\n const pageH = orientation === \"portrait\" ? dims.h : dims.w;\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n const restoreGetComputedStyle = patchGetComputedStyle(false, [window, ...elements.map((e) => e.ownerDocument.defaultView ?? window)]);\n\n try {\n for (const [index, element] of elements.entries()) {\n onProgress?.(index, elements.length);\n const canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: background,\n logging: false,\n windowWidth: element.scrollWidth,\n windowHeight: element.scrollHeight,\n onclone: (clonedDoc, clonedEl) => flattenFlips3d(clonedDoc, clonedEl),\n });\n if (index > 0) pdf.addPage();\n const isJpeg = imageFormat === \"jpeg\";\n pdf.addImage(\n canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n ),\n isJpeg ? \"JPEG\" : \"PNG\",\n 0,\n 0,\n pageW,\n pageH,\n );\n onProgress?.(index + 1, elements.length);\n }\n } finally {\n restoreGetComputedStyle();\n }\n\n return pdf;\n}\n\nasync function renderElementToPdf(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n) {\n return renderSectionsToPdf([element], options);\n}\n\n/**\n * Capture one or more continuous sections into one PDF. Each section is\n * sliced across as many pages as it needs, and every section after the first\n * STARTS ON A NEW PAGE — that is how a markdown page break survives the\n * canvas lane, where CSS `break-after` means nothing.\n */\nasync function renderSectionsToPdf(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n) {\n const {\n paperSize = \"a4\",\n orientation = \"portrait\",\n scale = 2,\n background = \"#ffffff\",\n imageFormat = \"png\",\n imageQuality = 0.92,\n onProgress,\n } = options;\n\n const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([\n import(\"html2canvas\"),\n import(\"jspdf\"),\n ]);\n\n const dims = PAGE_DIMS[paperSize];\n const pageW = orientation === \"portrait\" ? dims.w : dims.h;\n const pageH = orientation === \"portrait\" ? dims.h : dims.w;\n\n onProgress?.(0, 1);\n\n const isDark =\n options.theme === \"light\"\n ? false\n : document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n const bgColor = background === \"#ffffff\" && isDark ? \"#111827\" : background;\n\n // The element may live in its own frame (a message copied alone for capture): patch that window too.\n const restoreGetComputedStyle = patchGetComputedStyle(isDark, [window, ...elements.map((e) => e.ownerDocument.defaultView ?? window)]);\n\n // ONE PAGE AT A TIME. Each section is drawn in page-tall windows, never as\n // one canvas the height of the whole section: a long answer at scale 2 is a\n // canvas past what a browser will encode, `toDataURL` answers \"data:,\" and\n // jsPDF throws \"Unsupported image type\" — Full Print of a 25-page answer\n // never produced a file (2026-10-08). A window per page also stops every\n // page from embedding the whole section's image.\n const isJpeg = imageFormat === \"jpeg\";\n const imgFormat = isJpeg ? \"JPEG\" : \"PNG\";\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n\n // Half a millimetre of tolerance: a section that is an exact multiple of the\n // page height must not grow a blank trailing page from float rounding.\n const EPSILON_MM = 0.5;\n const sections = elements.map((element) => {\n const cssW = Math.max(1, element.scrollWidth);\n const cssH = Math.max(1, element.scrollHeight);\n const pagePx = (cssW * pageH) / pageW;\n const pages = Math.max(1, Math.ceil(((cssH * pageW) / cssW - EPSILON_MM) / pageH));\n return { element, cssW, cssH, pagePx, pages };\n });\n const totalPages = sections.reduce((a, s) => a + s.pages, 0);\n let page = 0;\n\n try {\n for (const { element, cssW, cssH, pagePx, pages } of sections) {\n const view = element.ownerDocument.defaultView ?? window;\n const box = element.getBoundingClientRect();\n const left = box.left + view.scrollX;\n const top = box.top + view.scrollY;\n for (let index = 0; index < pages; index += 1) {\n const offset = index * pagePx;\n const height = Math.max(1, Math.min(pagePx, cssH - offset));\n const canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: bgColor,\n logging: false,\n windowWidth: cssW,\n windowHeight: cssH,\n x: left,\n y: top + offset,\n width: cssW,\n height,\n onclone: (clonedDoc, clonedEl) => flattenFlips3d(clonedDoc, clonedEl),\n });\n const imgData = canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n );\n if (!imgData.startsWith(\"data:image/\")) {\n throw new Error(`Page ${page + 1} could not be drawn (the browser refused a ${canvas.width}×${canvas.height} image).`);\n }\n if (page > 0) pdf.addPage();\n pdf.addImage(imgData, imgFormat, 0, 0, pageW, (height * pageW) / cssW);\n page += 1;\n onProgress?.(page, totalPages);\n }\n }\n } finally {\n restoreGetComputedStyle();\n }\n\n return pdf;\n}\n\n/**\n * Capture a DOM element and copy it to the clipboard as a PNG, through THE one image door (kit\n * `copyImage`). `true` only when the image is on the clipboard — gate any \"Copied\" UI on it. A\n * refused copy answers `false` and changes nothing (it used to download `capture.png` silently and\n * resolve as if it had copied); the caller says so and may offer a download itself.\n */\nexport async function captureToClipboardImage(\n element: HTMLElement,\n): Promise<boolean> {\n const isDark =\n document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n const canvas = await captureCanvas(element, {\n scale: 2,\n backgroundColor: isDark ? \"#111827\" : \"#ffffff\",\n safeColors: { dark: isDark },\n });\n const blob = await new Promise<Blob | null>((resolve) =>\n canvas.toBlob(resolve, \"image/png\"),\n );\n if (!blob) {\n console.error(\"Failed to copy image: the capture could not be encoded\");\n return false;\n }\n return copyImage(blob);\n}\n\n// ─── THE one DOM-capture + canvas-to-PDF engine (AP-2 engine ruling) ─────────\n// Print sits BELOW alchemy, so the one html2canvas + jsPDF engine lives here and alchemy's\n// `/operate/capture` (`captureElement`, `pagesToPdf`, `elementsToPdf`) loads these lazily.\n\n/** Options for `captureCanvas` — html2canvas's, plus the colour-safety patch. */\nexport type CanvasCaptureOptions = {\n /** Device-pixel scale of the capture (default 2 — legible when scaled in a deck or print). */\n scale?: number;\n /** Canvas background; null keeps transparency. */\n backgroundColor?: string | null;\n useCORS?: boolean;\n allowTaint?: boolean;\n logging?: boolean;\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n scrollX?: number;\n scrollY?: number;\n windowWidth?: number;\n windowHeight?: number;\n ignoreElements?(element: Element): boolean;\n onclone?(document: Document, element: HTMLElement): void | Promise<void>;\n /** Replace unparseable modern colour functions while capturing (`{ dark }` picks the palette; `true` follows the page). */\n safeColors?: boolean | { dark: boolean };\n};\n\nconst prefersDark = () =>\n typeof document !== \"undefined\" &&\n (document.documentElement.classList.contains(\"dark\") ||\n (typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches));\n\n/** Rasterise a live element to a canvas (what the person sees, at `scale`). html2canvas loads on first use. */\nexport async function captureCanvas(\n element: HTMLElement,\n options: CanvasCaptureOptions = {},\n): Promise<HTMLCanvasElement> {\n const { safeColors, ...rest } = options;\n const { default: html2canvas } = await import(\"html2canvas\");\n const restore = safeColors\n ? patchGetComputedStyle(\n typeof safeColors === \"object\" ? safeColors.dark : prefersDark(),\n )\n : () => {};\n try {\n const callerClone = rest.onclone;\n return await html2canvas(element, {\n scale: 2,\n logging: false,\n useCORS: true,\n ...rest,\n onclone: async (clonedDoc: Document, clonedEl: HTMLElement) => {\n flattenFlips3d(clonedDoc, clonedEl);\n await callerClone?.(clonedDoc, clonedEl);\n },\n } as Parameters<typeof html2canvas>[1]);\n } finally {\n restore();\n }\n}\n\nexport type PdfMargins = { top: number; right: number; bottom: number; left: number };\nexport type PdfPage = {\n image: HTMLCanvasElement;\n /** \"JPEG\" (default 0.92 quality) keeps a flat chart an order of magnitude smaller than a PNG embed. */\n imageType?: \"PNG\" | \"JPEG\";\n quality?: number;\n compression?: \"NONE\" | \"FAST\" | \"MEDIUM\" | \"SLOW\";\n /** \"fill\": the image covers the page. Margins: the image is scaled to fit inside them, never cropped. */\n placement?: \"fill\" | PdfMargins;\n /** A line of text drawn on the page (e.g. the as-of date a chart is true of). */\n header?: { text: string; x: number; y: number; fontSize?: number };\n};\nexport type PdfDocumentOptions = {\n orientation?: \"portrait\" | \"landscape\";\n unit?: \"pt\" | \"mm\" | \"cm\" | \"in\" | \"px\";\n format?: string | [number, number];\n};\n\n/** Lay canvases out as PDF pages (one page each) and answer the PDF Blob. jsPDF loads on first use. */\nexport async function pagesToPdf(\n pages: readonly PdfPage[],\n options: PdfDocumentOptions = {},\n): Promise<Blob> {\n const { jsPDF } = await import(\"jspdf\");\n const doc = new jsPDF({\n orientation: options.orientation ?? \"portrait\",\n unit: options.unit ?? \"pt\",\n format: options.format ?? \"a4\",\n });\n pages.forEach((page, index) => {\n if (index > 0) doc.addPage();\n const pageW = doc.internal.pageSize.getWidth();\n const pageH = doc.internal.pageSize.getHeight();\n if (page.header) {\n doc.setFontSize(page.header.fontSize ?? 12);\n doc.text(page.header.text, page.header.x, page.header.y);\n }\n const type = page.imageType ?? \"PNG\";\n const data =\n type === \"JPEG\"\n ? page.image.toDataURL(\"image/jpeg\", page.quality ?? 0.92)\n : page.image.toDataURL(\"image/png\");\n const placement = page.placement ?? \"fill\";\n let x = 0,\n y = 0,\n w = pageW,\n h = pageH;\n if (placement !== \"fill\") {\n const availW = pageW - placement.left - placement.right;\n const availH = pageH - placement.top - placement.bottom;\n const scale = Math.min(availW / page.image.width, availH / page.image.height);\n x = placement.left;\n y = placement.top;\n w = page.image.width * scale;\n h = page.image.height * scale;\n }\n if (page.compression) doc.addImage(data, type, x, y, w, h, undefined, page.compression);\n else doc.addImage(data, type, x, y, w, h);\n });\n return doc.output(\"blob\");\n}\n\n/**\n * DOM-Capture BlockPrinter factory\n *\n * Creates a BlockPrinter that captures a provided DOM element and exports it as PDF.\n * Used by visual blocks (Timeline, ComparisonTable, DecisionTree, Diagram) where\n * the rendered DOM is the most faithful representation.\n *\n * Usage inside a block component:\n * const { captureRef, printAction } = useBlockDomCapture('Timeline');\n * // attach captureRef to the block's root element\n * // add printAction to customActions or controls\n */\n\n/**\n * Create a BlockPrinter backed by DOM capture.\n * The printer's `print(data)` call is a no-op — actual printing is triggered\n * imperatively via `captureElement(el)`.\n *\n * This exists so the block can register itself in the BlockPrinter contract\n * while keeping the DOM reference local to the component.\n */\nexport function createDomCapturePrinter(blockLabel: string): BlockPrinter {\n return {\n label: `Print ${blockLabel}`,\n variants: [],\n // No-op: DOM capture blocks use captureElement() directly\n print: () => {},\n };\n}\n\n/**\n * Capture a DOM element as PDF.\n * Called directly by block components when their print button is clicked.\n *\n * @param element - The DOM element to capture\n * @param filename - Output filename (without extension)\n * @param orientation - \"landscape\" (default) for wide visual blocks; \"portrait\" for tall/list content\n */\nexport async function captureBlockElement(\n element: HTMLElement,\n filename: string,\n orientation: \"landscape\" | \"portrait\" = \"landscape\",\n): Promise<void> {\n await captureToPDF(element, {\n filename,\n scale: 2,\n orientation,\n });\n}\n\n/**\n * markdownToPdfBlob — render a markdown string to a styled, multi-page PDF\n * Blob without any visible UI.\n *\n * Pipeline: markdown → WordPress-styled HTML (the same converter the\n * publish/copy-HTML flows use) → sanitized offscreen DOM host → html2canvas +\n * jsPDF via `captureToPDFBlob`. Everything heavy stays lazy-loaded behind\n * this module.\n *\n * Used by the chat \"Save as → PDF Document\" action, which uploads the blob\n * into the user's Files. Reuse this for any \"content → PDF file\" need — do\n * not fork another markdown-to-pdf path.\n */\n\n/**\n * OVERRIDES for markdownToPdfBlob — every field is optional.\n *\n * Both ports ship a working default in `@ai-matrx/print/markdown`\n * (`markdownToHtml` + `getMarkdownStylesheet`), so `markdownToPdfBlob(md)`\n * alone produces a styled PDF in a fresh app. Supplying a field replaces that\n * default; supplying neither is the normal case. Until 0.3.0 both fields were\n * REQUIRED, which forced every host to build the hard half itself.\n */\nexport interface MarkdownPdfDeps {\n /** markdown → HTML body string. Default: the package's `markdownToHtml`. */\n convertToHtml?: (markdown: string) => string;\n /** the stylesheet the rendered HTML expects. Default: the \"document\" skin. */\n loadCss?: () => Promise<string> | string;\n /** Which shipped look to use when `convertToHtml`/`loadCss` are not given. */\n skin?: MarkdownSkin;\n /** Host brand token overrides appended after the package's default values. */\n tokens?: string;\n /**\n * Where degraded conversion and unreachable images are announced. Nothing\n * fails silently: without a sink these still surface as `console.warn`.\n */\n onNotice?: (notice: MarkdownPdfNotice) => void;\n /**\n * How long to wait for images to decode before capturing, in ms.\n * Default 8000. html2canvas paints whatever has loaded, so an un-awaited\n * image is a blank rectangle in the PDF with no error anywhere.\n */\n imageTimeoutMs?: number;\n}\n\n/** A degradation the PDF lane detected, with the remedy. Never swallowed. */\nexport interface MarkdownPdfNotice {\n code:\n | \"flashcard-malformed\"\n | \"empty-output\"\n | \"html-stripped\"\n | \"image-unreachable\"\n | \"image-timeout\";\n message: string;\n remedy: string;\n}\n\n/**\n * Markdown can carry raw HTML; we are about to attach it to the live\n * document, so strip anything executable. html2canvas ignores scripts, but\n * inline handlers (`onerror`) and javascript: URLs would fire on attach.\n */\nexport function sanitizeForCapture(html: string): DocumentFragment {\n const template = document.createElement(\"template\");\n template.innerHTML = html;\n const fragment = template.content;\n\n fragment\n .querySelectorAll(\"script, iframe, object, embed, link, meta\")\n .forEach((el) => el.remove());\n fragment.querySelectorAll(\"*\").forEach((el) => {\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase();\n if (name.startsWith(\"on\")) el.removeAttribute(attr.name);\n if (\n (name === \"href\" || name === \"src\") &&\n attr.value.trim().toLowerCase().startsWith(\"javascript:\")\n ) {\n el.removeAttribute(attr.name);\n }\n }\n });\n return fragment;\n}\n\n/**\n * Wait for every `<img>` inside a host to finish decoding, and REPORT the ones\n * that never will.\n *\n * html2canvas captures whatever the browser has painted: an image still in\n * flight becomes a blank rectangle and an image that 404s becomes a blank\n * rectangle, both without a single error anywhere. This is the difference\n * between a PDF that is missing a figure and a PDF that is missing a figure\n * AND says so.\n */\nasync function settleImages(\n host: HTMLElement,\n timeoutMs: number,\n notify: (notice: MarkdownPdfNotice) => void,\n): Promise<void> {\n const images = Array.from(host.querySelectorAll(\"img\"));\n if (images.length === 0) return;\n\n const timedOut: string[] = [];\n await Promise.all(\n images.map(\n (img) =>\n new Promise<void>((resolve) => {\n if (img.complete) {\n if (img.naturalWidth === 0) {\n notify({\n code: \"image-unreachable\",\n message: `An image could not be loaded and will be blank in the PDF: ${img.src}`,\n remedy:\n \"Use a durable, publicly reachable https URL — a print render is an unauthenticated document and cannot use session-signed or expiring links.\",\n });\n }\n resolve();\n return;\n }\n const timer = setTimeout(() => {\n timedOut.push(img.src);\n resolve();\n }, timeoutMs);\n const done = (ok: boolean): void => {\n clearTimeout(timer);\n if (!ok) {\n notify({\n code: \"image-unreachable\",\n message: `An image could not be loaded and will be blank in the PDF: ${img.src}`,\n remedy:\n \"Use a durable, publicly reachable https URL — a print render is an unauthenticated document and cannot use session-signed or expiring links.\",\n });\n }\n resolve();\n };\n img.addEventListener(\"load\", () => done(true), { once: true });\n img.addEventListener(\"error\", () => done(false), { once: true });\n }),\n ),\n );\n\n if (timedOut.length > 0) {\n notify({\n code: \"image-timeout\",\n message: `${timedOut.length} image(s) did not finish loading in ${timeoutMs}ms and may be blank in the PDF: ${timedOut.join(\", \")}`,\n remedy:\n \"Raise `imageTimeoutMs`, or serve the images from a faster/CDN origin before exporting.\",\n });\n }\n}\n\nexport async function markdownToPdfBlob(\n markdown: string,\n deps: MarkdownPdfDeps = {},\n): Promise<Blob> {\n if (!markdown.trim()) {\n throw new Error(\"Nothing to convert — the content is empty.\");\n }\n\n // A caller with no sink still gets told. Silence is not an option the API\n // offers, because a silently degraded PDF looks exactly like a correct one.\n const notify = (notice: MarkdownPdfNotice): void => {\n if (deps.onNotice) {\n deps.onNotice(notice);\n return;\n }\n console.warn(`[@ai-matrx/print] ${notice.message} → ${notice.remedy}`);\n };\n\n // The defaults live behind a lazy import: a consumer that injects its own\n // converter and stylesheet never pulls the ~20KB of package CSS into its\n // bundle, and neither does one that imports /pdf for DOM capture only.\n //\n // The specifier is the package's OWN public name, not a relative path: tsup\n // runs with `splitting: false`, so `import(\"./markdown\")` gets inlined into\n // this chunk and the lazy edge silently disappears (measured — dist/pdf.js\n // grew by the full stylesheet). A self-reference is external to the bundle\n // and resolves through the export map at runtime. Same pattern as\n // @ai-matrx/associations `./react/lazy`.\n const needsDefaults = !deps.convertToHtml || !deps.loadCss;\n const markdownModule = needsDefaults\n ? ((await import(\"@ai-matrx/print/markdown\")) as MarkdownModule)\n : undefined;\n\n const bodyHtml = deps.convertToHtml\n ? deps.convertToHtml(markdown)\n : (markdownModule as MarkdownModule).markdownToHtmlForSkin(\n markdown,\n deps.skin ?? \"document\",\n { onNotice: notify },\n );\n\n const css = deps.loadCss\n ? await deps.loadCss()\n : (markdownModule as MarkdownModule).getMarkdownStylesheet({\n skin: deps.skin ?? \"document\",\n ...(deps.tokens === undefined ? {} : { tokens: deps.tokens }),\n });\n\n if (!bodyHtml.trim()) {\n throw new Error(\n \"The markdown converted to an empty document — nothing would be printed. \" +\n \"Check the content is more than whitespace or reasoning blocks.\",\n );\n }\n\n // Offscreen host — must be in the document for html2canvas to compute\n // styles, but never visible and never intercepting input.\n const host = document.createElement(\"div\");\n host.style.cssText =\n \"position:fixed;left:-10000px;top:0;width:800px;background:#ffffff;color:#111827;pointer-events:none;\";\n host.setAttribute(\"aria-hidden\", \"true\");\n\n const style = document.createElement(\"style\");\n style.textContent = css;\n host.appendChild(style);\n\n const contentEl = document.createElement(\"div\");\n // The document stylesheet is class-scoped (.matrx-*) and expects this\n // container; the article sheet styles bare tags under `.content`.\n contentEl.className =\n (deps.skin ?? \"document\") === \"article\" ? \"content\" : \"matrx-content-container\";\n contentEl.style.cssText = \"padding:32px;background:#ffffff;\";\n contentEl.appendChild(sanitizeForCapture(bodyHtml));\n host.appendChild(contentEl);\n\n const hosts = splitHostAtPageBreaks(host, contentEl);\n for (const h of hosts) document.body.appendChild(h);\n try {\n for (const h of hosts) await settleImages(h, deps.imageTimeoutMs ?? 8000, notify);\n // Always-light document render: \"#fff\" (not \"#ffffff\") bypasses the\n // dark-mode background flip, and theme:\"light\" keeps the oklch-fallback\n // patch from substituting dark text colors while the app is in dark mode.\n const pdf = await renderSectionsToPdf(hosts, { background: \"#fff\", theme: \"light\" });\n return pdf.output(\"blob\");\n } catch (error) {\n // The raw html2canvas/jsPDF failure is meaningless to a user and to a host\n // author. Name the stage and the remedy here so no host writes a catch\n // block to reinterpret it (C22).\n throw new Error(\n `The PDF could not be rendered from this content: ${\n error instanceof Error ? error.message : String(error)\n }. Try again; if it repeats, the content likely contains an element the ` +\n \"renderer cannot rasterize (a cross-origin image without CORS headers, \" +\n \"or an embedded iframe/canvas).\",\n error instanceof Error ? { cause: error } : undefined,\n );\n } finally {\n for (const h of hosts) h.remove();\n }\n}\n\n/**\n * Split an offscreen capture host at its top-level page breaks into one host\n * per printed section (same stylesheet, same container shells), so each\n * section starts on a fresh PDF page. No breaks → the host itself.\n */\nfunction splitHostAtPageBreaks(host: HTMLElement, contentEl: HTMLElement): HTMLElement[] {\n const inner = contentEl.firstElementChild;\n const flow =\n inner instanceof HTMLElement && inner.classList.contains(\"matrx-content-container\")\n ? inner\n : contentEl;\n const hasBreak = Array.from(flow.children).some((c) => c.classList.contains(PAGE_BREAK_CLASS));\n if (!hasBreak) return [host];\n\n const style = host.querySelector(\"style\");\n const shell = (): { h: HTMLElement; target: HTMLElement } => {\n const h = host.cloneNode(false) as HTMLElement;\n if (style) h.appendChild(style.cloneNode(true));\n const c = contentEl.cloneNode(false) as HTMLElement;\n h.appendChild(c);\n if (flow === contentEl) return { h, target: c };\n const f = flow.cloneNode(false) as HTMLElement;\n c.appendChild(f);\n return { h, target: f };\n };\n\n const sections: HTMLElement[] = [];\n let current = shell();\n let hasContent = false;\n for (const node of Array.from(flow.childNodes)) {\n if (node instanceof HTMLElement && node.classList.contains(PAGE_BREAK_CLASS)) {\n if (hasContent) {\n sections.push(current.h);\n current = shell();\n hasContent = false;\n }\n continue;\n }\n if (node.nodeType === 1 || (node.textContent ?? \"\").trim()) hasContent = true;\n current.target.appendChild(node);\n }\n if (hasContent || sections.length === 0) sections.push(current.h);\n return sections;\n}\n","/**\n * @ai-matrx/print/directives — the ONE definition of the print directives\n * markdown can carry, shared by every surface that reads markdown.\n *\n * Why a separate, zero-dependency entry: the directive syntax has three\n * readers — this package's two converters (print window / PDF / HTML page),\n * the chat renderer's remark plugin (the on-screen preview), and any editor\n * that inserts one. If each re-derived the regex they would drift the first\n * time someone added an alias. `/markdown` carries ~20KB of stylesheet, so the\n * chat bundle imports the syntax from here instead and pays nothing.\n *\n * ── Page break ────────────────────────────────────────────────────────────\n * A line that is exactly one of these (case-insensitive, surrounding\n * whitespace ignored) forces the next content onto a new printed page:\n *\n * <!-- pagebreak --> ← canonical; invisible in any other renderer\n * <!-- page-break --> <!-- page break --> <!-- newpage -->\n * \\pagebreak \\newpage ← LaTeX / pandoc habit\n * <div style=\"page-break-after: always\"></div> ← Typora / VS Code habit\n * (also `page-break-before`, `break-after: page`, `break-before: page`,\n * and `<div class=\"page-break\"></div>` / `class=\"pagebreak\"`)\n *\n * On screen every surface draws the same idea: a dashed rule labelled\n * \"Page break\" with generous padding. In print it is a zero-height element\n * with `break-after: page`.\n */\n\n/** The form editors insert and docs recommend. */\nexport const PAGE_BREAK_MARKDOWN = \"<!-- pagebreak -->\";\n\n/** The class every surface puts on a rendered page break. */\nexport const PAGE_BREAK_CLASS = \"matrx-page-break\";\n\n/** Screen label for a rendered page break. */\nexport const PAGE_BREAK_LABEL = \"Page break\";\n\nconst COMMENT_FORM = /^<!--\\s*(?:page[\\s_-]?break|new[\\s_-]?page)\\s*-->$/i;\nconst LATEX_FORM = /^\\\\(?:pagebreak|newpage|clearpage)\\s*(?:\\{\\s*\\})?$/i;\nconst DIV_FORM = /^<div\\b([^>]*)>\\s*<\\/div>$|^<div\\b([^>]*)\\/>$/i;\nconst DIV_BREAK_ATTR =\n /(?:page-break-(?:after|before)\\s*:\\s*always|break-(?:after|before)\\s*:\\s*page|class\\s*=\\s*[\"'](?:[^\"']*\\s)?page-?break(?:\\s[^\"']*)?[\"'])/i;\n\n/** Columns of leading indentation (tab = 4, NBSP counts as a space). */\nfunction indentColumns(line: string): number {\n let cols = 0;\n for (const ch of line) {\n if (ch === \" \" || ch === \"\\u00A0\") cols += 1;\n else if (ch === \"\\t\") cols += 4;\n else break;\n }\n return cols;\n}\n\n/**\n * Is this single line a page-break directive? Indented four or more columns\n * it is not — in markdown that is a code block, and every surface must agree.\n */\nexport function isPageBreakLine(line: string): boolean {\n if (indentColumns(line) >= 4) return false;\n const t = line.trim();\n if (t.length < 8 || t.length > 200) return false;\n if (COMMENT_FORM.test(t) || LATEX_FORM.test(t)) return true;\n const div = DIV_FORM.exec(t);\n if (!div) return false;\n return DIV_BREAK_ATTR.test(div[1] ?? div[2] ?? \"\");\n}\n\n// ── Section break ─────────────────────────────────────────────────────────\n//\n// A section break is a page break that can also change the page layout for\n// everything after it (Word's \"Section break (next page)\"):\n//\n// <!-- section --> ← new page, same layout\n// <!-- section: landscape --> ← new page, landscape from here on\n// <!-- section landscape columns=2 --> ← landscape AND two columns\n// <!-- section portrait columns=1 --> ← back to the default layout\n//\n// Recognised words: `landscape` / `portrait`, `columns=N` (or `cols=N`,\n// `N columns`, 1–3). Unknown words are ignored, never an error — the break\n// still happens. Every page-break reader treats a section break as a break.\n\n/** The form editors insert for a plain section break. */\nexport const SECTION_BREAK_MARKDOWN = \"<!-- section -->\";\n\nexport interface SectionBreakDirective {\n /** Absent = inherit the document's orientation. */\n orientation?: \"portrait\" | \"landscape\";\n /** Absent = inherit the document's column count. 1–3. */\n columns?: 1 | 2 | 3;\n}\n\nconst SECTION_FORM = /^<!--\\s*section(?:[\\s_-]?break)?\\b\\s*:?\\s*([^>]*?)\\s*-->$/i;\n\n/** Parse a section-break line; `null` when the line is not one. */\nexport function parseSectionBreakLine(line: string): SectionBreakDirective | null {\n if (indentColumns(line) >= 4) return null;\n const m = SECTION_FORM.exec(line.trim());\n if (!m) return null;\n const words = (m[1] ?? \"\").toLowerCase();\n const out: SectionBreakDirective = {};\n if (/\\blandscape\\b/.test(words)) out.orientation = \"landscape\";\n else if (/\\bportrait\\b/.test(words)) out.orientation = \"portrait\";\n const cols = /\\b(?:columns?|cols?)\\s*[=:]\\s*(\\d)\\b/.exec(words) ?? /\\b(\\d)\\s*[-\\s]?columns?\\b/.exec(words);\n if (cols) {\n const n = Math.min(3, Math.max(1, Number(cols[1])));\n out.columns = n as 1 | 2 | 3;\n }\n return out;\n}\n\n// ── Table of contents / bibliography placement ───────────────────────────\n//\n// `[[toc]]`, `[toc]`, `[TOC]`, `${toc}`, `<!-- toc -->`, `\\tableofcontents`\n// put the table of contents at that spot (otherwise frontmatter `toc: true`\n// puts it at the start). `<!-- bibliography -->`, `[[bibliography]]`,\n// `\\printbibliography` put the reference list there (otherwise it closes the\n// document whenever something is cited).\n\nexport const TOC_MARKDOWN = \"[[toc]]\";\nexport const BIBLIOGRAPHY_MARKDOWN = \"<!-- bibliography -->\";\n\nconst TOC_FORM = /^(?:\\[\\[\\s*toc\\s*\\]\\]|\\[toc\\]|\\$\\{toc\\}|<!--\\s*toc\\s*-->|\\\\tableofcontents)$/i;\nconst BIBLIOGRAPHY_FORM =\n /^(?:\\[\\[\\s*(?:bibliography|references)\\s*\\]\\]|<!--\\s*(?:bibliography|references)\\s*-->|\\\\printbibliography|\\\\bibliography\\{[^}]*\\})$/i;\n\nexport function isTocLine(line: string): boolean {\n return indentColumns(line) < 4 && TOC_FORM.test(line.trim());\n}\n\nexport function isBibliographyLine(line: string): boolean {\n return indentColumns(line) < 4 && BIBLIOGRAPHY_FORM.test(line.trim());\n}\n\n/** Any directive that ends the current printed page (page or section break). */\nexport function isBreakLine(line: string): boolean {\n return isPageBreakLine(line) || parseSectionBreakLine(line) !== null;\n}\n\n/**\n * Split markdown into the pieces that belong on separate pages. Fence-aware:\n * a directive inside a ``` / ~~~ code block is code, not a break. Always\n * returns at least one piece; empty pieces (two breaks in a row, a break at\n * the very start or end) are kept out so no blank page is produced.\n */\nexport function splitAtPageBreaks(markdown: string): string[] {\n const lines = markdown.split(/\\r?\\n/);\n const pieces: string[] = [];\n let current: string[] = [];\n let fence: string | null = null;\n\n for (const line of lines) {\n // Any indent — the converters accept list-nested fences, so must this.\n const fenceMatch = /^\\s*(`{3,}|~{3,})/.exec(line);\n if (fenceMatch) {\n const marker = fenceMatch[1] as string;\n if (fence === null) fence = marker;\n else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;\n } else if (fence === null && isBreakLine(line)) {\n if (current.join(\"\\n\").trim()) pieces.push(current.join(\"\\n\"));\n current = [];\n continue;\n }\n current.push(line);\n }\n if (current.join(\"\\n\").trim() || pieces.length === 0) pieces.push(current.join(\"\\n\"));\n return pieces;\n}\n","/**\n * Flatten CSS 3D flips for a canvas capture.\n *\n * html2canvas has no 3D: it ignores `backface-visibility` and `transform-style:\n * preserve-3d`, so a flip card (two faces stacked, the back turned with\n * `rotateY(180deg)`) is drawn with BOTH faces on top of each other and the back\n * face mirrored. This runs on the capture's cloned document and turns every such\n * stack into a flat row: the faces sit side by side, the face turned toward the\n * viewer first, every face un-mirrored and shown — so a printed card carries its\n * answer as well as its question. Plain 2D transforms are never touched.\n */\n\nconst IDENTITY = \"none\";\n\ntype FrameWindow = Window & typeof globalThis;\n\nfunction matrixOf(win: FrameWindow, cs: CSSStyleDeclaration): DOMMatrix {\n const t = cs.transform;\n if (!t || t === IDENTITY) return new win.DOMMatrix();\n try {\n return new win.DOMMatrix(t);\n } catch {\n return new win.DOMMatrix();\n }\n}\n\n/** True when the face, after every ancestor transform up to `root`, points toward the viewer. */\nfunction facesViewer(win: FrameWindow, face: HTMLElement, root: HTMLElement): boolean {\n let m = new win.DOMMatrix();\n for (let node: HTMLElement | null = face; node; node = node.parentElement) {\n m = matrixOf(win, win.getComputedStyle(node)).multiply(m);\n if (node === root) break;\n }\n // m33 is the z-component of the face normal after rotation: > 0 faces the viewer.\n return m.m33 >= 0;\n}\n\nfunction is3d(cs: CSSStyleDeclaration): boolean {\n return (\n cs.transformStyle === \"preserve-3d\" ||\n cs.backfaceVisibility === \"hidden\" ||\n cs.perspective !== \"none\" ||\n (cs.transform ?? \"\").startsWith(\"matrix3d\")\n );\n}\n\n/** Make every 3D flip in `root` html2canvas-safe. Idempotent; a no-op when there is no 3D. */\nexport function flattenFlips3d(doc: Document, root: HTMLElement): void {\n const win = doc.defaultView as FrameWindow | null;\n if (!win) return;\n const all = [root, ...Array.from(root.querySelectorAll<HTMLElement>(\"*\"))];\n const faces = all.filter((el) => win.getComputedStyle(el).backfaceVisibility === \"hidden\");\n if (faces.length === 0 && !all.some((el) => is3d(win.getComputedStyle(el)))) return;\n\n // Decide every face's facing BEFORE any style changes (it reads ancestor transforms).\n const facing = new Map<HTMLElement, boolean>();\n for (const face of faces) facing.set(face, facesViewer(win, face, root));\n\n // Faces grouped by the element that stacks them: that element becomes a flat row.\n const stacks = new Map<HTMLElement, HTMLElement[]>();\n for (const face of faces) {\n const parent = face.parentElement;\n if (!parent) continue;\n const list = stacks.get(parent) ?? [];\n list.push(face);\n stacks.set(parent, list);\n }\n\n for (const [stack, stackFaces] of stacks) {\n if (stackFaces.length > 1) {\n stack.style.setProperty(\"display\", \"grid\");\n stack.style.setProperty(\"grid-template-columns\", `repeat(${stackFaces.length}, minmax(0, 1fr))`);\n stack.style.setProperty(\"gap\", \"8px\");\n }\n for (const face of stackFaces) {\n face.style.setProperty(\"position\", \"relative\");\n face.style.setProperty(\"inset\", \"auto\");\n face.style.setProperty(\"order\", facing.get(face) ? \"0\" : \"1\");\n }\n }\n\n for (const el of all) {\n const cs = win.getComputedStyle(el);\n if (!is3d(cs)) continue;\n el.style.setProperty(\"transform\", \"none\");\n el.style.setProperty(\"transform-style\", \"flat\");\n el.style.setProperty(\"perspective\", \"none\");\n el.style.setProperty(\"backface-visibility\", \"visible\");\n }\n}\n"],"mappings":"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","names":["window"]}
|
|
1
|
+
{"version":3,"sources":["../src/pdf.ts","../src/directives.ts","../src/flatten-3d.ts"],"sourcesContent":["/**\n * DOM-Capture Print Utilities — Tier 2 Full-Message Print\n *\n * Captures a rendered DOM element using html2canvas, then pipes it into jsPDF.\n * This handles ALL block types because it screenshots the actual rendered output.\n *\n * Problem: Tailwind CSS 4 uses modern CSS color functions (oklch, lab, color(display-p3))\n * that html2canvas cannot parse. It calls console.error for each one, which gets\n * captured by AdminDebugContextCollector and shows up as false errors.\n *\n * Solution: Patch window.getComputedStyle during the capture so html2canvas always\n * receives safe hex fallbacks instead of unparseable color function strings.\n *\n * ---\n *\n * This module is the packaged union of three source files that always shipped\n * together:\n * • dom-capture-print-utils — the capture/PDF renderers (above),\n * • dom-capture-block-printer — the BlockPrinter factory + captureBlockElement,\n * • markdown-pdf — markdownToPdfBlob.\n *\n * Package-level seams (the only deliberate departures from the originals):\n * • `markdownToPdfBlob` takes its markdown→HTML converter and its stylesheet\n * loader INJECTED (`MarkdownPdfDeps`) instead of dynamically importing app\n * modules. The package cannot reach into an application's feature folders.\n * • `sanitizeForCapture` is a package-added export (it was module-private in\n * the original) so the sanitizer can be tested on its own.\n * • `captureBlockElement` calls `captureToPDF` directly rather than through a\n * dynamic import — html2canvas and jspdf are still lazily imported inside\n * the render functions, which is the laziness that actually matters.\n *\n * html2canvas and jspdf stay behind call-time dynamic imports: importing this\n * module pulls in neither, and the module touches `window`/`document` only when\n * one of its functions is called.\n */\n\nimport { copyImage } from \"@ai-matrx/kit/clipboard\";\nimport type { BlockPrinter } from \"./core\";\nimport type { MarkdownSkin } from \"./markdown\";\nimport { PAGE_BREAK_CLASS } from \"./directives\";\nimport { flattenFlips3d } from \"./flatten-3d\";\n\n/** Type-only view of the lazily imported defaults module. */\ntype MarkdownModule = typeof import(\"./markdown\");\n\nexport interface DomCaptureOptions {\n /** Paper size — 'a4' (default) or 'letter' */\n paperSize?: \"a4\" | \"letter\";\n /** Orientation */\n orientation?: \"portrait\" | \"landscape\";\n /** Scale factor for html2canvas (higher = sharper, slower). Default: 2 */\n scale?: number;\n /** Background colour for the capture. Default: '#ffffff' */\n background?: string;\n /** Filename for the downloaded PDF (without extension). Default: 'ai-response' */\n filename?: string;\n /** Image encoding inside the PDF. PNG is lossless; JPEG is much smaller for reports. */\n imageFormat?: \"png\" | \"jpeg\";\n /** JPEG quality from 0 to 1. Ignored for PNG. Default: 0.92. */\n imageQuality?: number;\n /** Called after each page is captured, with current page / total estimate */\n onProgress?: (page: number, total: number) => void;\n /**\n * Color-fallback theme for unparseable (oklch/lab) computed colors.\n * \"auto\" (default) follows the app's dark class — right for capturing\n * on-screen UI. Pass \"light\" for always-light offscreen document renders\n * so dark-mode fallbacks never bleed into a white-background PDF.\n */\n theme?: \"auto\" | \"light\";\n}\n\nconst PAGE_DIMS = {\n a4: { w: 210, h: 297 },\n letter: { w: 215.9, h: 279.4 },\n} as const;\n\n// Matches any modern CSS color function html2canvas cannot parse\nconst UNSAFE_COLOR_RE = /\\b(oklch|oklab|lab|lch|color)\\s*\\(/i;\n\nfunction isSafeColor(value: string): boolean {\n return !UNSAFE_COLOR_RE.test(value);\n}\n\n/**\n * Replaces modern CSS color functions in a single value string with a\n * context-appropriate hex fallback.\n */\nfunction safenColor(value: string, prop: string, isDark: boolean): string {\n if (isSafeColor(value)) return value;\n const lp = prop.toLowerCase();\n const fallback = lp.includes(\"background\")\n ? isDark ? \"#1f2937\" : \"#ffffff\"\n : lp === \"color\"\n ? isDark ? \"#f3f4f6\" : \"#111827\"\n : lp.includes(\"border\") || lp.includes(\"outline\")\n ? isDark ? \"#4b5563\" : \"#d1d5db\"\n : lp === \"fill\"\n ? isDark ? \"#9ca3af\" : \"#374151\"\n : \"#6b7280\";\n // Replace each colour FUNCTION in place, never the whole value: a gradient\n // (`background-image: linear-gradient(oklch(…), …)`) answered as a bare hex\n // is a value html2canvas cannot parse as an image (\"Unsupported image type 5\"\n // — Full Print died on every answer that carried one, 2026-10-08).\n return value.replace(UNSAFE_COLOR_FN_RE, fallback);\n}\n\n/** One modern colour function call, one level of nested parentheses allowed (`oklch(from var(--x) l c h)`). */\nconst UNSAFE_COLOR_FN_RE = /\\b(?:oklch|oklab|lab|lch|color)\\s*\\((?:[^()]|\\([^()]*\\))*\\)/gi;\n\n/**\n * Patch window.getComputedStyle so that any call during the capture returns\n * safe hex values instead of oklch/lab/etc.\n *\n * html2canvas calls getComputedStyle extensively — this is the authoritative\n * intercept point. Returns a restore function.\n */\nfunction patchGetComputedStyle(isDark: boolean, views: readonly Window[] = [window]): () => void {\n const restores = [...new Set(views)].map((view) => patchOneWindow(view, isDark));\n return () => restores.forEach((restore) => restore());\n}\n\nfunction patchOneWindow(window: Window, isDark: boolean): () => void {\n // Two references on purpose. `original` is the UNBOUND function that was\n // installed before us and is what the restore puts back — restoring a bound\n // wrapper instead would leave one more `.bind` layer installed after every\n // capture, so the real function identity would never come back. `callOriginal`\n // is the bound form we invoke, because getComputedStyle needs `window` as its\n // receiver.\n const original = window.getComputedStyle;\n const callOriginal = original.bind(window);\n\n window.getComputedStyle = function patchedGetComputedStyle(\n elt: Element,\n pseudoElt?: string | null,\n ): CSSStyleDeclaration {\n const computed = callOriginal(elt, pseudoElt);\n\n return new Proxy(computed, {\n get(target, prop) {\n // CSSStyleDeclaration accessors require the native object as their\n // receiver. Using the Proxy as the receiver throws \"Illegal\n // invocation\" while html2canvas clones inline styles.\n const value: unknown = Reflect.get(target, prop, target);\n\n // Intercept string property accesses (color values)\n if (typeof prop === \"string\" && typeof value === \"string\" && value) {\n if (!isSafeColor(value)) {\n return safenColor(value, prop, isDark);\n }\n }\n\n // Intercept getPropertyValue\n if (prop === \"getPropertyValue\") {\n return (propName: string) => {\n const raw = target.getPropertyValue(propName);\n if (raw && !isSafeColor(raw)) {\n return safenColor(raw, propName, isDark);\n }\n return raw;\n };\n }\n\n if (typeof value === \"function\") {\n const fn = value as (...args: unknown[]) => unknown;\n return fn.bind(target);\n }\n\n return value;\n },\n });\n };\n\n return () => {\n window.getComputedStyle = original;\n };\n}\n\n/**\n * Capture a DOM element and export it as a multi-page PDF download.\n */\nexport async function captureToPDF(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n): Promise<void> {\n const pdf = await renderElementToPdf(element, options);\n pdf.save(`${options.filename ?? \"ai-response\"}.pdf`);\n}\n\n/**\n * Capture a DOM element and return the multi-page PDF as a Blob — for\n * callers that upload/persist the PDF instead of downloading it (e.g. the\n * chat \"Save as → PDF Document\" action saving into the user's Files).\n */\nexport async function captureToPDFBlob(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n): Promise<Blob> {\n const pdf = await renderElementToPdf(element, options);\n return pdf.output(\"blob\");\n}\n\n/**\n * Capture deliberately paginated DOM elements as one PDF page each.\n *\n * Use this for designed reports whose page boundaries are part of the layout.\n * The ordinary capture helpers intentionally slice one continuous canvas; this\n * variant preserves explicit report pages and therefore cannot cut a section\n * in half at an arbitrary paper boundary.\n */\nexport async function captureElementsToPDF(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n): Promise<void> {\n const pdf = await renderElementsToPdf(elements, options);\n pdf.save(`${options.filename ?? \"ai-response\"}.pdf`);\n}\n\n/** Blob form of captureElementsToPDF for durable file-system uploads. */\nexport async function captureElementsToPDFBlob(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n): Promise<Blob> {\n const pdf = await renderElementsToPdf(elements, options);\n return pdf.output(\"blob\");\n}\n\nasync function renderElementsToPdf(\n elements: HTMLElement[],\n options: DomCaptureOptions,\n) {\n if (elements.length === 0) {\n throw new Error(\"No report pages are available to export.\");\n }\n\n const {\n paperSize = \"a4\",\n orientation = \"portrait\",\n scale = 2,\n background = \"#ffffff\",\n imageFormat = \"png\",\n imageQuality = 0.92,\n onProgress,\n } = options;\n const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([\n import(\"html2canvas\"),\n import(\"jspdf\"),\n ]);\n const dims = PAGE_DIMS[paperSize];\n const pageW = orientation === \"portrait\" ? dims.w : dims.h;\n const pageH = orientation === \"portrait\" ? dims.h : dims.w;\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n const restoreGetComputedStyle = patchGetComputedStyle(false, [window, ...elements.map((e) => e.ownerDocument.defaultView ?? window)]);\n\n try {\n for (const [index, element] of elements.entries()) {\n onProgress?.(index, elements.length);\n const canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: background,\n logging: false,\n windowWidth: element.scrollWidth,\n windowHeight: element.scrollHeight,\n onclone: (clonedDoc, clonedEl) => flattenFlips3d(clonedDoc, clonedEl),\n });\n if (index > 0) pdf.addPage();\n const isJpeg = imageFormat === \"jpeg\";\n pdf.addImage(\n canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n ),\n isJpeg ? \"JPEG\" : \"PNG\",\n 0,\n 0,\n pageW,\n pageH,\n );\n onProgress?.(index + 1, elements.length);\n }\n } finally {\n restoreGetComputedStyle();\n }\n\n return pdf;\n}\n\nasync function renderElementToPdf(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n) {\n return renderSectionsToPdf([element], options);\n}\n\n/**\n * Capture one or more continuous sections into one PDF. Each section is\n * sliced across as many pages as it needs, and every section after the first\n * STARTS ON A NEW PAGE — that is how a markdown page break survives the\n * canvas lane, where CSS `break-after` means nothing.\n */\nasync function renderSectionsToPdf(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n) {\n const {\n paperSize = \"a4\",\n orientation = \"portrait\",\n scale = 2,\n background = \"#ffffff\",\n imageFormat = \"png\",\n imageQuality = 0.92,\n onProgress,\n } = options;\n\n const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([\n import(\"html2canvas\"),\n import(\"jspdf\"),\n ]);\n\n const dims = PAGE_DIMS[paperSize];\n const pageW = orientation === \"portrait\" ? dims.w : dims.h;\n const pageH = orientation === \"portrait\" ? dims.h : dims.w;\n\n onProgress?.(0, 1);\n\n const isDark =\n options.theme === \"light\"\n ? false\n : document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n const bgColor = background === \"#ffffff\" && isDark ? \"#111827\" : background;\n\n // The element may live in its own frame (a message copied alone for capture): patch that window too.\n const restoreGetComputedStyle = patchGetComputedStyle(isDark, [window, ...elements.map((e) => e.ownerDocument.defaultView ?? window)]);\n\n // ONE PAGE AT A TIME. Each section is drawn in page-tall windows, never as\n // one canvas the height of the whole section: a long answer at scale 2 is a\n // canvas past what a browser will encode, `toDataURL` answers \"data:,\" and\n // jsPDF throws \"Unsupported image type\" — Full Print of a 25-page answer\n // never produced a file (2026-10-08). A window per page also stops every\n // page from embedding the whole section's image.\n const isJpeg = imageFormat === \"jpeg\";\n const imgFormat = isJpeg ? \"JPEG\" : \"PNG\";\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n\n // Half a millimetre of tolerance: a section that is an exact multiple of the\n // page height must not grow a blank trailing page from float rounding.\n const EPSILON_MM = 0.5;\n const sections = elements.map((element) => {\n const cssW = Math.max(1, element.scrollWidth);\n const cssH = Math.max(1, element.scrollHeight);\n const pagePx = (cssW * pageH) / pageW;\n const pages = Math.max(1, Math.ceil(((cssH * pageW) / cssW - EPSILON_MM) / pageH));\n return { element, cssW, cssH, pagePx, pages };\n });\n const totalPages = sections.reduce((a, s) => a + s.pages, 0);\n let page = 0;\n\n try {\n for (const { element, cssW, cssH, pagePx, pages } of sections) {\n const view = element.ownerDocument.defaultView ?? window;\n const box = element.getBoundingClientRect();\n const left = box.left + view.scrollX;\n const top = box.top + view.scrollY;\n for (let index = 0; index < pages; index += 1) {\n const offset = index * pagePx;\n const height = Math.max(1, Math.min(pagePx, cssH - offset));\n const canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: bgColor,\n logging: false,\n windowWidth: cssW,\n windowHeight: cssH,\n x: left,\n y: top + offset,\n width: cssW,\n height,\n onclone: (clonedDoc, clonedEl) => flattenFlips3d(clonedDoc, clonedEl),\n });\n const imgData = canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n );\n if (!imgData.startsWith(\"data:image/\")) {\n throw new Error(`Page ${page + 1} could not be drawn (the browser refused a ${canvas.width}×${canvas.height} image).`);\n }\n if (page > 0) pdf.addPage();\n pdf.addImage(imgData, imgFormat, 0, 0, pageW, (height * pageW) / cssW);\n page += 1;\n onProgress?.(page, totalPages);\n }\n }\n } finally {\n restoreGetComputedStyle();\n }\n\n return pdf;\n}\n\n/**\n * Capture a DOM element and copy it to the clipboard as a PNG, through THE one image door (kit\n * `copyImage`). `true` only when the image is on the clipboard — gate any \"Copied\" UI on it. A\n * refused copy answers `false` and changes nothing (it used to download `capture.png` silently and\n * resolve as if it had copied); the caller says so and may offer a download itself.\n */\nexport async function captureToClipboardImage(\n element: HTMLElement,\n): Promise<boolean> {\n const isDark =\n document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n const canvas = await captureCanvas(element, {\n scale: 2,\n backgroundColor: isDark ? \"#111827\" : \"#ffffff\",\n safeColors: { dark: isDark },\n });\n const blob = await new Promise<Blob | null>((resolve) =>\n canvas.toBlob(resolve, \"image/png\"),\n );\n if (!blob) {\n console.error(\"Failed to copy image: the capture could not be encoded\");\n return false;\n }\n return copyImage(blob);\n}\n\n// ─── THE one DOM-capture + canvas-to-PDF engine (AP-2 engine ruling) ─────────\n// Print sits BELOW alchemy, so the one html2canvas + jsPDF engine lives here and alchemy's\n// `/operate/capture` (`captureElement`, `pagesToPdf`, `elementsToPdf`) loads these lazily.\n\n/** Options for `captureCanvas` — html2canvas's, plus the colour-safety patch. */\nexport type CanvasCaptureOptions = {\n /** Device-pixel scale of the capture (default 2 — legible when scaled in a deck or print). */\n scale?: number;\n /** Canvas background; null keeps transparency. */\n backgroundColor?: string | null;\n useCORS?: boolean;\n allowTaint?: boolean;\n logging?: boolean;\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n scrollX?: number;\n scrollY?: number;\n windowWidth?: number;\n windowHeight?: number;\n ignoreElements?(element: Element): boolean;\n onclone?(document: Document, element: HTMLElement): void | Promise<void>;\n /** Replace unparseable modern colour functions while capturing (`{ dark }` picks the palette; `true` follows the page). */\n safeColors?: boolean | { dark: boolean };\n};\n\nconst prefersDark = () =>\n typeof document !== \"undefined\" &&\n (document.documentElement.classList.contains(\"dark\") ||\n (typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches));\n\n/** Rasterise a live element to a canvas (what the person sees, at `scale`). html2canvas loads on first use. */\nexport async function captureCanvas(\n element: HTMLElement,\n options: CanvasCaptureOptions = {},\n): Promise<HTMLCanvasElement> {\n const { safeColors, ...rest } = options;\n const { default: html2canvas } = await import(\"html2canvas\");\n const restore = safeColors\n ? patchGetComputedStyle(\n typeof safeColors === \"object\" ? safeColors.dark : prefersDark(),\n )\n : () => {};\n try {\n const callerClone = rest.onclone;\n return await html2canvas(element, {\n scale: 2,\n logging: false,\n useCORS: true,\n ...rest,\n onclone: async (clonedDoc: Document, clonedEl: HTMLElement) => {\n flattenFlips3d(clonedDoc, clonedEl);\n await callerClone?.(clonedDoc, clonedEl);\n },\n } as Parameters<typeof html2canvas>[1]);\n } finally {\n restore();\n }\n}\n\nexport type PdfMargins = { top: number; right: number; bottom: number; left: number };\nexport type PdfPage = {\n image: HTMLCanvasElement;\n /** \"JPEG\" (default 0.92 quality) keeps a flat chart an order of magnitude smaller than a PNG embed. */\n imageType?: \"PNG\" | \"JPEG\";\n quality?: number;\n compression?: \"NONE\" | \"FAST\" | \"MEDIUM\" | \"SLOW\";\n /** \"fill\": the image covers the page. Margins: the image is scaled to fit inside them, never cropped. */\n placement?: \"fill\" | PdfMargins;\n /** A line of text drawn on the page (e.g. the as-of date a chart is true of). */\n header?: { text: string; x: number; y: number; fontSize?: number };\n};\nexport type PdfDocumentOptions = {\n orientation?: \"portrait\" | \"landscape\";\n unit?: \"pt\" | \"mm\" | \"cm\" | \"in\" | \"px\";\n format?: string | [number, number];\n};\n\n/** Lay canvases out as PDF pages (one page each) and answer the PDF Blob. jsPDF loads on first use. */\nexport async function pagesToPdf(\n pages: readonly PdfPage[],\n options: PdfDocumentOptions = {},\n): Promise<Blob> {\n const { jsPDF } = await import(\"jspdf\");\n const doc = new jsPDF({\n orientation: options.orientation ?? \"portrait\",\n unit: options.unit ?? \"pt\",\n format: options.format ?? \"a4\",\n });\n pages.forEach((page, index) => {\n if (index > 0) doc.addPage();\n const pageW = doc.internal.pageSize.getWidth();\n const pageH = doc.internal.pageSize.getHeight();\n if (page.header) {\n doc.setFontSize(page.header.fontSize ?? 12);\n doc.text(page.header.text, page.header.x, page.header.y);\n }\n const type = page.imageType ?? \"PNG\";\n const data =\n type === \"JPEG\"\n ? page.image.toDataURL(\"image/jpeg\", page.quality ?? 0.92)\n : page.image.toDataURL(\"image/png\");\n const placement = page.placement ?? \"fill\";\n let x = 0,\n y = 0,\n w = pageW,\n h = pageH;\n if (placement !== \"fill\") {\n const availW = pageW - placement.left - placement.right;\n const availH = pageH - placement.top - placement.bottom;\n const scale = Math.min(availW / page.image.width, availH / page.image.height);\n x = placement.left;\n y = placement.top;\n w = page.image.width * scale;\n h = page.image.height * scale;\n }\n if (page.compression) doc.addImage(data, type, x, y, w, h, undefined, page.compression);\n else doc.addImage(data, type, x, y, w, h);\n });\n return doc.output(\"blob\");\n}\n\n/**\n * DOM-Capture BlockPrinter factory\n *\n * Creates a BlockPrinter that captures a provided DOM element and exports it as PDF.\n * Used by visual blocks (Timeline, ComparisonTable, DecisionTree, Diagram) where\n * the rendered DOM is the most faithful representation.\n *\n * Usage inside a block component:\n * const { captureRef, printAction } = useBlockDomCapture('Timeline');\n * // attach captureRef to the block's root element\n * // add printAction to customActions or controls\n */\n\n/**\n * Create a BlockPrinter backed by DOM capture.\n * The printer's `print(data)` call is a no-op — actual printing is triggered\n * imperatively via `captureElement(el)`.\n *\n * This exists so the block can register itself in the BlockPrinter contract\n * while keeping the DOM reference local to the component.\n */\nexport function createDomCapturePrinter(blockLabel: string): BlockPrinter {\n return {\n label: `Print ${blockLabel}`,\n variants: [],\n // No-op: DOM capture blocks use captureElement() directly\n print: () => {},\n };\n}\n\n/**\n * Capture a DOM element as PDF.\n * Called directly by block components when their print button is clicked.\n *\n * @param element - The DOM element to capture\n * @param filename - Output filename (without extension)\n * @param orientation - \"landscape\" (default) for wide visual blocks; \"portrait\" for tall/list content\n */\nexport async function captureBlockElement(\n element: HTMLElement,\n filename: string,\n orientation: \"landscape\" | \"portrait\" = \"landscape\",\n): Promise<void> {\n await captureToPDF(element, {\n filename,\n scale: 2,\n orientation,\n });\n}\n\n/**\n * markdownToPdfBlob — render a markdown string to a styled, multi-page PDF\n * Blob without any visible UI.\n *\n * Pipeline: markdown → WordPress-styled HTML (the same converter the\n * publish/copy-HTML flows use) → sanitized offscreen DOM host → html2canvas +\n * jsPDF via `captureToPDFBlob`. Everything heavy stays lazy-loaded behind\n * this module.\n *\n * Used by the chat \"Save as → PDF Document\" action, which uploads the blob\n * into the user's Files. Reuse this for any \"content → PDF file\" need — do\n * not fork another markdown-to-pdf path.\n */\n\n/**\n * OVERRIDES for markdownToPdfBlob — every field is optional.\n *\n * Both ports ship a working default in `@ai-matrx/print/markdown`\n * (`markdownToHtml` + `getMarkdownStylesheet`), so `markdownToPdfBlob(md)`\n * alone produces a styled PDF in a fresh app. Supplying a field replaces that\n * default; supplying neither is the normal case. Until 0.3.0 both fields were\n * REQUIRED, which forced every host to build the hard half itself.\n */\nexport interface MarkdownPdfDeps {\n /** markdown → HTML body string. Default: the package's `markdownToHtml`. */\n convertToHtml?: (markdown: string) => string;\n /** the stylesheet the rendered HTML expects. Default: the \"document\" skin. */\n loadCss?: () => Promise<string> | string;\n /** Which shipped look to use when `convertToHtml`/`loadCss` are not given. */\n skin?: MarkdownSkin;\n /** Host brand token overrides appended after the package's default values. */\n tokens?: string;\n /**\n * Where degraded conversion and unreachable images are announced. Nothing\n * fails silently: without a sink these still surface as `console.warn`.\n */\n onNotice?: (notice: MarkdownPdfNotice) => void;\n /**\n * How long to wait for images to decode before capturing, in ms.\n * Default 8000. html2canvas paints whatever has loaded, so an un-awaited\n * image is a blank rectangle in the PDF with no error anywhere.\n */\n imageTimeoutMs?: number;\n}\n\n/** A degradation the PDF lane detected, with the remedy. Never swallowed. */\nexport interface MarkdownPdfNotice {\n code:\n | \"flashcard-malformed\"\n | \"empty-output\"\n | \"html-stripped\"\n | \"image-unreachable\"\n | \"image-timeout\";\n message: string;\n remedy: string;\n}\n\n/**\n * Markdown can carry raw HTML; we are about to attach it to the live\n * document, so strip anything executable. html2canvas ignores scripts, but\n * inline handlers (`onerror`) and javascript: URLs would fire on attach.\n */\nexport function sanitizeForCapture(html: string): DocumentFragment {\n const template = document.createElement(\"template\");\n template.innerHTML = html;\n const fragment = template.content;\n\n fragment\n .querySelectorAll(\"script, iframe, object, embed, link, meta\")\n .forEach((el) => el.remove());\n fragment.querySelectorAll(\"*\").forEach((el) => {\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase();\n if (name.startsWith(\"on\")) el.removeAttribute(attr.name);\n if (\n (name === \"href\" || name === \"src\") &&\n attr.value.trim().toLowerCase().startsWith(\"javascript:\")\n ) {\n el.removeAttribute(attr.name);\n }\n }\n });\n return fragment;\n}\n\n/**\n * Wait for every `<img>` inside a host to finish decoding, and REPORT the ones\n * that never will.\n *\n * html2canvas captures whatever the browser has painted: an image still in\n * flight becomes a blank rectangle and an image that 404s becomes a blank\n * rectangle, both without a single error anywhere. This is the difference\n * between a PDF that is missing a figure and a PDF that is missing a figure\n * AND says so.\n */\nasync function settleImages(\n host: HTMLElement,\n timeoutMs: number,\n notify: (notice: MarkdownPdfNotice) => void,\n): Promise<void> {\n const images = Array.from(host.querySelectorAll(\"img\"));\n if (images.length === 0) return;\n\n const timedOut: string[] = [];\n await Promise.all(\n images.map(\n (img) =>\n new Promise<void>((resolve) => {\n if (img.complete) {\n if (img.naturalWidth === 0) {\n notify({\n code: \"image-unreachable\",\n message: `An image could not be loaded and will be blank in the PDF: ${img.src}`,\n remedy:\n \"Use a durable, publicly reachable https URL — a print render is an unauthenticated document and cannot use session-signed or expiring links.\",\n });\n }\n resolve();\n return;\n }\n const timer = setTimeout(() => {\n timedOut.push(img.src);\n resolve();\n }, timeoutMs);\n const done = (ok: boolean): void => {\n clearTimeout(timer);\n if (!ok) {\n notify({\n code: \"image-unreachable\",\n message: `An image could not be loaded and will be blank in the PDF: ${img.src}`,\n remedy:\n \"Use a durable, publicly reachable https URL — a print render is an unauthenticated document and cannot use session-signed or expiring links.\",\n });\n }\n resolve();\n };\n img.addEventListener(\"load\", () => done(true), { once: true });\n img.addEventListener(\"error\", () => done(false), { once: true });\n }),\n ),\n );\n\n if (timedOut.length > 0) {\n notify({\n code: \"image-timeout\",\n message: `${timedOut.length} image(s) did not finish loading in ${timeoutMs}ms and may be blank in the PDF: ${timedOut.join(\", \")}`,\n remedy:\n \"Raise `imageTimeoutMs`, or serve the images from a faster/CDN origin before exporting.\",\n });\n }\n}\n\nexport async function markdownToPdfBlob(\n markdown: string,\n deps: MarkdownPdfDeps = {},\n): Promise<Blob> {\n if (!markdown.trim()) {\n throw new Error(\"Nothing to convert — the content is empty.\");\n }\n\n // A caller with no sink still gets told. Silence is not an option the API\n // offers, because a silently degraded PDF looks exactly like a correct one.\n const notify = (notice: MarkdownPdfNotice): void => {\n if (deps.onNotice) {\n deps.onNotice(notice);\n return;\n }\n console.warn(`[@ai-matrx/print] ${notice.message} → ${notice.remedy}`);\n };\n\n // The defaults live behind a lazy import: a consumer that injects its own\n // converter and stylesheet never pulls the ~20KB of package CSS into its\n // bundle, and neither does one that imports /pdf for DOM capture only.\n //\n // The specifier is the package's OWN public name, not a relative path: tsup\n // runs with `splitting: false`, so `import(\"./markdown\")` gets inlined into\n // this chunk and the lazy edge silently disappears (measured — dist/pdf.js\n // grew by the full stylesheet). A self-reference is external to the bundle\n // and resolves through the export map at runtime. Same pattern as\n // @ai-matrx/associations `./react/lazy`.\n const needsDefaults = !deps.convertToHtml || !deps.loadCss;\n const markdownModule = needsDefaults\n ? ((await import(\"@ai-matrx/print/markdown\")) as MarkdownModule)\n : undefined;\n\n const bodyHtml = deps.convertToHtml\n ? deps.convertToHtml(markdown)\n : (markdownModule as MarkdownModule).markdownToHtmlForSkin(\n markdown,\n deps.skin ?? \"document\",\n { onNotice: notify },\n );\n\n const css = deps.loadCss\n ? await deps.loadCss()\n : (markdownModule as MarkdownModule).getMarkdownStylesheet({\n skin: deps.skin ?? \"document\",\n ...(deps.tokens === undefined ? {} : { tokens: deps.tokens }),\n });\n\n if (!bodyHtml.trim()) {\n throw new Error(\n \"The markdown converted to an empty document — nothing would be printed. \" +\n \"Check the content is more than whitespace or reasoning blocks.\",\n );\n }\n\n // Offscreen host — must be in the document for html2canvas to compute\n // styles, but never visible and never intercepting input.\n const host = document.createElement(\"div\");\n host.style.cssText =\n \"position:fixed;left:-10000px;top:0;width:800px;background:#ffffff;color:#111827;pointer-events:none;\";\n host.setAttribute(\"aria-hidden\", \"true\");\n\n const style = document.createElement(\"style\");\n style.textContent = css;\n host.appendChild(style);\n\n const contentEl = document.createElement(\"div\");\n // The document stylesheet is class-scoped (.matrx-*) and expects this\n // container; the article sheet styles bare tags under `.content`.\n contentEl.className =\n (deps.skin ?? \"document\") === \"article\" ? \"content\" : \"matrx-content-container\";\n contentEl.style.cssText = \"padding:32px;background:#ffffff;\";\n contentEl.appendChild(sanitizeForCapture(bodyHtml));\n host.appendChild(contentEl);\n\n const hosts = splitHostAtPageBreaks(host, contentEl);\n for (const h of hosts) document.body.appendChild(h);\n try {\n for (const h of hosts) await settleImages(h, deps.imageTimeoutMs ?? 8000, notify);\n // Always-light document render: \"#fff\" (not \"#ffffff\") bypasses the\n // dark-mode background flip, and theme:\"light\" keeps the oklch-fallback\n // patch from substituting dark text colors while the app is in dark mode.\n const pdf = await renderSectionsToPdf(hosts, { background: \"#fff\", theme: \"light\" });\n return pdf.output(\"blob\");\n } catch (error) {\n // The raw html2canvas/jsPDF failure is meaningless to a user and to a host\n // author. Name the stage and the remedy here so no host writes a catch\n // block to reinterpret it (C22).\n throw new Error(\n `The PDF could not be rendered from this content: ${\n error instanceof Error ? error.message : String(error)\n }. Try again; if it repeats, the content likely contains an element the ` +\n \"renderer cannot rasterize (a cross-origin image without CORS headers, \" +\n \"or an embedded iframe/canvas).\",\n error instanceof Error ? { cause: error } : undefined,\n );\n } finally {\n for (const h of hosts) h.remove();\n }\n}\n\n/**\n * Split an offscreen capture host at its top-level page breaks into one host\n * per printed section (same stylesheet, same container shells), so each\n * section starts on a fresh PDF page. No breaks → the host itself.\n */\nfunction splitHostAtPageBreaks(host: HTMLElement, contentEl: HTMLElement): HTMLElement[] {\n const inner = contentEl.firstElementChild;\n const flow =\n inner instanceof HTMLElement && inner.classList.contains(\"matrx-content-container\")\n ? inner\n : contentEl;\n const hasBreak = Array.from(flow.children).some((c) => c.classList.contains(PAGE_BREAK_CLASS));\n if (!hasBreak) return [host];\n\n const style = host.querySelector(\"style\");\n const shell = (): { h: HTMLElement; target: HTMLElement } => {\n const h = host.cloneNode(false) as HTMLElement;\n if (style) h.appendChild(style.cloneNode(true));\n const c = contentEl.cloneNode(false) as HTMLElement;\n h.appendChild(c);\n if (flow === contentEl) return { h, target: c };\n const f = flow.cloneNode(false) as HTMLElement;\n c.appendChild(f);\n return { h, target: f };\n };\n\n const sections: HTMLElement[] = [];\n let current = shell();\n let hasContent = false;\n for (const node of Array.from(flow.childNodes)) {\n if (node instanceof HTMLElement && node.classList.contains(PAGE_BREAK_CLASS)) {\n if (hasContent) {\n sections.push(current.h);\n current = shell();\n hasContent = false;\n }\n continue;\n }\n if (node.nodeType === 1 || (node.textContent ?? \"\").trim()) hasContent = true;\n current.target.appendChild(node);\n }\n if (hasContent || sections.length === 0) sections.push(current.h);\n return sections;\n}\n","/**\n * @ai-matrx/print/directives — the ONE definition of the print directives\n * markdown can carry, shared by every surface that reads markdown.\n *\n * Why a separate, zero-dependency entry: the directive syntax has three\n * readers — this package's two converters (print window / PDF / HTML page),\n * the chat renderer's remark plugin (the on-screen preview), and any editor\n * that inserts one. If each re-derived the regex they would drift the first\n * time someone added an alias. `/markdown` carries ~20KB of stylesheet, so the\n * chat bundle imports the syntax from here instead and pays nothing.\n *\n * ── Page break ────────────────────────────────────────────────────────────\n * A line that is exactly one of these (case-insensitive, surrounding\n * whitespace ignored) forces the next content onto a new printed page:\n *\n * <!-- pagebreak --> ← canonical; invisible in any other renderer\n * <!-- page-break --> <!-- page break --> <!-- newpage -->\n * \\pagebreak \\newpage ← LaTeX / pandoc habit\n * <div style=\"page-break-after: always\"></div> ← Typora / VS Code habit\n * (also `page-break-before`, `break-after: page`, `break-before: page`,\n * and `<div class=\"page-break\"></div>` / `class=\"pagebreak\"`)\n *\n * On screen every surface draws the same idea: a dashed rule labelled\n * \"Page break\" with generous padding. In print it is a zero-height element\n * with `break-after: page`.\n */\n\n/** The form editors insert and docs recommend. */\nexport const PAGE_BREAK_MARKDOWN = \"<!-- pagebreak -->\";\n\n/** The class every surface puts on a rendered page break. */\nexport const PAGE_BREAK_CLASS = \"matrx-page-break\";\n\n/** Screen label for a rendered page break. */\nexport const PAGE_BREAK_LABEL = \"Page break\";\n\nconst COMMENT_FORM = /^<!--\\s*(?:page[\\s_-]?break|new[\\s_-]?page)\\s*-->$/i;\nconst LATEX_FORM = /^\\\\(?:pagebreak|newpage|clearpage)\\s*(?:\\{\\s*\\})?$/i;\nconst DIV_FORM = /^<div\\b([^>]*)>\\s*<\\/div>$|^<div\\b([^>]*)\\/>$/i;\nconst DIV_BREAK_ATTR =\n /(?:page-break-(?:after|before)\\s*:\\s*always|break-(?:after|before)\\s*:\\s*page|class\\s*=\\s*[\"'](?:[^\"']*\\s)?page-?break(?:\\s[^\"']*)?[\"'])/i;\n\n/** Columns of leading indentation (tab = 4, NBSP counts as a space). */\nfunction indentColumns(line: string): number {\n let cols = 0;\n for (const ch of line) {\n if (ch === \" \" || ch === \"\\u00A0\") cols += 1;\n else if (ch === \"\\t\") cols += 4;\n else break;\n }\n return cols;\n}\n\n/**\n * Is this single line a page-break directive? Indented four or more columns\n * it is not — in markdown that is a code block, and every surface must agree.\n */\nexport function isPageBreakLine(line: string): boolean {\n if (indentColumns(line) >= 4) return false;\n const t = line.trim();\n if (t.length < 8 || t.length > 200) return false;\n if (COMMENT_FORM.test(t) || LATEX_FORM.test(t)) return true;\n const div = DIV_FORM.exec(t);\n if (!div) return false;\n return DIV_BREAK_ATTR.test(div[1] ?? div[2] ?? \"\");\n}\n\n// ── Section break ─────────────────────────────────────────────────────────\n//\n// A section break is a page break that can also change the page layout for\n// everything after it (Word's \"Section break (next page)\"):\n//\n// <!-- section --> ← new page, same layout\n// <!-- section: landscape --> ← new page, landscape from here on\n// <!-- section landscape columns=2 --> ← landscape AND two columns\n// <!-- section portrait columns=1 --> ← back to the default layout\n//\n// Recognised words: `landscape` / `portrait`, `columns=N` (or `cols=N`,\n// `N columns`, 1–3). Unknown words are ignored, never an error — the break\n// still happens. Every page-break reader treats a section break as a break.\n\n/** The form editors insert for a plain section break. */\nexport const SECTION_BREAK_MARKDOWN = \"<!-- section -->\";\n\nexport interface SectionBreakDirective {\n /** Absent = inherit the document's orientation. */\n orientation?: \"portrait\" | \"landscape\";\n /** Absent = inherit the document's column count. 1–3. */\n columns?: 1 | 2 | 3;\n}\n\nconst SECTION_FORM = /^<!--\\s*section(?:[\\s_-]?break)?\\b\\s*:?\\s*([^>]*?)\\s*-->$/i;\n\n/** Parse a section-break line; `null` when the line is not one. */\nexport function parseSectionBreakLine(line: string): SectionBreakDirective | null {\n if (indentColumns(line) >= 4) return null;\n const m = SECTION_FORM.exec(line.trim());\n if (!m) return null;\n const words = (m[1] ?? \"\").toLowerCase();\n const out: SectionBreakDirective = {};\n if (/\\blandscape\\b/.test(words)) out.orientation = \"landscape\";\n else if (/\\bportrait\\b/.test(words)) out.orientation = \"portrait\";\n const cols = /\\b(?:columns?|cols?)\\s*[=:]\\s*(\\d)\\b/.exec(words) ?? /\\b(\\d)\\s*[-\\s]?columns?\\b/.exec(words);\n if (cols) {\n const n = Math.min(3, Math.max(1, Number(cols[1])));\n out.columns = n as 1 | 2 | 3;\n }\n return out;\n}\n\n// ── Table of contents / bibliography placement ───────────────────────────\n//\n// `[[toc]]`, `[toc]`, `[TOC]`, `${toc}`, `<!-- toc -->`, `\\tableofcontents`\n// put the table of contents at that spot (otherwise frontmatter `toc: true`\n// puts it at the start). `<!-- bibliography -->`, `[[bibliography]]`,\n// `\\printbibliography` put the reference list there (otherwise it closes the\n// document whenever something is cited).\n\nexport const TOC_MARKDOWN = \"[[toc]]\";\nexport const BIBLIOGRAPHY_MARKDOWN = \"<!-- bibliography -->\";\n\nconst TOC_FORM = /^(?:\\[\\[\\s*toc\\s*\\]\\]|\\[toc\\]|\\$\\{toc\\}|<!--\\s*toc\\s*-->|\\\\tableofcontents)$/i;\nconst BIBLIOGRAPHY_FORM =\n /^(?:\\[\\[\\s*(?:bibliography|references)\\s*\\]\\]|<!--\\s*(?:bibliography|references)\\s*-->|\\\\printbibliography|\\\\bibliography\\{[^}]*\\})$/i;\n\nexport function isTocLine(line: string): boolean {\n return indentColumns(line) < 4 && TOC_FORM.test(line.trim());\n}\n\nexport function isBibliographyLine(line: string): boolean {\n return indentColumns(line) < 4 && BIBLIOGRAPHY_FORM.test(line.trim());\n}\n\n/** Any directive that ends the current printed page (page or section break). */\nexport function isBreakLine(line: string): boolean {\n return isPageBreakLine(line) || parseSectionBreakLine(line) !== null;\n}\n\n/**\n * Split markdown into the pieces that belong on separate pages. Fence-aware:\n * a directive inside a ``` / ~~~ code block is code, not a break. Always\n * returns at least one piece; empty pieces (two breaks in a row, a break at\n * the very start or end) are kept out so no blank page is produced.\n */\nexport function splitAtPageBreaks(markdown: string): string[] {\n const lines = markdown.split(/\\r?\\n/);\n const pieces: string[] = [];\n let current: string[] = [];\n let fence: string | null = null;\n\n for (const line of lines) {\n // Any indent — the converters accept list-nested fences, so must this.\n const fenceMatch = /^\\s*(`{3,}|~{3,})/.exec(line);\n if (fenceMatch) {\n const marker = fenceMatch[1] as string;\n if (fence === null) fence = marker;\n else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;\n } else if (fence === null && isBreakLine(line)) {\n if (current.join(\"\\n\").trim()) pieces.push(current.join(\"\\n\"));\n current = [];\n continue;\n }\n current.push(line);\n }\n if (current.join(\"\\n\").trim() || pieces.length === 0) pieces.push(current.join(\"\\n\"));\n return pieces;\n}\n","/**\n * Flatten CSS 3D flips for a canvas capture.\n *\n * html2canvas has no 3D: it ignores `backface-visibility` and `transform-style:\n * preserve-3d`, so a flip card (two faces stacked, the back turned with\n * `rotateY(180deg)`) is drawn with BOTH faces on top of each other and the back\n * face mirrored. This runs on the capture's cloned document and turns every such\n * stack into a flat row: the faces sit side by side, the face turned toward the\n * viewer first, every face un-mirrored and shown — so a printed card carries its\n * answer as well as its question. Plain 2D transforms are never touched.\n */\n\nconst IDENTITY = \"none\";\n\ntype FrameWindow = Window & typeof globalThis;\n\nfunction matrixOf(win: FrameWindow, cs: CSSStyleDeclaration): DOMMatrix {\n const t = cs.transform;\n if (!t || t === IDENTITY) return new win.DOMMatrix();\n try {\n return new win.DOMMatrix(t);\n } catch {\n return new win.DOMMatrix();\n }\n}\n\n/** True when the face, after every ancestor transform up to `root`, points toward the viewer. */\nfunction facesViewer(win: FrameWindow, face: HTMLElement, root: HTMLElement): boolean {\n let m = new win.DOMMatrix();\n for (let node: HTMLElement | null = face; node; node = node.parentElement) {\n m = matrixOf(win, win.getComputedStyle(node)).multiply(m);\n if (node === root) break;\n }\n // m33 is the z-component of the face normal after rotation: > 0 faces the viewer.\n return m.m33 >= 0;\n}\n\nfunction is3d(cs: CSSStyleDeclaration): boolean {\n return (\n cs.transformStyle === \"preserve-3d\" ||\n cs.backfaceVisibility === \"hidden\" ||\n cs.perspective !== \"none\" ||\n (cs.transform ?? \"\").startsWith(\"matrix3d\")\n );\n}\n\n/** Make every 3D flip in `root` html2canvas-safe. Idempotent; a no-op when there is no 3D. */\nexport function flattenFlips3d(doc: Document, root: HTMLElement): void {\n const win = doc.defaultView as FrameWindow | null;\n if (!win) return;\n const all = [root, ...Array.from(root.querySelectorAll<HTMLElement>(\"*\"))];\n const faces = all.filter((el) => win.getComputedStyle(el).backfaceVisibility === \"hidden\");\n if (faces.length === 0 && !all.some((el) => is3d(win.getComputedStyle(el)))) return;\n\n // Decide every face's facing BEFORE any style changes (it reads ancestor transforms).\n const facing = new Map<HTMLElement, boolean>();\n for (const face of faces) facing.set(face, facesViewer(win, face, root));\n\n // Faces grouped by the element that stacks them: that element becomes a flat row.\n const stacks = new Map<HTMLElement, HTMLElement[]>();\n for (const face of faces) {\n const parent = face.parentElement;\n if (!parent) continue;\n const list = stacks.get(parent) ?? [];\n list.push(face);\n stacks.set(parent, list);\n }\n\n for (const [stack, stackFaces] of stacks) {\n // A card is a fixed-height box with clipped faces; a printed face is wider-than-tall\n // text that must not be cut mid-line. Keep the box as tall as it was, let it grow.\n for (const box of [stack, stack.parentElement]) {\n if (!box || box === root) continue;\n const height = win.getComputedStyle(box).height;\n if (height.endsWith(\"px\")) box.style.setProperty(\"min-height\", height);\n box.style.setProperty(\"height\", \"auto\");\n box.style.setProperty(\"max-height\", \"none\");\n }\n if (stackFaces.length > 1) {\n stack.style.setProperty(\"display\", \"grid\");\n stack.style.setProperty(\"grid-template-columns\", `repeat(${stackFaces.length}, minmax(0, 1fr))`);\n stack.style.setProperty(\"gap\", \"8px\");\n }\n for (const face of stackFaces) {\n face.style.setProperty(\"position\", \"relative\");\n face.style.setProperty(\"inset\", \"auto\");\n face.style.setProperty(\"height\", \"auto\");\n face.style.setProperty(\"overflow\", \"visible\");\n face.style.setProperty(\"order\", facing.get(face) ? \"0\" : \"1\");\n }\n }\n\n for (const el of all) {\n const cs = win.getComputedStyle(el);\n if (!is3d(cs)) continue;\n el.style.setProperty(\"transform\", \"none\");\n el.style.setProperty(\"transform-style\", \"flat\");\n el.style.setProperty(\"perspective\", \"none\");\n el.style.setProperty(\"backface-visibility\", \"visible\");\n }\n}\n"],"mappings":"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","names":["window"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/print",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.18",
|
|
4
4
|
"description": "The AI Matrx print system: the BlockPrinter contract and print-window composition, the flashcards deck printer (duplex-mirrored cut sheets, Avery 5388, study sheets), QR generation with spec quiet zones and e-commerce payload builders, label-sheet printing on Avery and roll stock with custom label formats, and a complete markdown pipeline: a shipped markdown converter, two default print stylesheets, print windows, and markdown/DOM to PDF, and print-grade documents (cover, TOC, captions, cross-references, citations, sections) exported to PDF, DOCX, EPUB and HTML from one parsed tree.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -366,5 +366,5 @@
|
|
|
366
366
|
"test:watch": "vitest",
|
|
367
367
|
"verify:tarball": "node ./scripts/verify-tarball.mjs"
|
|
368
368
|
},
|
|
369
|
-
"gitHead": "
|
|
369
|
+
"gitHead": "776d512c11dbe2d4909742a217ecc47fcfa2116f"
|
|
370
370
|
}
|