@ai-matrx/print 0.1.0
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 +56 -0
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/core.cjs +184 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +139 -0
- package/dist/core.d.ts +139 -0
- package/dist/core.js +163 -0
- package/dist/core.js.map +1 -0
- package/dist/flashcards.cjs +1709 -0
- package/dist/flashcards.cjs.map +1 -0
- package/dist/flashcards.d.cts +35 -0
- package/dist/flashcards.d.ts +35 -0
- package/dist/flashcards.js +1686 -0
- package/dist/flashcards.js.map +1 -0
- package/dist/index.cjs +19 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.cjs +1035 -0
- package/dist/labels.cjs.map +1 -0
- package/dist/labels.d.cts +270 -0
- package/dist/labels.d.ts +270 -0
- package/dist/labels.js +1002 -0
- package/dist/labels.js.map +1 -0
- package/dist/pdf.cjs +325 -0
- package/dist/pdf.cjs.map +1 -0
- package/dist/pdf.d.cts +151 -0
- package/dist/pdf.d.ts +151 -0
- package/dist/pdf.js +294 -0
- package/dist/pdf.js.map +1 -0
- package/dist/qr.cjs +328 -0
- package/dist/qr.cjs.map +1 -0
- package/dist/qr.d.cts +91 -0
- package/dist/qr.d.ts +91 -0
- package/dist/qr.js +297 -0
- package/dist/qr.js.map +1 -0
- package/dist/react.cjs +1569 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +301 -0
- package/dist/react.d.ts +301 -0
- package/dist/react.js +1539 -0
- package/dist/react.js.map +1 -0
- package/package.json +163 -0
package/dist/pdf.d.cts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { BlockPrinter } from './core.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DOM-Capture Print Utilities — Tier 2 Full-Message Print
|
|
5
|
+
*
|
|
6
|
+
* Captures a rendered DOM element using html2canvas, then pipes it into jsPDF.
|
|
7
|
+
* This handles ALL block types because it screenshots the actual rendered output.
|
|
8
|
+
*
|
|
9
|
+
* Problem: Tailwind CSS 4 uses modern CSS color functions (oklch, lab, color(display-p3))
|
|
10
|
+
* that html2canvas cannot parse. It calls console.error for each one, which gets
|
|
11
|
+
* captured by AdminDebugContextCollector and shows up as false errors.
|
|
12
|
+
*
|
|
13
|
+
* Solution: Patch window.getComputedStyle during the capture so html2canvas always
|
|
14
|
+
* receives safe hex fallbacks instead of unparseable color function strings.
|
|
15
|
+
*
|
|
16
|
+
* ---
|
|
17
|
+
*
|
|
18
|
+
* This module is the packaged union of three source files that always shipped
|
|
19
|
+
* together:
|
|
20
|
+
* • dom-capture-print-utils — the capture/PDF renderers (above),
|
|
21
|
+
* • dom-capture-block-printer — the BlockPrinter factory + captureBlockElement,
|
|
22
|
+
* • markdown-pdf — markdownToPdfBlob.
|
|
23
|
+
*
|
|
24
|
+
* Package-level seams (the only deliberate departures from the originals):
|
|
25
|
+
* • `markdownToPdfBlob` takes its markdown→HTML converter and its stylesheet
|
|
26
|
+
* loader INJECTED (`MarkdownPdfDeps`) instead of dynamically importing app
|
|
27
|
+
* modules. The package cannot reach into an application's feature folders.
|
|
28
|
+
* • `sanitizeForCapture` is a package-added export (it was module-private in
|
|
29
|
+
* the original) so the sanitizer can be tested on its own.
|
|
30
|
+
* • `captureBlockElement` calls `captureToPDF` directly rather than through a
|
|
31
|
+
* dynamic import — html2canvas and jspdf are still lazily imported inside
|
|
32
|
+
* the render functions, which is the laziness that actually matters.
|
|
33
|
+
*
|
|
34
|
+
* html2canvas and jspdf stay behind call-time dynamic imports: importing this
|
|
35
|
+
* module pulls in neither, and the module touches `window`/`document` only when
|
|
36
|
+
* one of its functions is called.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
interface DomCaptureOptions {
|
|
40
|
+
/** Paper size — 'a4' (default) or 'letter' */
|
|
41
|
+
paperSize?: "a4" | "letter";
|
|
42
|
+
/** Orientation */
|
|
43
|
+
orientation?: "portrait" | "landscape";
|
|
44
|
+
/** Scale factor for html2canvas (higher = sharper, slower). Default: 2 */
|
|
45
|
+
scale?: number;
|
|
46
|
+
/** Background colour for the capture. Default: '#ffffff' */
|
|
47
|
+
background?: string;
|
|
48
|
+
/** Filename for the downloaded PDF (without extension). Default: 'ai-response' */
|
|
49
|
+
filename?: string;
|
|
50
|
+
/** Image encoding inside the PDF. PNG is lossless; JPEG is much smaller for reports. */
|
|
51
|
+
imageFormat?: "png" | "jpeg";
|
|
52
|
+
/** JPEG quality from 0 to 1. Ignored for PNG. Default: 0.92. */
|
|
53
|
+
imageQuality?: number;
|
|
54
|
+
/** Called after each page is captured, with current page / total estimate */
|
|
55
|
+
onProgress?: (page: number, total: number) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Color-fallback theme for unparseable (oklch/lab) computed colors.
|
|
58
|
+
* "auto" (default) follows the app's dark class — right for capturing
|
|
59
|
+
* on-screen UI. Pass "light" for always-light offscreen document renders
|
|
60
|
+
* so dark-mode fallbacks never bleed into a white-background PDF.
|
|
61
|
+
*/
|
|
62
|
+
theme?: "auto" | "light";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Capture a DOM element and export it as a multi-page PDF download.
|
|
66
|
+
*/
|
|
67
|
+
declare function captureToPDF(element: HTMLElement, options?: DomCaptureOptions): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Capture a DOM element and return the multi-page PDF as a Blob — for
|
|
70
|
+
* callers that upload/persist the PDF instead of downloading it (e.g. the
|
|
71
|
+
* chat "Save as → PDF Document" action saving into the user's Files).
|
|
72
|
+
*/
|
|
73
|
+
declare function captureToPDFBlob(element: HTMLElement, options?: DomCaptureOptions): Promise<Blob>;
|
|
74
|
+
/**
|
|
75
|
+
* Capture deliberately paginated DOM elements as one PDF page each.
|
|
76
|
+
*
|
|
77
|
+
* Use this for designed reports whose page boundaries are part of the layout.
|
|
78
|
+
* The ordinary capture helpers intentionally slice one continuous canvas; this
|
|
79
|
+
* variant preserves explicit report pages and therefore cannot cut a section
|
|
80
|
+
* in half at an arbitrary paper boundary.
|
|
81
|
+
*/
|
|
82
|
+
declare function captureElementsToPDF(elements: HTMLElement[], options?: DomCaptureOptions): Promise<void>;
|
|
83
|
+
/** Blob form of captureElementsToPDF for durable file-system uploads. */
|
|
84
|
+
declare function captureElementsToPDFBlob(elements: HTMLElement[], options?: DomCaptureOptions): Promise<Blob>;
|
|
85
|
+
/**
|
|
86
|
+
* Capture a DOM element and copy it to clipboard (or download) as PNG.
|
|
87
|
+
*/
|
|
88
|
+
declare function captureToClipboardImage(element: HTMLElement): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* DOM-Capture BlockPrinter factory
|
|
91
|
+
*
|
|
92
|
+
* Creates a BlockPrinter that captures a provided DOM element and exports it as PDF.
|
|
93
|
+
* Used by visual blocks (Timeline, ComparisonTable, DecisionTree, Diagram) where
|
|
94
|
+
* the rendered DOM is the most faithful representation.
|
|
95
|
+
*
|
|
96
|
+
* Usage inside a block component:
|
|
97
|
+
* const { captureRef, printAction } = useBlockDomCapture('Timeline');
|
|
98
|
+
* // attach captureRef to the block's root element
|
|
99
|
+
* // add printAction to customActions or controls
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* Create a BlockPrinter backed by DOM capture.
|
|
103
|
+
* The printer's `print(data)` call is a no-op — actual printing is triggered
|
|
104
|
+
* imperatively via `captureElement(el)`.
|
|
105
|
+
*
|
|
106
|
+
* This exists so the block can register itself in the BlockPrinter contract
|
|
107
|
+
* while keeping the DOM reference local to the component.
|
|
108
|
+
*/
|
|
109
|
+
declare function createDomCapturePrinter(blockLabel: string): BlockPrinter;
|
|
110
|
+
/**
|
|
111
|
+
* Capture a DOM element as PDF.
|
|
112
|
+
* Called directly by block components when their print button is clicked.
|
|
113
|
+
*
|
|
114
|
+
* @param element - The DOM element to capture
|
|
115
|
+
* @param filename - Output filename (without extension)
|
|
116
|
+
* @param orientation - "landscape" (default) for wide visual blocks; "portrait" for tall/list content
|
|
117
|
+
*/
|
|
118
|
+
declare function captureBlockElement(element: HTMLElement, filename: string, orientation?: "landscape" | "portrait"): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* markdownToPdfBlob — render a markdown string to a styled, multi-page PDF
|
|
121
|
+
* Blob without any visible UI.
|
|
122
|
+
*
|
|
123
|
+
* Pipeline: markdown → WordPress-styled HTML (the same converter the
|
|
124
|
+
* publish/copy-HTML flows use) → sanitized offscreen DOM host → html2canvas +
|
|
125
|
+
* jsPDF via `captureToPDFBlob`. Everything heavy stays lazy-loaded behind
|
|
126
|
+
* this module.
|
|
127
|
+
*
|
|
128
|
+
* Used by the chat "Save as → PDF Document" action, which uploads the blob
|
|
129
|
+
* into the user's Files. Reuse this for any "content → PDF file" need — do
|
|
130
|
+
* not fork another markdown-to-pdf path.
|
|
131
|
+
*/
|
|
132
|
+
/**
|
|
133
|
+
* The two application-owned pieces markdownToPdfBlob needs. The original
|
|
134
|
+
* reached for them with dynamic imports of app feature modules; a package
|
|
135
|
+
* takes them from its caller instead.
|
|
136
|
+
*/
|
|
137
|
+
interface MarkdownPdfDeps {
|
|
138
|
+
/** markdown → HTML body string (the same converter the publish/copy-HTML flows use) */
|
|
139
|
+
convertToHtml: (markdown: string) => string;
|
|
140
|
+
/** the class-scoped stylesheet text the rendered HTML expects */
|
|
141
|
+
loadCss: () => Promise<string> | string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Markdown can carry raw HTML; we are about to attach it to the live
|
|
145
|
+
* document, so strip anything executable. html2canvas ignores scripts, but
|
|
146
|
+
* inline handlers (`onerror`) and javascript: URLs would fire on attach.
|
|
147
|
+
*/
|
|
148
|
+
declare function sanitizeForCapture(html: string): DocumentFragment;
|
|
149
|
+
declare function markdownToPdfBlob(markdown: string, deps: MarkdownPdfDeps): Promise<Blob>;
|
|
150
|
+
|
|
151
|
+
export { type DomCaptureOptions, type MarkdownPdfDeps, captureBlockElement, captureElementsToPDF, captureElementsToPDFBlob, captureToClipboardImage, captureToPDF, captureToPDFBlob, createDomCapturePrinter, markdownToPdfBlob, sanitizeForCapture };
|
package/dist/pdf.d.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { BlockPrinter } from './core.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DOM-Capture Print Utilities — Tier 2 Full-Message Print
|
|
5
|
+
*
|
|
6
|
+
* Captures a rendered DOM element using html2canvas, then pipes it into jsPDF.
|
|
7
|
+
* This handles ALL block types because it screenshots the actual rendered output.
|
|
8
|
+
*
|
|
9
|
+
* Problem: Tailwind CSS 4 uses modern CSS color functions (oklch, lab, color(display-p3))
|
|
10
|
+
* that html2canvas cannot parse. It calls console.error for each one, which gets
|
|
11
|
+
* captured by AdminDebugContextCollector and shows up as false errors.
|
|
12
|
+
*
|
|
13
|
+
* Solution: Patch window.getComputedStyle during the capture so html2canvas always
|
|
14
|
+
* receives safe hex fallbacks instead of unparseable color function strings.
|
|
15
|
+
*
|
|
16
|
+
* ---
|
|
17
|
+
*
|
|
18
|
+
* This module is the packaged union of three source files that always shipped
|
|
19
|
+
* together:
|
|
20
|
+
* • dom-capture-print-utils — the capture/PDF renderers (above),
|
|
21
|
+
* • dom-capture-block-printer — the BlockPrinter factory + captureBlockElement,
|
|
22
|
+
* • markdown-pdf — markdownToPdfBlob.
|
|
23
|
+
*
|
|
24
|
+
* Package-level seams (the only deliberate departures from the originals):
|
|
25
|
+
* • `markdownToPdfBlob` takes its markdown→HTML converter and its stylesheet
|
|
26
|
+
* loader INJECTED (`MarkdownPdfDeps`) instead of dynamically importing app
|
|
27
|
+
* modules. The package cannot reach into an application's feature folders.
|
|
28
|
+
* • `sanitizeForCapture` is a package-added export (it was module-private in
|
|
29
|
+
* the original) so the sanitizer can be tested on its own.
|
|
30
|
+
* • `captureBlockElement` calls `captureToPDF` directly rather than through a
|
|
31
|
+
* dynamic import — html2canvas and jspdf are still lazily imported inside
|
|
32
|
+
* the render functions, which is the laziness that actually matters.
|
|
33
|
+
*
|
|
34
|
+
* html2canvas and jspdf stay behind call-time dynamic imports: importing this
|
|
35
|
+
* module pulls in neither, and the module touches `window`/`document` only when
|
|
36
|
+
* one of its functions is called.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
interface DomCaptureOptions {
|
|
40
|
+
/** Paper size — 'a4' (default) or 'letter' */
|
|
41
|
+
paperSize?: "a4" | "letter";
|
|
42
|
+
/** Orientation */
|
|
43
|
+
orientation?: "portrait" | "landscape";
|
|
44
|
+
/** Scale factor for html2canvas (higher = sharper, slower). Default: 2 */
|
|
45
|
+
scale?: number;
|
|
46
|
+
/** Background colour for the capture. Default: '#ffffff' */
|
|
47
|
+
background?: string;
|
|
48
|
+
/** Filename for the downloaded PDF (without extension). Default: 'ai-response' */
|
|
49
|
+
filename?: string;
|
|
50
|
+
/** Image encoding inside the PDF. PNG is lossless; JPEG is much smaller for reports. */
|
|
51
|
+
imageFormat?: "png" | "jpeg";
|
|
52
|
+
/** JPEG quality from 0 to 1. Ignored for PNG. Default: 0.92. */
|
|
53
|
+
imageQuality?: number;
|
|
54
|
+
/** Called after each page is captured, with current page / total estimate */
|
|
55
|
+
onProgress?: (page: number, total: number) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Color-fallback theme for unparseable (oklch/lab) computed colors.
|
|
58
|
+
* "auto" (default) follows the app's dark class — right for capturing
|
|
59
|
+
* on-screen UI. Pass "light" for always-light offscreen document renders
|
|
60
|
+
* so dark-mode fallbacks never bleed into a white-background PDF.
|
|
61
|
+
*/
|
|
62
|
+
theme?: "auto" | "light";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Capture a DOM element and export it as a multi-page PDF download.
|
|
66
|
+
*/
|
|
67
|
+
declare function captureToPDF(element: HTMLElement, options?: DomCaptureOptions): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Capture a DOM element and return the multi-page PDF as a Blob — for
|
|
70
|
+
* callers that upload/persist the PDF instead of downloading it (e.g. the
|
|
71
|
+
* chat "Save as → PDF Document" action saving into the user's Files).
|
|
72
|
+
*/
|
|
73
|
+
declare function captureToPDFBlob(element: HTMLElement, options?: DomCaptureOptions): Promise<Blob>;
|
|
74
|
+
/**
|
|
75
|
+
* Capture deliberately paginated DOM elements as one PDF page each.
|
|
76
|
+
*
|
|
77
|
+
* Use this for designed reports whose page boundaries are part of the layout.
|
|
78
|
+
* The ordinary capture helpers intentionally slice one continuous canvas; this
|
|
79
|
+
* variant preserves explicit report pages and therefore cannot cut a section
|
|
80
|
+
* in half at an arbitrary paper boundary.
|
|
81
|
+
*/
|
|
82
|
+
declare function captureElementsToPDF(elements: HTMLElement[], options?: DomCaptureOptions): Promise<void>;
|
|
83
|
+
/** Blob form of captureElementsToPDF for durable file-system uploads. */
|
|
84
|
+
declare function captureElementsToPDFBlob(elements: HTMLElement[], options?: DomCaptureOptions): Promise<Blob>;
|
|
85
|
+
/**
|
|
86
|
+
* Capture a DOM element and copy it to clipboard (or download) as PNG.
|
|
87
|
+
*/
|
|
88
|
+
declare function captureToClipboardImage(element: HTMLElement): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* DOM-Capture BlockPrinter factory
|
|
91
|
+
*
|
|
92
|
+
* Creates a BlockPrinter that captures a provided DOM element and exports it as PDF.
|
|
93
|
+
* Used by visual blocks (Timeline, ComparisonTable, DecisionTree, Diagram) where
|
|
94
|
+
* the rendered DOM is the most faithful representation.
|
|
95
|
+
*
|
|
96
|
+
* Usage inside a block component:
|
|
97
|
+
* const { captureRef, printAction } = useBlockDomCapture('Timeline');
|
|
98
|
+
* // attach captureRef to the block's root element
|
|
99
|
+
* // add printAction to customActions or controls
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* Create a BlockPrinter backed by DOM capture.
|
|
103
|
+
* The printer's `print(data)` call is a no-op — actual printing is triggered
|
|
104
|
+
* imperatively via `captureElement(el)`.
|
|
105
|
+
*
|
|
106
|
+
* This exists so the block can register itself in the BlockPrinter contract
|
|
107
|
+
* while keeping the DOM reference local to the component.
|
|
108
|
+
*/
|
|
109
|
+
declare function createDomCapturePrinter(blockLabel: string): BlockPrinter;
|
|
110
|
+
/**
|
|
111
|
+
* Capture a DOM element as PDF.
|
|
112
|
+
* Called directly by block components when their print button is clicked.
|
|
113
|
+
*
|
|
114
|
+
* @param element - The DOM element to capture
|
|
115
|
+
* @param filename - Output filename (without extension)
|
|
116
|
+
* @param orientation - "landscape" (default) for wide visual blocks; "portrait" for tall/list content
|
|
117
|
+
*/
|
|
118
|
+
declare function captureBlockElement(element: HTMLElement, filename: string, orientation?: "landscape" | "portrait"): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* markdownToPdfBlob — render a markdown string to a styled, multi-page PDF
|
|
121
|
+
* Blob without any visible UI.
|
|
122
|
+
*
|
|
123
|
+
* Pipeline: markdown → WordPress-styled HTML (the same converter the
|
|
124
|
+
* publish/copy-HTML flows use) → sanitized offscreen DOM host → html2canvas +
|
|
125
|
+
* jsPDF via `captureToPDFBlob`. Everything heavy stays lazy-loaded behind
|
|
126
|
+
* this module.
|
|
127
|
+
*
|
|
128
|
+
* Used by the chat "Save as → PDF Document" action, which uploads the blob
|
|
129
|
+
* into the user's Files. Reuse this for any "content → PDF file" need — do
|
|
130
|
+
* not fork another markdown-to-pdf path.
|
|
131
|
+
*/
|
|
132
|
+
/**
|
|
133
|
+
* The two application-owned pieces markdownToPdfBlob needs. The original
|
|
134
|
+
* reached for them with dynamic imports of app feature modules; a package
|
|
135
|
+
* takes them from its caller instead.
|
|
136
|
+
*/
|
|
137
|
+
interface MarkdownPdfDeps {
|
|
138
|
+
/** markdown → HTML body string (the same converter the publish/copy-HTML flows use) */
|
|
139
|
+
convertToHtml: (markdown: string) => string;
|
|
140
|
+
/** the class-scoped stylesheet text the rendered HTML expects */
|
|
141
|
+
loadCss: () => Promise<string> | string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Markdown can carry raw HTML; we are about to attach it to the live
|
|
145
|
+
* document, so strip anything executable. html2canvas ignores scripts, but
|
|
146
|
+
* inline handlers (`onerror`) and javascript: URLs would fire on attach.
|
|
147
|
+
*/
|
|
148
|
+
declare function sanitizeForCapture(html: string): DocumentFragment;
|
|
149
|
+
declare function markdownToPdfBlob(markdown: string, deps: MarkdownPdfDeps): Promise<Blob>;
|
|
150
|
+
|
|
151
|
+
export { type DomCaptureOptions, type MarkdownPdfDeps, captureBlockElement, captureElementsToPDF, captureElementsToPDFBlob, captureToClipboardImage, captureToPDF, captureToPDFBlob, createDomCapturePrinter, markdownToPdfBlob, sanitizeForCapture };
|
package/dist/pdf.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// src/pdf.ts
|
|
2
|
+
var PAGE_DIMS = {
|
|
3
|
+
a4: { w: 210, h: 297 },
|
|
4
|
+
letter: { w: 215.9, h: 279.4 }
|
|
5
|
+
};
|
|
6
|
+
var UNSAFE_COLOR_RE = /\b(oklch|oklab|lab|lch|color)\s*\(/i;
|
|
7
|
+
function isSafeColor(value) {
|
|
8
|
+
return !UNSAFE_COLOR_RE.test(value);
|
|
9
|
+
}
|
|
10
|
+
function safenColor(value, prop, isDark) {
|
|
11
|
+
if (isSafeColor(value)) return value;
|
|
12
|
+
const lp = prop.toLowerCase();
|
|
13
|
+
if (lp.includes("background")) return isDark ? "#1f2937" : "#ffffff";
|
|
14
|
+
if (lp === "color") return isDark ? "#f3f4f6" : "#111827";
|
|
15
|
+
if (lp.includes("border") || lp.includes("outline"))
|
|
16
|
+
return isDark ? "#4b5563" : "#d1d5db";
|
|
17
|
+
if (lp === "fill") return isDark ? "#9ca3af" : "#374151";
|
|
18
|
+
if (lp === "stroke") return isDark ? "#6b7280" : "#6b7280";
|
|
19
|
+
return isDark ? "#6b7280" : "#6b7280";
|
|
20
|
+
}
|
|
21
|
+
function patchGetComputedStyle(isDark) {
|
|
22
|
+
const original = window.getComputedStyle;
|
|
23
|
+
const callOriginal = original.bind(window);
|
|
24
|
+
window.getComputedStyle = function patchedGetComputedStyle(elt, pseudoElt) {
|
|
25
|
+
const computed = callOriginal(elt, pseudoElt);
|
|
26
|
+
return new Proxy(computed, {
|
|
27
|
+
get(target, prop) {
|
|
28
|
+
const value = Reflect.get(target, prop, target);
|
|
29
|
+
if (typeof prop === "string" && typeof value === "string" && value) {
|
|
30
|
+
if (!isSafeColor(value)) {
|
|
31
|
+
return safenColor(value, prop, isDark);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (prop === "getPropertyValue") {
|
|
35
|
+
return (propName) => {
|
|
36
|
+
const raw = target.getPropertyValue(propName);
|
|
37
|
+
if (raw && !isSafeColor(raw)) {
|
|
38
|
+
return safenColor(raw, propName, isDark);
|
|
39
|
+
}
|
|
40
|
+
return raw;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "function") {
|
|
44
|
+
const fn = value;
|
|
45
|
+
return fn.bind(target);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
return () => {
|
|
52
|
+
window.getComputedStyle = original;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function captureToPDF(element, options = {}) {
|
|
56
|
+
const pdf = await renderElementToPdf(element, options);
|
|
57
|
+
pdf.save(`${options.filename ?? "ai-response"}.pdf`);
|
|
58
|
+
}
|
|
59
|
+
async function captureToPDFBlob(element, options = {}) {
|
|
60
|
+
const pdf = await renderElementToPdf(element, options);
|
|
61
|
+
return pdf.output("blob");
|
|
62
|
+
}
|
|
63
|
+
async function captureElementsToPDF(elements, options = {}) {
|
|
64
|
+
const pdf = await renderElementsToPdf(elements, options);
|
|
65
|
+
pdf.save(`${options.filename ?? "ai-response"}.pdf`);
|
|
66
|
+
}
|
|
67
|
+
async function captureElementsToPDFBlob(elements, options = {}) {
|
|
68
|
+
const pdf = await renderElementsToPdf(elements, options);
|
|
69
|
+
return pdf.output("blob");
|
|
70
|
+
}
|
|
71
|
+
async function renderElementsToPdf(elements, options) {
|
|
72
|
+
if (elements.length === 0) {
|
|
73
|
+
throw new Error("No report pages are available to export.");
|
|
74
|
+
}
|
|
75
|
+
const {
|
|
76
|
+
paperSize = "a4",
|
|
77
|
+
orientation = "portrait",
|
|
78
|
+
scale = 2,
|
|
79
|
+
background = "#ffffff",
|
|
80
|
+
imageFormat = "png",
|
|
81
|
+
imageQuality = 0.92,
|
|
82
|
+
onProgress
|
|
83
|
+
} = options;
|
|
84
|
+
const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([
|
|
85
|
+
import("html2canvas"),
|
|
86
|
+
import("jspdf")
|
|
87
|
+
]);
|
|
88
|
+
const dims = PAGE_DIMS[paperSize];
|
|
89
|
+
const pageW = orientation === "portrait" ? dims.w : dims.h;
|
|
90
|
+
const pageH = orientation === "portrait" ? dims.h : dims.w;
|
|
91
|
+
const pdf = new jsPDF({ orientation, unit: "mm", format: paperSize });
|
|
92
|
+
const restoreGetComputedStyle = patchGetComputedStyle(false);
|
|
93
|
+
try {
|
|
94
|
+
for (const [index, element] of elements.entries()) {
|
|
95
|
+
onProgress?.(index, elements.length);
|
|
96
|
+
const canvas = await html2canvas(element, {
|
|
97
|
+
scale,
|
|
98
|
+
useCORS: true,
|
|
99
|
+
backgroundColor: background,
|
|
100
|
+
logging: false,
|
|
101
|
+
windowWidth: element.scrollWidth,
|
|
102
|
+
windowHeight: element.scrollHeight
|
|
103
|
+
});
|
|
104
|
+
if (index > 0) pdf.addPage();
|
|
105
|
+
const isJpeg = imageFormat === "jpeg";
|
|
106
|
+
pdf.addImage(
|
|
107
|
+
canvas.toDataURL(
|
|
108
|
+
isJpeg ? "image/jpeg" : "image/png",
|
|
109
|
+
isJpeg ? imageQuality : void 0
|
|
110
|
+
),
|
|
111
|
+
isJpeg ? "JPEG" : "PNG",
|
|
112
|
+
0,
|
|
113
|
+
0,
|
|
114
|
+
pageW,
|
|
115
|
+
pageH
|
|
116
|
+
);
|
|
117
|
+
onProgress?.(index + 1, elements.length);
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
restoreGetComputedStyle();
|
|
121
|
+
}
|
|
122
|
+
return pdf;
|
|
123
|
+
}
|
|
124
|
+
async function renderElementToPdf(element, options = {}) {
|
|
125
|
+
const {
|
|
126
|
+
paperSize = "a4",
|
|
127
|
+
orientation = "portrait",
|
|
128
|
+
scale = 2,
|
|
129
|
+
background = "#ffffff",
|
|
130
|
+
imageFormat = "png",
|
|
131
|
+
imageQuality = 0.92,
|
|
132
|
+
onProgress
|
|
133
|
+
} = options;
|
|
134
|
+
const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([
|
|
135
|
+
import("html2canvas"),
|
|
136
|
+
import("jspdf")
|
|
137
|
+
]);
|
|
138
|
+
const dims = PAGE_DIMS[paperSize];
|
|
139
|
+
const pageW = orientation === "portrait" ? dims.w : dims.h;
|
|
140
|
+
const pageH = orientation === "portrait" ? dims.h : dims.w;
|
|
141
|
+
onProgress?.(0, 1);
|
|
142
|
+
const isDark = options.theme === "light" ? false : document.documentElement.classList.contains("dark") || window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
143
|
+
const bgColor = background === "#ffffff" && isDark ? "#111827" : background;
|
|
144
|
+
const restoreGetComputedStyle = patchGetComputedStyle(isDark);
|
|
145
|
+
let canvas;
|
|
146
|
+
try {
|
|
147
|
+
canvas = await html2canvas(element, {
|
|
148
|
+
scale,
|
|
149
|
+
useCORS: true,
|
|
150
|
+
backgroundColor: bgColor,
|
|
151
|
+
logging: false,
|
|
152
|
+
windowWidth: element.scrollWidth,
|
|
153
|
+
windowHeight: element.scrollHeight
|
|
154
|
+
});
|
|
155
|
+
} finally {
|
|
156
|
+
restoreGetComputedStyle();
|
|
157
|
+
}
|
|
158
|
+
const isJpeg = imageFormat === "jpeg";
|
|
159
|
+
const imgData = canvas.toDataURL(
|
|
160
|
+
isJpeg ? "image/jpeg" : "image/png",
|
|
161
|
+
isJpeg ? imageQuality : void 0
|
|
162
|
+
);
|
|
163
|
+
const imgFormat = isJpeg ? "JPEG" : "PNG";
|
|
164
|
+
const imgW = pageW;
|
|
165
|
+
const imgH = canvas.height * pageW / canvas.width;
|
|
166
|
+
const pdf = new jsPDF({ orientation, unit: "mm", format: paperSize });
|
|
167
|
+
let heightLeft = imgH;
|
|
168
|
+
let yOffset = 0;
|
|
169
|
+
let page = 0;
|
|
170
|
+
pdf.addImage(imgData, imgFormat, 0, yOffset, imgW, imgH);
|
|
171
|
+
heightLeft -= pageH;
|
|
172
|
+
page++;
|
|
173
|
+
const totalPages = Math.ceil(imgH / pageH);
|
|
174
|
+
while (heightLeft > 0) {
|
|
175
|
+
yOffset -= pageH;
|
|
176
|
+
pdf.addPage();
|
|
177
|
+
pdf.addImage(imgData, imgFormat, 0, yOffset, imgW, imgH);
|
|
178
|
+
heightLeft -= pageH;
|
|
179
|
+
page++;
|
|
180
|
+
onProgress?.(page, totalPages);
|
|
181
|
+
}
|
|
182
|
+
return pdf;
|
|
183
|
+
}
|
|
184
|
+
async function captureToClipboardImage(element) {
|
|
185
|
+
const { default: html2canvas } = await import("html2canvas");
|
|
186
|
+
const isDark = document.documentElement.classList.contains("dark") || window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
187
|
+
const restoreGetComputedStyle = patchGetComputedStyle(isDark);
|
|
188
|
+
let canvas;
|
|
189
|
+
try {
|
|
190
|
+
canvas = await html2canvas(element, {
|
|
191
|
+
scale: 2,
|
|
192
|
+
useCORS: true,
|
|
193
|
+
backgroundColor: isDark ? "#111827" : "#ffffff",
|
|
194
|
+
logging: false
|
|
195
|
+
});
|
|
196
|
+
} finally {
|
|
197
|
+
restoreGetComputedStyle();
|
|
198
|
+
}
|
|
199
|
+
await new Promise((resolve, reject) => {
|
|
200
|
+
canvas.toBlob(async (blob) => {
|
|
201
|
+
if (!blob) {
|
|
202
|
+
resolve();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
try {
|
|
207
|
+
await navigator.clipboard.write([
|
|
208
|
+
new ClipboardItem({ "image/png": blob })
|
|
209
|
+
]);
|
|
210
|
+
} catch {
|
|
211
|
+
const url = URL.createObjectURL(blob);
|
|
212
|
+
const a = document.createElement("a");
|
|
213
|
+
a.href = url;
|
|
214
|
+
a.download = "capture.png";
|
|
215
|
+
document.body.appendChild(a);
|
|
216
|
+
a.click();
|
|
217
|
+
document.body.removeChild(a);
|
|
218
|
+
URL.revokeObjectURL(url);
|
|
219
|
+
}
|
|
220
|
+
resolve();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
223
|
+
}
|
|
224
|
+
}, "image/png");
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function createDomCapturePrinter(blockLabel) {
|
|
228
|
+
return {
|
|
229
|
+
label: `Print ${blockLabel}`,
|
|
230
|
+
variants: [],
|
|
231
|
+
// No-op: DOM capture blocks use captureElement() directly
|
|
232
|
+
print: () => {
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
async function captureBlockElement(element, filename, orientation = "landscape") {
|
|
237
|
+
await captureToPDF(element, {
|
|
238
|
+
filename,
|
|
239
|
+
scale: 2,
|
|
240
|
+
orientation
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function sanitizeForCapture(html) {
|
|
244
|
+
const template = document.createElement("template");
|
|
245
|
+
template.innerHTML = html;
|
|
246
|
+
const fragment = template.content;
|
|
247
|
+
fragment.querySelectorAll("script, iframe, object, embed, link, meta").forEach((el) => el.remove());
|
|
248
|
+
fragment.querySelectorAll("*").forEach((el) => {
|
|
249
|
+
for (const attr of Array.from(el.attributes)) {
|
|
250
|
+
const name = attr.name.toLowerCase();
|
|
251
|
+
if (name.startsWith("on")) el.removeAttribute(attr.name);
|
|
252
|
+
if ((name === "href" || name === "src") && attr.value.trim().toLowerCase().startsWith("javascript:")) {
|
|
253
|
+
el.removeAttribute(attr.name);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
return fragment;
|
|
258
|
+
}
|
|
259
|
+
async function markdownToPdfBlob(markdown, deps) {
|
|
260
|
+
if (!markdown.trim()) {
|
|
261
|
+
throw new Error("Nothing to convert \u2014 the content is empty.");
|
|
262
|
+
}
|
|
263
|
+
const bodyHtml = deps.convertToHtml(markdown);
|
|
264
|
+
const css = await deps.loadCss();
|
|
265
|
+
const host = document.createElement("div");
|
|
266
|
+
host.style.cssText = "position:fixed;left:-10000px;top:0;width:800px;background:#ffffff;color:#111827;pointer-events:none;";
|
|
267
|
+
host.setAttribute("aria-hidden", "true");
|
|
268
|
+
const style = document.createElement("style");
|
|
269
|
+
style.textContent = css;
|
|
270
|
+
host.appendChild(style);
|
|
271
|
+
const contentEl = document.createElement("div");
|
|
272
|
+
contentEl.className = "matrx-content-container";
|
|
273
|
+
contentEl.style.cssText = "padding:32px;background:#ffffff;";
|
|
274
|
+
contentEl.appendChild(sanitizeForCapture(bodyHtml));
|
|
275
|
+
host.appendChild(contentEl);
|
|
276
|
+
document.body.appendChild(host);
|
|
277
|
+
try {
|
|
278
|
+
return await captureToPDFBlob(host, { background: "#fff", theme: "light" });
|
|
279
|
+
} finally {
|
|
280
|
+
host.remove();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
export {
|
|
284
|
+
captureBlockElement,
|
|
285
|
+
captureElementsToPDF,
|
|
286
|
+
captureElementsToPDFBlob,
|
|
287
|
+
captureToClipboardImage,
|
|
288
|
+
captureToPDF,
|
|
289
|
+
captureToPDFBlob,
|
|
290
|
+
createDomCapturePrinter,
|
|
291
|
+
markdownToPdfBlob,
|
|
292
|
+
sanitizeForCapture
|
|
293
|
+
};
|
|
294
|
+
//# sourceMappingURL=pdf.js.map
|
package/dist/pdf.js.map
ADDED
|
@@ -0,0 +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":[]}
|