@ai-matrx/print 0.2.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pdf.js CHANGED
@@ -256,12 +256,79 @@ function sanitizeForCapture(html) {
256
256
  });
257
257
  return fragment;
258
258
  }
259
- async function markdownToPdfBlob(markdown, deps) {
259
+ async function settleImages(host, timeoutMs, notify) {
260
+ const images = Array.from(host.querySelectorAll("img"));
261
+ if (images.length === 0) return;
262
+ const timedOut = [];
263
+ await Promise.all(
264
+ images.map(
265
+ (img) => new Promise((resolve) => {
266
+ if (img.complete) {
267
+ if (img.naturalWidth === 0) {
268
+ notify({
269
+ code: "image-unreachable",
270
+ message: `An image could not be loaded and will be blank in the PDF: ${img.src}`,
271
+ remedy: "Use a durable, publicly reachable https URL \u2014 a print render is an unauthenticated document and cannot use session-signed or expiring links."
272
+ });
273
+ }
274
+ resolve();
275
+ return;
276
+ }
277
+ const timer = setTimeout(() => {
278
+ timedOut.push(img.src);
279
+ resolve();
280
+ }, timeoutMs);
281
+ const done = (ok) => {
282
+ clearTimeout(timer);
283
+ if (!ok) {
284
+ notify({
285
+ code: "image-unreachable",
286
+ message: `An image could not be loaded and will be blank in the PDF: ${img.src}`,
287
+ remedy: "Use a durable, publicly reachable https URL \u2014 a print render is an unauthenticated document and cannot use session-signed or expiring links."
288
+ });
289
+ }
290
+ resolve();
291
+ };
292
+ img.addEventListener("load", () => done(true), { once: true });
293
+ img.addEventListener("error", () => done(false), { once: true });
294
+ })
295
+ )
296
+ );
297
+ if (timedOut.length > 0) {
298
+ notify({
299
+ code: "image-timeout",
300
+ message: `${timedOut.length} image(s) did not finish loading in ${timeoutMs}ms and may be blank in the PDF: ${timedOut.join(", ")}`,
301
+ remedy: "Raise `imageTimeoutMs`, or serve the images from a faster/CDN origin before exporting."
302
+ });
303
+ }
304
+ }
305
+ async function markdownToPdfBlob(markdown, deps = {}) {
260
306
  if (!markdown.trim()) {
261
307
  throw new Error("Nothing to convert \u2014 the content is empty.");
262
308
  }
263
- const bodyHtml = deps.convertToHtml(markdown);
264
- const css = await deps.loadCss();
309
+ const notify = (notice) => {
310
+ if (deps.onNotice) {
311
+ deps.onNotice(notice);
312
+ return;
313
+ }
314
+ console.warn(`[@ai-matrx/print] ${notice.message} \u2192 ${notice.remedy}`);
315
+ };
316
+ const needsDefaults = !deps.convertToHtml || !deps.loadCss;
317
+ const markdownModule = needsDefaults ? await import("@ai-matrx/print/markdown") : void 0;
318
+ const bodyHtml = deps.convertToHtml ? deps.convertToHtml(markdown) : markdownModule.markdownToHtmlForSkin(
319
+ markdown,
320
+ deps.skin ?? "document",
321
+ { onNotice: notify }
322
+ );
323
+ const css = deps.loadCss ? await deps.loadCss() : markdownModule.getMarkdownStylesheet({
324
+ skin: deps.skin ?? "document",
325
+ ...deps.tokens === void 0 ? {} : { tokens: deps.tokens }
326
+ });
327
+ if (!bodyHtml.trim()) {
328
+ throw new Error(
329
+ "The markdown converted to an empty document \u2014 nothing would be printed. Check the content is more than whitespace or reasoning blocks."
330
+ );
331
+ }
265
332
  const host = document.createElement("div");
266
333
  host.style.cssText = "position:fixed;left:-10000px;top:0;width:800px;background:#ffffff;color:#111827;pointer-events:none;";
267
334
  host.setAttribute("aria-hidden", "true");
@@ -269,13 +336,19 @@ async function markdownToPdfBlob(markdown, deps) {
269
336
  style.textContent = css;
270
337
  host.appendChild(style);
271
338
  const contentEl = document.createElement("div");
272
- contentEl.className = "matrx-content-container";
339
+ contentEl.className = (deps.skin ?? "document") === "article" ? "content" : "matrx-content-container";
273
340
  contentEl.style.cssText = "padding:32px;background:#ffffff;";
274
341
  contentEl.appendChild(sanitizeForCapture(bodyHtml));
275
342
  host.appendChild(contentEl);
276
343
  document.body.appendChild(host);
277
344
  try {
345
+ await settleImages(host, deps.imageTimeoutMs ?? 8e3, notify);
278
346
  return await captureToPDFBlob(host, { background: "#fff", theme: "light" });
347
+ } catch (error) {
348
+ throw new Error(
349
+ `The PDF could not be rendered from this content: ${error instanceof Error ? error.message : String(error)}. Try again; if it repeats, the content likely contains an element the renderer cannot rasterize (a cross-origin image without CORS headers, or an embedded iframe/canvas).`,
350
+ error instanceof Error ? { cause: error } : void 0
351
+ );
279
352
  } finally {
280
353
  host.remove();
281
354
  }
package/dist/pdf.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pdf.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 type { BlockPrinter } from \"./core\";\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 if (lp.includes(\"background\")) return isDark ? \"#1f2937\" : \"#ffffff\";\n if (lp === \"color\") return isDark ? \"#f3f4f6\" : \"#111827\";\n if (lp.includes(\"border\") || lp.includes(\"outline\"))\n return isDark ? \"#4b5563\" : \"#d1d5db\";\n if (lp === \"fill\") return isDark ? \"#9ca3af\" : \"#374151\";\n if (lp === \"stroke\") return isDark ? \"#6b7280\" : \"#6b7280\";\n return isDark ? \"#6b7280\" : \"#6b7280\";\n}\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): () => 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);\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 });\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 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 const restoreGetComputedStyle = patchGetComputedStyle(isDark);\n\n let canvas: HTMLCanvasElement;\n try {\n canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: bgColor,\n logging: false,\n windowWidth: element.scrollWidth,\n windowHeight: element.scrollHeight,\n });\n } finally {\n restoreGetComputedStyle();\n }\n\n // imageFormat/imageQuality are honoured here too. They are documented on\n // DomCaptureOptions with no \"elements-only\" caveat, and the paginated\n // renderer above already obeys them; this path used to hardcode PNG and\n // silently drop whatever the caller asked for. PNG remains the default, so\n // callers that never set the option are unaffected.\n const isJpeg = imageFormat === \"jpeg\";\n const imgData = canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n );\n const imgFormat = isJpeg ? \"JPEG\" : \"PNG\";\n const imgW = pageW;\n const imgH = (canvas.height * pageW) / canvas.width;\n\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n\n let heightLeft = imgH;\n let yOffset = 0;\n let page = 0;\n\n pdf.addImage(imgData, imgFormat, 0, yOffset, imgW, imgH);\n heightLeft -= pageH;\n page++;\n\n const totalPages = Math.ceil(imgH / pageH);\n while (heightLeft > 0) {\n yOffset -= pageH;\n pdf.addPage();\n pdf.addImage(imgData, imgFormat, 0, yOffset, imgW, imgH);\n heightLeft -= pageH;\n page++;\n onProgress?.(page, totalPages);\n }\n\n return pdf;\n}\n\n/**\n * Capture a DOM element and copy it to clipboard (or download) as PNG.\n */\nexport async function captureToClipboardImage(\n element: HTMLElement,\n): Promise<void> {\n const { default: html2canvas } = await import(\"html2canvas\");\n\n const isDark =\n document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n const restoreGetComputedStyle = patchGetComputedStyle(isDark);\n\n let canvas: HTMLCanvasElement;\n try {\n canvas = await html2canvas(element, {\n scale: 2,\n useCORS: true,\n backgroundColor: isDark ? \"#111827\" : \"#ffffff\",\n logging: false,\n });\n } finally {\n restoreGetComputedStyle();\n }\n\n // The returned promise covers the copy/download, not just the capture. The\n // original resolved the moment toBlob was *scheduled*, so `await\n // captureToClipboardImage(el)` followed by a \"Copied!\" toast fired before\n // anything had been copied, and a throw inside the fallback path escaped as\n // an unhandled rejection.\n await new Promise<void>((resolve, reject) => {\n canvas.toBlob(async (blob) => {\n if (!blob) {\n resolve();\n return;\n }\n try {\n try {\n await navigator.clipboard.write([\n new ClipboardItem({ \"image/png\": blob }),\n ]);\n } catch {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = \"capture.png\";\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n }\n resolve();\n } catch (error) {\n reject(error instanceof Error ? error : new Error(String(error)));\n }\n }, \"image/png\");\n });\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 * The two application-owned pieces markdownToPdfBlob needs. The original\n * reached for them with dynamic imports of app feature modules; a package\n * takes them from its caller instead.\n */\nexport interface MarkdownPdfDeps {\n /** markdown → HTML body string (the same converter the publish/copy-HTML flows use) */\n convertToHtml: (markdown: string) => string;\n /** the class-scoped stylesheet text the rendered HTML expects */\n loadCss: () => Promise<string> | 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\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 const bodyHtml = deps.convertToHtml(markdown);\n const css = await deps.loadCss();\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 WP stylesheet is class-scoped (.matrx-*) and expects this container.\n contentEl.className = \"matrx-content-container\";\n contentEl.style.cssText = \"padding:32px;background:#ffffff;\";\n contentEl.appendChild(sanitizeForCapture(bodyHtml));\n host.appendChild(contentEl);\n\n document.body.appendChild(host);\n try {\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 return await captureToPDFBlob(host, { background: \"#fff\", theme: \"light\" });\n } finally {\n host.remove();\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/pdf.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 type { BlockPrinter } from \"./core\";\nimport type { MarkdownSkin } from \"./markdown\";\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 if (lp.includes(\"background\")) return isDark ? \"#1f2937\" : \"#ffffff\";\n if (lp === \"color\") return isDark ? \"#f3f4f6\" : \"#111827\";\n if (lp.includes(\"border\") || lp.includes(\"outline\"))\n return isDark ? \"#4b5563\" : \"#d1d5db\";\n if (lp === \"fill\") return isDark ? \"#9ca3af\" : \"#374151\";\n if (lp === \"stroke\") return isDark ? \"#6b7280\" : \"#6b7280\";\n return isDark ? \"#6b7280\" : \"#6b7280\";\n}\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): () => 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);\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 });\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 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 const restoreGetComputedStyle = patchGetComputedStyle(isDark);\n\n let canvas: HTMLCanvasElement;\n try {\n canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: bgColor,\n logging: false,\n windowWidth: element.scrollWidth,\n windowHeight: element.scrollHeight,\n });\n } finally {\n restoreGetComputedStyle();\n }\n\n // imageFormat/imageQuality are honoured here too. They are documented on\n // DomCaptureOptions with no \"elements-only\" caveat, and the paginated\n // renderer above already obeys them; this path used to hardcode PNG and\n // silently drop whatever the caller asked for. PNG remains the default, so\n // callers that never set the option are unaffected.\n const isJpeg = imageFormat === \"jpeg\";\n const imgData = canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n );\n const imgFormat = isJpeg ? \"JPEG\" : \"PNG\";\n const imgW = pageW;\n const imgH = (canvas.height * pageW) / canvas.width;\n\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n\n let heightLeft = imgH;\n let yOffset = 0;\n let page = 0;\n\n pdf.addImage(imgData, imgFormat, 0, yOffset, imgW, imgH);\n heightLeft -= pageH;\n page++;\n\n const totalPages = Math.ceil(imgH / pageH);\n while (heightLeft > 0) {\n yOffset -= pageH;\n pdf.addPage();\n pdf.addImage(imgData, imgFormat, 0, yOffset, imgW, imgH);\n heightLeft -= pageH;\n page++;\n onProgress?.(page, totalPages);\n }\n\n return pdf;\n}\n\n/**\n * Capture a DOM element and copy it to clipboard (or download) as PNG.\n */\nexport async function captureToClipboardImage(\n element: HTMLElement,\n): Promise<void> {\n const { default: html2canvas } = await import(\"html2canvas\");\n\n const isDark =\n document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n const restoreGetComputedStyle = patchGetComputedStyle(isDark);\n\n let canvas: HTMLCanvasElement;\n try {\n canvas = await html2canvas(element, {\n scale: 2,\n useCORS: true,\n backgroundColor: isDark ? \"#111827\" : \"#ffffff\",\n logging: false,\n });\n } finally {\n restoreGetComputedStyle();\n }\n\n // The returned promise covers the copy/download, not just the capture. The\n // original resolved the moment toBlob was *scheduled*, so `await\n // captureToClipboardImage(el)` followed by a \"Copied!\" toast fired before\n // anything had been copied, and a throw inside the fallback path escaped as\n // an unhandled rejection.\n await new Promise<void>((resolve, reject) => {\n canvas.toBlob(async (blob) => {\n if (!blob) {\n resolve();\n return;\n }\n try {\n try {\n await navigator.clipboard.write([\n new ClipboardItem({ \"image/png\": blob }),\n ]);\n } catch {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = \"capture.png\";\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n }\n resolve();\n } catch (error) {\n reject(error instanceof Error ? error : new Error(String(error)));\n }\n }, \"image/png\");\n });\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 document.body.appendChild(host);\n try {\n await settleImages(host, 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 return await captureToPDFBlob(host, { background: \"#fff\", theme: \"light\" });\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 host.remove();\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ai-matrx/print",
3
- "version": "0.2.1",
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 DOM-capture/markdown to PDF.",
3
+ "version": "0.3.2",
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.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -136,6 +136,16 @@
136
136
  "types": "./dist/booklet.d.cts",
137
137
  "default": "./dist/booklet.cjs"
138
138
  }
139
+ },
140
+ "./markdown": {
141
+ "import": {
142
+ "types": "./dist/markdown.d.ts",
143
+ "default": "./dist/markdown.js"
144
+ },
145
+ "require": {
146
+ "types": "./dist/markdown.d.cts",
147
+ "default": "./dist/markdown.cjs"
148
+ }
139
149
  }
140
150
  },
141
151
  "dependencies": {