structured-render 0.0.1 → 0.0.3
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.
|
@@ -55,7 +55,9 @@ export const VirMarkdown = defineElement()({
|
|
|
55
55
|
class=${contentDivClass}
|
|
56
56
|
${onDomCreated((element) => {
|
|
57
57
|
if (element instanceof HTMLElement) {
|
|
58
|
-
updateState({
|
|
58
|
+
updateState({
|
|
59
|
+
renderedElement: element,
|
|
60
|
+
});
|
|
59
61
|
}
|
|
60
62
|
})}
|
|
61
63
|
>
|
|
@@ -4,10 +4,9 @@ import DOMPurify from 'dompurify';
|
|
|
4
4
|
import { convertTemplateToString, html } from 'element-vir';
|
|
5
5
|
import { marked } from 'marked';
|
|
6
6
|
import { importHtml2Pdf } from './html-to-pdf.js';
|
|
7
|
-
import { contentDivClass } from './render-markdown-styles.js';
|
|
7
|
+
import { baseContentResetStyles, contentDivClass } from './render-markdown-styles.js';
|
|
8
8
|
import { renderStructuredMarkdown } from './render-markdown.js';
|
|
9
9
|
import { defaultRenderMarkdownOptions, } from './render-types.js';
|
|
10
|
-
const globalStyleId = 'structured-rendering-style-id';
|
|
11
10
|
// cspell:disable
|
|
12
11
|
/**
|
|
13
12
|
* Output method types for jsPDF. Extracted from:
|
|
@@ -116,48 +115,141 @@ export async function renderInBrowser(structuredRenderData, { fileName, outputTy
|
|
|
116
115
|
throw new Error(`${renderInNode.name} cannot run outside of a browser.`);
|
|
117
116
|
}
|
|
118
117
|
const options = mergeDefinedProperties(defaultRenderMarkdownOptions, userOptions);
|
|
119
|
-
const htmlToPdf = await
|
|
118
|
+
const [htmlToPdf] = await Promise.all([
|
|
119
|
+
importHtml2Pdf(),
|
|
120
|
+
preloadHtml2Canvas(),
|
|
121
|
+
]);
|
|
120
122
|
const dirtyHtml = await marked.parse(renderStructuredMarkdown(structuredRenderData, options));
|
|
121
|
-
const styleElement = assertWrap.instanceOf(globalThis.document.head.querySelector(`#${globalStyleId}`) ||
|
|
122
|
-
globalThis.document.createElement('style'), HTMLStyleElement);
|
|
123
|
-
styleElement.id = globalStyleId;
|
|
124
|
-
if (!styleElement.isConnected) {
|
|
125
|
-
globalThis.document.head.append(styleElement);
|
|
126
|
-
}
|
|
127
|
-
styleElement.textContent = String(options.styles);
|
|
128
123
|
const htmlString = convertTemplateToString(html `
|
|
129
124
|
<div class=${contentDivClass}>${DOMPurify.sanitize(dirtyHtml)}</div>
|
|
130
125
|
`);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
126
|
+
/**
|
|
127
|
+
* Render in a clean iframe context so html2canvas only clones the minimal iframe document
|
|
128
|
+
* instead of the entire host page DOM (which is extremely slow on complex pages).
|
|
129
|
+
*/
|
|
130
|
+
const html2PdfOptions = createHtml2PdfOptions(fileName);
|
|
131
|
+
const canvas = await renderHtmlToCanvas(htmlString, String(options.styles), html2PdfOptions);
|
|
132
|
+
const instance = htmlToPdf().set(html2PdfOptions).from(canvas);
|
|
133
|
+
if (outputType.pdf) {
|
|
134
|
+
if (outputType.pdf === OutputPdfType.Download) {
|
|
135
|
+
return await instance.save(fileName);
|
|
143
136
|
}
|
|
144
|
-
else
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
137
|
+
else {
|
|
138
|
+
return await instance.outputPdf(outputType.pdf, {
|
|
139
|
+
filename: fileName,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
143
|
+
}
|
|
144
|
+
else if (outputType.image) {
|
|
145
|
+
if (outputType.image === OutputImageType.Download) {
|
|
146
|
+
return await instance.toImg().save(fileName);
|
|
151
147
|
}
|
|
152
148
|
else {
|
|
153
|
-
|
|
154
|
-
throw new Error(`Invalid output type: ${stringify(outputType)}`);
|
|
149
|
+
return await instance.outputImg(outputType.image);
|
|
155
150
|
}
|
|
156
151
|
}
|
|
152
|
+
else {
|
|
153
|
+
assert.tsType(outputType).equals();
|
|
154
|
+
throw new Error(`Invalid output type: ${stringify(outputType)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Render HTML content to a canvas using a clean, minimal iframe. This avoids the massive
|
|
159
|
+
* performance penalty from html2canvas cloning the entire host page document (including all custom
|
|
160
|
+
* elements, shadow DOM, and styles) just to render a small content div.
|
|
161
|
+
*/
|
|
162
|
+
async function renderHtmlToCanvas(htmlString, stylesString, html2PdfOptions) {
|
|
163
|
+
const marginArray = normalizeMargin(html2PdfOptions.margin);
|
|
164
|
+
/** A4 page width in mm. */
|
|
165
|
+
const pageWidthMm = 210;
|
|
166
|
+
const innerWidthMm = pageWidthMm - marginArray[1] - marginArray[3];
|
|
167
|
+
const iframe = globalThis.document.createElement('iframe');
|
|
168
|
+
iframe.style.position = 'fixed';
|
|
169
|
+
iframe.style.left = '-10000px';
|
|
170
|
+
iframe.style.top = '0';
|
|
171
|
+
iframe.style.width = `${innerWidthMm}mm`;
|
|
172
|
+
iframe.style.height = '0';
|
|
173
|
+
iframe.style.border = 'none';
|
|
174
|
+
iframe.setAttribute('aria-hidden', 'true');
|
|
175
|
+
iframe.srcdoc = [
|
|
176
|
+
'<!DOCTYPE html><html><head><style>',
|
|
177
|
+
baseContentResetStyles,
|
|
178
|
+
stylesString,
|
|
179
|
+
'</style></head><body style="margin:0;padding:0;">',
|
|
180
|
+
htmlString,
|
|
181
|
+
'</body></html>',
|
|
182
|
+
].join('');
|
|
183
|
+
globalThis.document.body.append(iframe);
|
|
184
|
+
try {
|
|
185
|
+
await new Promise((resolve) => {
|
|
186
|
+
iframe.addEventListener('load', () => {
|
|
187
|
+
resolve();
|
|
188
|
+
}, {
|
|
189
|
+
once: true,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
const iframeDoc = assertWrap.isDefined(iframe.contentDocument);
|
|
193
|
+
await iframeDoc.fonts.ready;
|
|
194
|
+
/**
|
|
195
|
+
* Elements from the iframe exist in a different window context, so `instanceof HTMLElement`
|
|
196
|
+
* (from the parent window) fails. Use assertWrap.isDefined since we control the HTML
|
|
197
|
+
* written to the iframe and know the first child is always an element.
|
|
198
|
+
*/
|
|
199
|
+
const contentElement = assertWrap.isDefined(iframeDoc.body.firstElementChild);
|
|
200
|
+
return await importHtml2Canvas()(contentElement, {
|
|
201
|
+
...html2PdfOptions.html2canvas,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
157
204
|
finally {
|
|
158
|
-
|
|
205
|
+
iframe.remove();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
let cachedHtml2Canvas;
|
|
209
|
+
function importHtml2Canvas() {
|
|
210
|
+
if (!cachedHtml2Canvas) {
|
|
211
|
+
throw new Error('html2canvas has not been loaded yet. Call preloadHtml2Canvas() first.');
|
|
212
|
+
}
|
|
213
|
+
return cachedHtml2Canvas;
|
|
214
|
+
}
|
|
215
|
+
async function preloadHtml2Canvas() {
|
|
216
|
+
if (!cachedHtml2Canvas) {
|
|
217
|
+
// @ts-expect-error: html2canvas is a CJS module that doesn't properly resolve under node16 module resolution.
|
|
218
|
+
cachedHtml2Canvas = (await import('html2canvas')).default;
|
|
159
219
|
}
|
|
160
220
|
}
|
|
221
|
+
function normalizeMargin(margin) {
|
|
222
|
+
if (margin == undefined) {
|
|
223
|
+
return [
|
|
224
|
+
0,
|
|
225
|
+
0,
|
|
226
|
+
0,
|
|
227
|
+
0,
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
if (typeof margin === 'number') {
|
|
231
|
+
return [
|
|
232
|
+
margin,
|
|
233
|
+
margin,
|
|
234
|
+
margin,
|
|
235
|
+
margin,
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
if (margin.length === 2) {
|
|
239
|
+
return [
|
|
240
|
+
margin[0],
|
|
241
|
+
margin[1],
|
|
242
|
+
margin[0],
|
|
243
|
+
margin[1],
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
return [
|
|
247
|
+
margin[0],
|
|
248
|
+
margin[1],
|
|
249
|
+
margin[2],
|
|
250
|
+
margin[3],
|
|
251
|
+
];
|
|
252
|
+
}
|
|
161
253
|
/**
|
|
162
254
|
* Internal common Node.js structure render renderer.
|
|
163
255
|
*
|
|
@@ -187,12 +279,20 @@ export async function renderInNode(structuredRenderData, { saveLocationPath, out
|
|
|
187
279
|
const browser = await chromium.launch();
|
|
188
280
|
try {
|
|
189
281
|
const page = await browser.newPage();
|
|
190
|
-
await page.setContent(baseHtmlString, {
|
|
191
|
-
|
|
192
|
-
|
|
282
|
+
await page.setContent(baseHtmlString, {
|
|
283
|
+
waitUntil: 'networkidle',
|
|
284
|
+
});
|
|
285
|
+
await page.addScriptTag({
|
|
286
|
+
content: html2pdfScript,
|
|
287
|
+
});
|
|
288
|
+
await page.addScriptTag({
|
|
289
|
+
content: domPurifyScript,
|
|
290
|
+
});
|
|
193
291
|
const html2pdfOptions = createHtml2PdfOptions(basename(saveLocationPath));
|
|
194
292
|
if (outputType.image) {
|
|
195
|
-
html2pdfOptions.image = {
|
|
293
|
+
html2pdfOptions.image = {
|
|
294
|
+
type: 'png',
|
|
295
|
+
};
|
|
196
296
|
}
|
|
197
297
|
const pdfBase64 = await page.evaluate(async ({ html2pdfOptions, outputType, dirtyMarkdown, wrapperClass, outputImageType, outputPdfType, }) => {
|
|
198
298
|
const cleanHtml = DOMPurify.sanitize(dirtyMarkdown);
|
|
@@ -223,7 +323,9 @@ export async function renderInNode(structuredRenderData, { saveLocationPath, out
|
|
|
223
323
|
outputPdfType: OutputPdfType.DataUriString,
|
|
224
324
|
});
|
|
225
325
|
const base64Data = pdfBase64.split(',')[1];
|
|
226
|
-
await mkdir(dirname(saveLocationPath), {
|
|
326
|
+
await mkdir(dirname(saveLocationPath), {
|
|
327
|
+
recursive: true,
|
|
328
|
+
});
|
|
227
329
|
await writeFile(saveLocationPath, Buffer.from(base64Data, 'base64'));
|
|
228
330
|
}
|
|
229
331
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "structured-render",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "A library for safely rendering arbitrary data generated from any source.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"render",
|
|
@@ -39,22 +39,23 @@
|
|
|
39
39
|
"test:web": "virmator test web"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@augment-vir/assert": "^31.
|
|
43
|
-
"@augment-vir/common": "^31.
|
|
44
|
-
"@augment-vir/web": "^31.
|
|
42
|
+
"@augment-vir/assert": "^31.67.0",
|
|
43
|
+
"@augment-vir/common": "^31.67.0",
|
|
44
|
+
"@augment-vir/web": "^31.67.0",
|
|
45
45
|
"@electrovir/color": "^1.7.8",
|
|
46
46
|
"dompurify": "^3.3.1",
|
|
47
|
-
"element-vir": "^26.14.
|
|
47
|
+
"element-vir": "^26.14.4",
|
|
48
|
+
"html2canvas": "^1.4.1",
|
|
48
49
|
"html2pdf.js": "^0.14.0",
|
|
49
50
|
"lit-css-vars": "^3.5.0",
|
|
50
51
|
"marked": "^17.0.3",
|
|
51
52
|
"object-shape-tester": "^6.11.0",
|
|
52
53
|
"theme-vir": "^28.22.0",
|
|
53
54
|
"type-fest": "^5.4.4",
|
|
54
|
-
"vira": "^29.
|
|
55
|
+
"vira": "^29.7.2"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
|
-
"@augment-vir/test": "^31.
|
|
58
|
+
"@augment-vir/test": "^31.67.0",
|
|
58
59
|
"@virmator/test": "^14.7.2",
|
|
59
60
|
"@web/dev-server-esbuild": "^1.0.5",
|
|
60
61
|
"@web/test-runner": "^0.20.2",
|