docgen-utils 1.0.15 → 1.0.17
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/bundle.js +126 -48
- package/dist/bundle.min.js +2 -2
- package/dist/cli.js +330 -24017
- package/dist/packages/cli/commands/export-docs.d.ts +3 -4
- package/dist/packages/cli/commands/export-docs.d.ts.map +1 -1
- package/dist/packages/cli/commands/export-docs.js +245 -299
- package/dist/packages/cli/commands/export-docs.js.map +1 -1
- package/dist/packages/docs/create-document.d.ts.map +1 -1
- package/dist/packages/docs/create-document.js +39 -22
- package/dist/packages/docs/create-document.js.map +1 -1
- package/dist/packages/docs/export.d.ts.map +1 -1
- package/dist/packages/docs/export.js +25 -89
- package/dist/packages/docs/export.js.map +1 -1
- package/dist/packages/docs/parse-colors.d.ts.map +1 -1
- package/dist/packages/docs/parse-colors.js +85 -10
- package/dist/packages/docs/parse-colors.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Export docs command - converts HTML to DOCX
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* - SVG charts
|
|
7
|
-
* - Data URLs: passed through directly
|
|
4
|
+
* Uses Playwright to:
|
|
5
|
+
* - Fetch external images via browser canvas (accurate naturalWidth/naturalHeight)
|
|
6
|
+
* - Render SVG charts by screenshotting live DOM elements (preserves CSS context)
|
|
8
7
|
*/
|
|
9
8
|
export interface ExportDocsOptions {
|
|
10
9
|
name?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export-docs.d.ts","sourceRoot":"","sources":["../../../../packages/cli/commands/export-docs.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"export-docs.d.ts","sourceRoot":"","sources":["../../../../packages/cli/commands/export-docs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAuTH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDjH"}
|
|
@@ -1,338 +1,270 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Export docs command - converts HTML to DOCX
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* - SVG charts
|
|
7
|
-
* - Data URLs: passed through directly
|
|
4
|
+
* Uses Playwright to:
|
|
5
|
+
* - Fetch external images via browser canvas (accurate naturalWidth/naturalHeight)
|
|
6
|
+
* - Render SVG charts by screenshotting live DOM elements (preserves CSS context)
|
|
8
7
|
*/
|
|
9
8
|
import * as fs from "node:fs";
|
|
10
9
|
import * as path from "node:path";
|
|
11
10
|
import { chromium } from "playwright";
|
|
12
11
|
import { createDocxBuffer } from "../../docs/create-document";
|
|
13
12
|
import { parseHtmlContent } from "../../docs/parse";
|
|
14
|
-
|
|
13
|
+
// Maximum content width in DOCX (6.5 inches at 96 DPI = 624 pixels)
|
|
14
|
+
// docx library ImageRun uses pixels: width * 9525 EMU/px
|
|
15
|
+
const MAX_IMAGE_WIDTH = 624;
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* Fetch external images from the HTML page using Playwright browser canvas.
|
|
18
|
+
* Opens the actual HTML file so images load naturally, then extracts pixel data
|
|
19
|
+
* via canvas with accurate naturalWidth/naturalHeight dimensions.
|
|
18
20
|
*/
|
|
19
|
-
function
|
|
20
|
-
const images = [];
|
|
21
|
-
for (const el of elements) {
|
|
22
|
-
if (el.type === "image") {
|
|
23
|
-
images.push(el);
|
|
24
|
-
}
|
|
25
|
-
// Recursively search blockquote/callout content
|
|
26
|
-
if (el.type === "blockquote" && el.content) {
|
|
27
|
-
images.push(...findAllImageElements(el.content));
|
|
28
|
-
}
|
|
29
|
-
// Recursively search two-column-layout content
|
|
30
|
-
if (el.type === "two-column-layout") {
|
|
31
|
-
images.push(...findAllImageElements(el.sidebar.content));
|
|
32
|
-
images.push(...findAllImageElements(el.main.content));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return images;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Fetch external images from parsed elements and convert to image data.
|
|
39
|
-
* Uses Node.js native fetch API to download images.
|
|
40
|
-
* Returns a Map of image src URL to ChartImageData.
|
|
41
|
-
*/
|
|
42
|
-
async function fetchExternalImages(elements) {
|
|
21
|
+
async function fetchExternalImages(browser, htmlPath) {
|
|
43
22
|
const imageMap = new Map();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
23
|
+
const absoluteHtmlPath = path.resolve(htmlPath);
|
|
24
|
+
if (!fs.existsSync(absoluteHtmlPath)) {
|
|
25
|
+
return imageMap;
|
|
26
|
+
}
|
|
27
|
+
const page = await browser.newPage({
|
|
28
|
+
viewport: { width: 1200, height: 800 },
|
|
29
|
+
});
|
|
30
|
+
// Navigate to the HTML page to have proper context for relative URLs
|
|
31
|
+
await page.goto(`file://${absoluteHtmlPath}`, {
|
|
32
|
+
waitUntil: "networkidle",
|
|
33
|
+
});
|
|
34
|
+
// Wait for images to load
|
|
35
|
+
await page.waitForTimeout(1000);
|
|
36
|
+
// Find all unique image URLs in the page
|
|
37
|
+
const uniqueSrcs = await page.evaluate(() => {
|
|
38
|
+
const images = Array.from(document.querySelectorAll("img"));
|
|
39
|
+
const srcs = images.map(img => img.src).filter(src => src && !src.startsWith("data:"));
|
|
40
|
+
return [...new Set(srcs)];
|
|
41
|
+
});
|
|
42
|
+
if (uniqueSrcs.length === 0) {
|
|
43
|
+
await page.close();
|
|
49
44
|
return imageMap;
|
|
50
45
|
}
|
|
51
|
-
console.log(`Fetching ${
|
|
52
|
-
|
|
53
|
-
const maxWidth = 468;
|
|
54
|
-
for (const imgEl of externalImages) {
|
|
46
|
+
console.log(`Fetching ${uniqueSrcs.length} external image(s)...`);
|
|
47
|
+
for (const src of uniqueSrcs) {
|
|
55
48
|
try {
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
49
|
+
// Use Playwright to fetch and render the image via browser canvas
|
|
50
|
+
// This handles CORS and gives accurate naturalWidth/naturalHeight
|
|
51
|
+
const imageData = await page.evaluate(async ({ src, maxWidth }) => {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const img = new Image();
|
|
54
|
+
img.crossOrigin = "anonymous";
|
|
55
|
+
img.onload = () => {
|
|
56
|
+
try {
|
|
57
|
+
// Scale to fit within document width
|
|
58
|
+
let width = img.naturalWidth;
|
|
59
|
+
let height = img.naturalHeight;
|
|
60
|
+
if (width > maxWidth) {
|
|
61
|
+
const scale = maxWidth / width;
|
|
62
|
+
width = maxWidth;
|
|
63
|
+
height = Math.round(height * scale);
|
|
64
|
+
}
|
|
65
|
+
// Draw to canvas and get PNG data
|
|
66
|
+
const canvas = document.createElement("canvas");
|
|
67
|
+
canvas.width = width;
|
|
68
|
+
canvas.height = height;
|
|
69
|
+
const ctx = canvas.getContext("2d");
|
|
70
|
+
if (!ctx) {
|
|
71
|
+
resolve(null);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
75
|
+
// Get image data as base64
|
|
76
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
77
|
+
const base64 = dataUrl.split(",")[1];
|
|
78
|
+
if (!base64) {
|
|
79
|
+
resolve(null);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Convert base64 to byte array
|
|
83
|
+
const binaryString = atob(base64);
|
|
84
|
+
const bytes = new Array(binaryString.length);
|
|
85
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
86
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
87
|
+
}
|
|
88
|
+
resolve({ data: bytes, width, height });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.warn("Error processing image:", err);
|
|
92
|
+
resolve(null);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
img.onerror = () => {
|
|
96
|
+
console.warn("Failed to load image:", src);
|
|
97
|
+
resolve(null);
|
|
98
|
+
};
|
|
99
|
+
// Set timeout for slow images
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
console.warn("Image load timeout:", src);
|
|
102
|
+
resolve(null);
|
|
103
|
+
}, 10000);
|
|
104
|
+
img.src = src;
|
|
105
|
+
});
|
|
106
|
+
}, { src, maxWidth: MAX_IMAGE_WIDTH });
|
|
107
|
+
if (imageData) {
|
|
108
|
+
imageMap.set(src, {
|
|
109
|
+
data: Buffer.from(imageData.data),
|
|
110
|
+
width: imageData.width,
|
|
111
|
+
height: imageData.height,
|
|
112
|
+
});
|
|
75
113
|
}
|
|
76
|
-
// Scale to fit within document width
|
|
77
|
-
if (width > maxWidth) {
|
|
78
|
-
const scale = maxWidth / width;
|
|
79
|
-
height = Math.round(height * scale);
|
|
80
|
-
width = maxWidth;
|
|
81
|
-
}
|
|
82
|
-
imageMap.set(src, { data, width, height });
|
|
83
114
|
}
|
|
84
115
|
catch (err) {
|
|
85
|
-
console.warn(`
|
|
116
|
+
console.warn(`Failed to fetch image ${src}:`, err);
|
|
86
117
|
continue;
|
|
87
118
|
}
|
|
88
119
|
}
|
|
120
|
+
await page.close();
|
|
89
121
|
return imageMap;
|
|
90
122
|
}
|
|
91
123
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
124
|
+
* Render SVG chart elements to PNG by screenshotting them from the live HTML page.
|
|
125
|
+
*
|
|
126
|
+
* Opens the actual HTML file in Playwright, locates SVG elements using multiple
|
|
127
|
+
* matching criteria (viewBox, gradient/pattern IDs, aria-labels), and screenshots
|
|
128
|
+
* each matched element directly. This preserves full CSS context, fonts, and styling
|
|
129
|
+
* that would be lost when rendering SVG strings in isolation.
|
|
94
130
|
*/
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const width = data.readUInt32BE(16);
|
|
101
|
-
const height = data.readUInt32BE(20);
|
|
102
|
-
return { width, height };
|
|
131
|
+
async function renderChartImages(browser, htmlPath) {
|
|
132
|
+
const chartImages = new Map();
|
|
133
|
+
const absoluteHtmlPath = path.resolve(htmlPath);
|
|
134
|
+
if (!fs.existsSync(absoluteHtmlPath)) {
|
|
135
|
+
return chartImages;
|
|
103
136
|
}
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
137
|
+
// Parse the HTML using the library to get svg-chart elements
|
|
138
|
+
const htmlContent = fs.readFileSync(absoluteHtmlPath, "utf-8");
|
|
139
|
+
const elements = parseHtmlContent(htmlContent);
|
|
140
|
+
// Get svg-chart elements the library found, including those nested inside blockquotes and layouts
|
|
141
|
+
const svgChartElements = [];
|
|
142
|
+
function collectSvgCharts(els) {
|
|
143
|
+
for (const el of els) {
|
|
144
|
+
if (el.type === "svg-chart") {
|
|
145
|
+
svgChartElements.push(el);
|
|
111
146
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
147
|
+
else if (el.type === "blockquote" && el.content) {
|
|
148
|
+
collectSvgCharts(el.content);
|
|
149
|
+
}
|
|
150
|
+
else if (el.type === "two-column-layout") {
|
|
151
|
+
collectSvgCharts(el.sidebar.content);
|
|
152
|
+
collectSvgCharts(el.main.content);
|
|
118
153
|
}
|
|
119
|
-
// Skip to next marker
|
|
120
|
-
const length = data.readUInt16BE(offset + 2);
|
|
121
|
-
offset += 2 + length;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
// GIF: Check for GIF87a or GIF89a signature
|
|
125
|
-
if (data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46) {
|
|
126
|
-
const width = data.readUInt16LE(6);
|
|
127
|
-
const height = data.readUInt16LE(8);
|
|
128
|
-
return { width, height };
|
|
129
|
-
}
|
|
130
|
-
// WebP: Check for RIFF....WEBP signature
|
|
131
|
-
if (data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46 &&
|
|
132
|
-
data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50) {
|
|
133
|
-
// VP8 format (lossy)
|
|
134
|
-
if (data[12] === 0x56 && data[13] === 0x50 && data[14] === 0x38 && data[15] === 0x20) {
|
|
135
|
-
const width = data.readUInt16LE(26) & 0x3FFF;
|
|
136
|
-
const height = data.readUInt16LE(28) & 0x3FFF;
|
|
137
|
-
return { width, height };
|
|
138
|
-
}
|
|
139
|
-
// VP8L format (lossless)
|
|
140
|
-
if (data[12] === 0x56 && data[13] === 0x50 && data[14] === 0x38 && data[15] === 0x4C) {
|
|
141
|
-
const bits = data.readUInt32LE(21);
|
|
142
|
-
const width = (bits & 0x3FFF) + 1;
|
|
143
|
-
const height = ((bits >> 14) & 0x3FFF) + 1;
|
|
144
|
-
return { width, height };
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Render SVG chart elements to PNG images using Playwright browser canvas.
|
|
151
|
-
* This allows Node.js CLI to render SVGs the same way the browser export does.
|
|
152
|
-
*/
|
|
153
|
-
async function renderSvgChartsWithPlaywright(elements) {
|
|
154
|
-
const chartImages = new Map();
|
|
155
|
-
// Find all svg-chart elements
|
|
156
|
-
const svgCharts = elements.filter((el) => el.type === "svg-chart");
|
|
157
|
-
if (svgCharts.length === 0) {
|
|
156
|
+
collectSvgCharts(elements);
|
|
157
|
+
if (svgChartElements.length === 0) {
|
|
158
158
|
return chartImages;
|
|
159
159
|
}
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
160
|
+
console.log(`Rendering ${svgChartElements.length} SVG chart(s)...`);
|
|
161
|
+
const page = await browser.newPage({
|
|
162
|
+
viewport: { width: 1200, height: 800 },
|
|
163
|
+
});
|
|
164
|
+
await page.goto(`file://${absoluteHtmlPath}`, {
|
|
165
|
+
waitUntil: "networkidle",
|
|
166
|
+
});
|
|
167
|
+
// Wait for fonts/styles to settle
|
|
168
|
+
await page.waitForTimeout(500);
|
|
169
|
+
// For each svg-chart element from the library, find the matching SVG in the DOM
|
|
170
|
+
// by extracting unique identifiers from the svgContent
|
|
171
|
+
for (let i = 0; i < svgChartElements.length; i++) {
|
|
172
|
+
const svgChartEl = svgChartElements[i];
|
|
173
|
+
const svgContent = svgChartEl.svgContent;
|
|
174
|
+
// Extract multiple identifiers for robust matching
|
|
175
|
+
const viewBoxMatch = svgContent.match(/viewBox\s*=\s*["']([^"']+)["']/);
|
|
176
|
+
const viewBox = viewBoxMatch ? viewBoxMatch[1] : null;
|
|
177
|
+
// Extract first gradient/pattern ID as a unique identifier
|
|
178
|
+
const gradientIdMatch = svgContent.match(/id\s*=\s*["']([^"']+)["']/);
|
|
179
|
+
const firstId = gradientIdMatch ? gradientIdMatch[1] : null;
|
|
180
|
+
// Extract aria-label as another identifier
|
|
181
|
+
// Use separate patterns for double-quoted and single-quoted values to handle
|
|
182
|
+
// aria-labels that contain the other quote character (e.g., "Korea's cat culture")
|
|
183
|
+
const ariaLabelMatch = svgContent.match(/aria-label\s*=\s*"([^"]+)"/) || svgContent.match(/aria-label\s*=\s*'([^']+)'/);
|
|
184
|
+
const ariaLabel = ariaLabelMatch ? ariaLabelMatch[1] : null;
|
|
185
|
+
// Find the SVG with matching identifiers and mark IT directly (not its parent)
|
|
186
|
+
// This ensures we only screenshot the SVG, not surrounding DOM elements
|
|
187
|
+
const found = await page.evaluate(({ viewBox, firstId, ariaLabel, index }) => {
|
|
188
|
+
const svgs = Array.from(document.querySelectorAll("svg"));
|
|
189
|
+
for (const svg of svgs) {
|
|
190
|
+
// Skip already marked SVGs
|
|
191
|
+
if (svg.hasAttribute("data-svg-chart-index"))
|
|
192
|
+
continue;
|
|
193
|
+
// Match by multiple criteria for robustness
|
|
194
|
+
const svgViewBox = svg.getAttribute("viewBox");
|
|
195
|
+
const svgAriaLabel = svg.getAttribute("aria-label");
|
|
196
|
+
// Check for first ID match (gradient/pattern IDs are often unique per SVG)
|
|
197
|
+
let hasMatchingId = false;
|
|
198
|
+
if (firstId) {
|
|
199
|
+
const idEl = svg.querySelector(`#${CSS.escape(firstId)}`);
|
|
200
|
+
hasMatchingId = !!idEl;
|
|
201
|
+
}
|
|
202
|
+
// Match: viewBox + (firstId OR ariaLabel)
|
|
203
|
+
if (viewBox && svgViewBox === viewBox) {
|
|
204
|
+
if (hasMatchingId || (ariaLabel && svgAriaLabel === ariaLabel) || (!firstId && !ariaLabel)) {
|
|
205
|
+
svg.setAttribute("data-svg-chart-index", String(index));
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
180
208
|
}
|
|
181
209
|
}
|
|
182
|
-
|
|
183
|
-
|
|
210
|
+
return false;
|
|
211
|
+
}, { viewBox, firstId, ariaLabel, index: i });
|
|
212
|
+
if (!found) {
|
|
213
|
+
// Fallback: try to match by width/height if viewBox matching failed
|
|
214
|
+
const widthMatch = svgContent.match(/width\s*=\s*["']([^"']+)["']/);
|
|
215
|
+
const heightMatch = svgContent.match(/height\s*=\s*["']([^"']+)["']/);
|
|
216
|
+
const svgWidth = widthMatch ? widthMatch[1] : null;
|
|
217
|
+
const svgHeight = heightMatch ? heightMatch[1] : null;
|
|
218
|
+
if (svgWidth && svgHeight) {
|
|
219
|
+
await page.evaluate(({ w, h, firstId, index }) => {
|
|
220
|
+
const svgs = Array.from(document.querySelectorAll("svg"));
|
|
221
|
+
for (const svg of svgs) {
|
|
222
|
+
if (svg.getAttribute("width") === w && svg.getAttribute("height") === h) {
|
|
223
|
+
if (!svg.hasAttribute("data-svg-chart-index")) {
|
|
224
|
+
// Additional check: if we have firstId, verify it exists in this SVG
|
|
225
|
+
if (firstId) {
|
|
226
|
+
const idEl = svg.querySelector(`#${CSS.escape(firstId)}`);
|
|
227
|
+
if (!idEl)
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
svg.setAttribute("data-svg-chart-index", String(index));
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}, { w: svgWidth, h: svgHeight, firstId, index: i });
|
|
184
237
|
}
|
|
185
|
-
svgIndex++;
|
|
186
238
|
}
|
|
187
|
-
await page.close();
|
|
188
|
-
await context.close();
|
|
189
239
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Render a single SVG to PNG in the browser using Canvas.
|
|
197
|
-
*/
|
|
198
|
-
async function renderSvgInBrowser(page, svgContent, width, height, backgroundColor) {
|
|
199
|
-
// Create HTML page with the SVG rendering logic
|
|
200
|
-
const renderPage = `<!DOCTYPE html>
|
|
201
|
-
<html>
|
|
202
|
-
<head>
|
|
203
|
-
<style>
|
|
204
|
-
body { margin: 0; padding: 0; }
|
|
205
|
-
#svg-container { display: inline-block; }
|
|
206
|
-
</style>
|
|
207
|
-
</head>
|
|
208
|
-
<body>
|
|
209
|
-
<div id="svg-container"></div>
|
|
210
|
-
<canvas id="canvas"></canvas>
|
|
211
|
-
<script>
|
|
212
|
-
window.renderSvgToPng = async function(svgContent, width, height, backgroundColor) {
|
|
213
|
-
// Parse SVG to get dimensions if not provided
|
|
214
|
-
const parser = new DOMParser();
|
|
215
|
-
const svgDoc = parser.parseFromString(svgContent, "image/svg+xml");
|
|
216
|
-
const svgElement = svgDoc.querySelector("svg");
|
|
217
|
-
|
|
218
|
-
if (!svgElement) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Get dimensions from SVG
|
|
223
|
-
let svgWidth = width;
|
|
224
|
-
let svgHeight = height;
|
|
225
|
-
|
|
226
|
-
if (!svgWidth || !svgHeight) {
|
|
227
|
-
const viewBox = svgElement.getAttribute("viewBox");
|
|
228
|
-
if (viewBox) {
|
|
229
|
-
const parts = viewBox.split(/\\s+/).map(Number);
|
|
230
|
-
if (parts.length >= 4) {
|
|
231
|
-
svgWidth = svgWidth || parts[2];
|
|
232
|
-
svgHeight = svgHeight || parts[3];
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (!svgWidth) {
|
|
236
|
-
const widthAttr = svgElement.getAttribute("width");
|
|
237
|
-
if (widthAttr && !widthAttr.includes("%")) {
|
|
238
|
-
svgWidth = parseFloat(widthAttr);
|
|
239
|
-
}
|
|
240
|
+
// Screenshot each marked SVG element directly
|
|
241
|
+
for (let i = 0; i < svgChartElements.length; i++) {
|
|
242
|
+
const svgEl = await page.$(`svg[data-svg-chart-index="${i}"]`);
|
|
243
|
+
if (!svgEl) {
|
|
244
|
+
continue;
|
|
240
245
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
+
// Get bounding box
|
|
247
|
+
const box = await svgEl.boundingBox();
|
|
248
|
+
if (!box) {
|
|
249
|
+
continue;
|
|
246
250
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Serialize the modified SVG
|
|
263
|
-
const serializer = new XMLSerializer();
|
|
264
|
-
const svgString = serializer.serializeToString(svgElement);
|
|
265
|
-
|
|
266
|
-
// Create a blob URL for the SVG
|
|
267
|
-
const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
|
|
268
|
-
const svgUrl = URL.createObjectURL(svgBlob);
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
// Create an image from the SVG
|
|
272
|
-
const img = new Image();
|
|
273
|
-
await new Promise((resolve, reject) => {
|
|
274
|
-
img.onload = resolve;
|
|
275
|
-
img.onerror = reject;
|
|
276
|
-
img.src = svgUrl;
|
|
251
|
+
// Take screenshot of just this SVG element
|
|
252
|
+
const screenshot = await svgEl.screenshot({
|
|
253
|
+
type: "png",
|
|
254
|
+
});
|
|
255
|
+
// Scale to fit within document content width
|
|
256
|
+
const scale = box.width > MAX_IMAGE_WIDTH ? MAX_IMAGE_WIDTH / box.width : 1;
|
|
257
|
+
const width = Math.round(box.width * scale);
|
|
258
|
+
const height = Math.round(box.height * scale);
|
|
259
|
+
// Use svg-chart-{index} as the key to match the library's indexing
|
|
260
|
+
chartImages.set(`svg-chart-${i}`, {
|
|
261
|
+
data: screenshot,
|
|
262
|
+
width,
|
|
263
|
+
height,
|
|
277
264
|
});
|
|
278
|
-
|
|
279
|
-
// Create canvas
|
|
280
|
-
const scale = window.devicePixelRatio || 1;
|
|
281
|
-
const canvas = document.getElementById("canvas");
|
|
282
|
-
canvas.width = svgWidth * scale;
|
|
283
|
-
canvas.height = svgHeight * scale;
|
|
284
|
-
|
|
285
|
-
const ctx = canvas.getContext("2d");
|
|
286
|
-
if (!ctx) return null;
|
|
287
|
-
|
|
288
|
-
// Fill with background color
|
|
289
|
-
ctx.fillStyle = backgroundColor ? "#" + backgroundColor : "white";
|
|
290
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
291
|
-
|
|
292
|
-
// Scale for device pixel ratio
|
|
293
|
-
ctx.scale(scale, scale);
|
|
294
|
-
|
|
295
|
-
// Draw the SVG
|
|
296
|
-
ctx.drawImage(img, 0, 0, svgWidth, svgHeight);
|
|
297
|
-
|
|
298
|
-
// Convert to PNG data URL
|
|
299
|
-
const dataUrl = canvas.toDataURL("image/png");
|
|
300
|
-
|
|
301
|
-
// Scale dimensions to fit within DOCX content width
|
|
302
|
-
const maxWidth = 624;
|
|
303
|
-
const aspectRatio = svgHeight / svgWidth;
|
|
304
|
-
const finalWidth = Math.min(svgWidth, maxWidth);
|
|
305
|
-
const finalHeight = finalWidth * aspectRatio;
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
dataUrl,
|
|
309
|
-
width: Math.round(finalWidth),
|
|
310
|
-
height: Math.round(finalHeight),
|
|
311
|
-
};
|
|
312
|
-
} finally {
|
|
313
|
-
URL.revokeObjectURL(svgUrl);
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
</script>
|
|
317
|
-
</body>
|
|
318
|
-
</html>`;
|
|
319
|
-
await page.setContent(renderPage, { waitUntil: "domcontentloaded" });
|
|
320
|
-
// Call the rendering function in the browser
|
|
321
|
-
const result = await page.evaluate(async ({ svgContent, width, height, backgroundColor }) => {
|
|
322
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
323
|
-
return window.renderSvgToPng(svgContent, width, height, backgroundColor);
|
|
324
|
-
}, { svgContent, width, height, backgroundColor });
|
|
325
|
-
if (!result || !result.dataUrl) {
|
|
326
|
-
return null;
|
|
327
265
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const data = Buffer.from(base64Data, "base64");
|
|
331
|
-
return {
|
|
332
|
-
data,
|
|
333
|
-
width: result.width,
|
|
334
|
-
height: result.height,
|
|
335
|
-
};
|
|
266
|
+
await page.close();
|
|
267
|
+
return chartImages;
|
|
336
268
|
}
|
|
337
269
|
export async function exportDocs(filePath, outDir, options = {}) {
|
|
338
270
|
const { name: outputName, pageless } = options;
|
|
@@ -345,12 +277,26 @@ export async function exportDocs(filePath, outDir, options = {}) {
|
|
|
345
277
|
const html = fs.readFileSync(absolutePath, "utf-8");
|
|
346
278
|
// Get filename for title - use provided name or derive from input file
|
|
347
279
|
const baseName = outputName || path.basename(filePath, ".html");
|
|
348
|
-
//
|
|
280
|
+
// Launch browser for image fetching and SVG rendering
|
|
281
|
+
const launchOptions = { headless: true };
|
|
282
|
+
const proxyServer = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
|
283
|
+
if (proxyServer) {
|
|
284
|
+
launchOptions.proxy = { server: proxyServer };
|
|
285
|
+
}
|
|
286
|
+
const browser = await chromium.launch(launchOptions);
|
|
287
|
+
let imageMap;
|
|
288
|
+
let chartImages;
|
|
289
|
+
try {
|
|
290
|
+
// Fetch external images using browser canvas (accurate dimensions via naturalWidth/naturalHeight)
|
|
291
|
+
imageMap = await fetchExternalImages(browser, absolutePath);
|
|
292
|
+
// Render SVG charts by screenshotting live DOM elements (preserves CSS context)
|
|
293
|
+
chartImages = await renderChartImages(browser, absolutePath);
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
await browser.close();
|
|
297
|
+
}
|
|
298
|
+
// Parse HTML to get elements (pass pre-parsed to avoid double-parsing in createDocxBuffer)
|
|
349
299
|
const elements = parseHtmlContent(html);
|
|
350
|
-
// Fetch external images (for <img> tags with HTTP URLs)
|
|
351
|
-
const imageMap = await fetchExternalImages(elements);
|
|
352
|
-
// Render SVG charts to PNG using Playwright browser canvas
|
|
353
|
-
const chartImages = await renderSvgChartsWithPlaywright(elements);
|
|
354
300
|
// Create DOCX buffer with pre-parsed elements, external images, and rendered charts
|
|
355
301
|
const buffer = await createDocxBuffer(html, {
|
|
356
302
|
title: baseName,
|