docgen-utils 1.0.5

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.
Files changed (88) hide show
  1. package/README.md +118 -0
  2. package/dist/bundle.js +36086 -0
  3. package/dist/bundle.min.js +197 -0
  4. package/dist/cli.js +47432 -0
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +9 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/packages/cli/commands/export-docs.d.ts +5 -0
  10. package/dist/packages/cli/commands/export-docs.d.ts.map +1 -0
  11. package/dist/packages/cli/commands/export-docs.js +24 -0
  12. package/dist/packages/cli/commands/export-docs.js.map +1 -0
  13. package/dist/packages/cli/commands/export-slides.d.ts +5 -0
  14. package/dist/packages/cli/commands/export-slides.d.ts.map +1 -0
  15. package/dist/packages/cli/commands/export-slides.js +86 -0
  16. package/dist/packages/cli/commands/export-slides.js.map +1 -0
  17. package/dist/packages/cli/commands/import-docx.d.ts +5 -0
  18. package/dist/packages/cli/commands/import-docx.d.ts.map +1 -0
  19. package/dist/packages/cli/commands/import-docx.js +27 -0
  20. package/dist/packages/cli/commands/import-docx.js.map +1 -0
  21. package/dist/packages/cli/commands/import-pptx.d.ts +5 -0
  22. package/dist/packages/cli/commands/import-pptx.d.ts.map +1 -0
  23. package/dist/packages/cli/commands/import-pptx.js +44 -0
  24. package/dist/packages/cli/commands/import-pptx.js.map +1 -0
  25. package/dist/packages/cli/index.d.ts +11 -0
  26. package/dist/packages/cli/index.d.ts.map +1 -0
  27. package/dist/packages/cli/index.js +103 -0
  28. package/dist/packages/cli/index.js.map +1 -0
  29. package/dist/packages/docs/common.d.ts +183 -0
  30. package/dist/packages/docs/common.d.ts.map +1 -0
  31. package/dist/packages/docs/common.js +27 -0
  32. package/dist/packages/docs/common.js.map +1 -0
  33. package/dist/packages/docs/convert.d.ts +7 -0
  34. package/dist/packages/docs/convert.d.ts.map +1 -0
  35. package/dist/packages/docs/convert.js +1399 -0
  36. package/dist/packages/docs/convert.js.map +1 -0
  37. package/dist/packages/docs/create-document.d.ts +30 -0
  38. package/dist/packages/docs/create-document.d.ts.map +1 -0
  39. package/dist/packages/docs/create-document.js +170 -0
  40. package/dist/packages/docs/create-document.js.map +1 -0
  41. package/dist/packages/docs/export.d.ts +57 -0
  42. package/dist/packages/docs/export.d.ts.map +1 -0
  43. package/dist/packages/docs/export.js +430 -0
  44. package/dist/packages/docs/export.js.map +1 -0
  45. package/dist/packages/docs/import-docx.d.ts +13 -0
  46. package/dist/packages/docs/import-docx.d.ts.map +1 -0
  47. package/dist/packages/docs/import-docx.js +2299 -0
  48. package/dist/packages/docs/import-docx.js.map +1 -0
  49. package/dist/packages/docs/parse.d.ts +6 -0
  50. package/dist/packages/docs/parse.d.ts.map +1 -0
  51. package/dist/packages/docs/parse.js +4253 -0
  52. package/dist/packages/docs/parse.js.map +1 -0
  53. package/dist/packages/shared/dom-parser-shim.d.ts +30 -0
  54. package/dist/packages/shared/dom-parser-shim.d.ts.map +1 -0
  55. package/dist/packages/shared/dom-parser-shim.js +152 -0
  56. package/dist/packages/shared/dom-parser-shim.js.map +1 -0
  57. package/dist/packages/slides/common.d.ts +325 -0
  58. package/dist/packages/slides/common.d.ts.map +1 -0
  59. package/dist/packages/slides/common.js +12 -0
  60. package/dist/packages/slides/common.js.map +1 -0
  61. package/dist/packages/slides/convert.d.ts +35 -0
  62. package/dist/packages/slides/convert.d.ts.map +1 -0
  63. package/dist/packages/slides/convert.js +308 -0
  64. package/dist/packages/slides/convert.js.map +1 -0
  65. package/dist/packages/slides/createPresentation.d.ts +51 -0
  66. package/dist/packages/slides/createPresentation.d.ts.map +1 -0
  67. package/dist/packages/slides/createPresentation.js +265 -0
  68. package/dist/packages/slides/createPresentation.js.map +1 -0
  69. package/dist/packages/slides/export.d.ts +24 -0
  70. package/dist/packages/slides/export.d.ts.map +1 -0
  71. package/dist/packages/slides/export.js +52 -0
  72. package/dist/packages/slides/export.js.map +1 -0
  73. package/dist/packages/slides/import-pptx.d.ts +13 -0
  74. package/dist/packages/slides/import-pptx.d.ts.map +1 -0
  75. package/dist/packages/slides/import-pptx.js +619 -0
  76. package/dist/packages/slides/import-pptx.js.map +1 -0
  77. package/dist/packages/slides/parse.d.ts +45 -0
  78. package/dist/packages/slides/parse.d.ts.map +1 -0
  79. package/dist/packages/slides/parse.js +1185 -0
  80. package/dist/packages/slides/parse.js.map +1 -0
  81. package/dist/packages/slides/transform.d.ts +37 -0
  82. package/dist/packages/slides/transform.d.ts.map +1 -0
  83. package/dist/packages/slides/transform.js +140 -0
  84. package/dist/packages/slides/transform.js.map +1 -0
  85. package/dist/packages/slides/vendor/VENDORING.md +58 -0
  86. package/dist/packages/slides/vendor/pptxgen.d.ts +805 -0
  87. package/dist/packages/slides/vendor/pptxgen.js +7442 -0
  88. package/package.json +57 -0
@@ -0,0 +1,1185 @@
1
+ /**
2
+ * parse.ts - Extract slide data from a live browser DOM.
3
+ *
4
+ * This is a faithful TypeScript port of `extractSlideData()` and all its
5
+ * helper functions from dist/html2pptx.js. It also ports `parseCssGradient()`
6
+ * which is used both here and by other modules.
7
+ *
8
+ * The main export is `parseSlideHtml(doc)` which walks the DOM tree and returns
9
+ * a `ParsedSlide` intermediate representation consumed by the rendering layer.
10
+ */
11
+ // ---------------------------------------------------------------------------
12
+ // Constants
13
+ // ---------------------------------------------------------------------------
14
+ const PT_PER_PX = 0.75;
15
+ const PX_PER_IN = 96;
16
+ // ---------------------------------------------------------------------------
17
+ // Single-weight fonts (bold is skipped for these)
18
+ // ---------------------------------------------------------------------------
19
+ const SINGLE_WEIGHT_FONTS = ['impact'];
20
+ // ---------------------------------------------------------------------------
21
+ // Exported helper functions
22
+ // ---------------------------------------------------------------------------
23
+ /** Convert pixel value to inches. */
24
+ export function pxToInch(px) {
25
+ return px / PX_PER_IN;
26
+ }
27
+ /** Convert a CSS pixel string (e.g. "16px") to points. */
28
+ export function pxToPoints(pxStr) {
29
+ return parseFloat(pxStr) * PT_PER_PX;
30
+ }
31
+ /**
32
+ * Convert an `rgb()` / `rgba()` color string to a 6-char hex string.
33
+ * Returns `'FFFFFF'` for transparent or unparseable values.
34
+ */
35
+ export function rgbToHex(rgbStr) {
36
+ if (rgbStr === 'rgba(0, 0, 0, 0)' || rgbStr === 'transparent')
37
+ return 'FFFFFF';
38
+ const match = rgbStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
39
+ if (!match)
40
+ return 'FFFFFF';
41
+ return match
42
+ .slice(1)
43
+ .map((n) => parseInt(n).toString(16).padStart(2, '0'))
44
+ .join('');
45
+ }
46
+ /**
47
+ * Extract transparency percentage from an `rgba()` string.
48
+ * Returns `null` for opaque or non-rgba values, otherwise 0-100.
49
+ */
50
+ export function extractAlpha(rgbStr) {
51
+ const match = rgbStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
52
+ if (!match || !match[4])
53
+ return null;
54
+ const alpha = parseFloat(match[4]);
55
+ return Math.round((1 - alpha) * 100);
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // parseCssGradient (exported, used by other modules)
59
+ // ---------------------------------------------------------------------------
60
+ /**
61
+ * Parse a CSS gradient string (`linear-gradient(...)` or `radial-gradient(...)`)
62
+ * into a PptxGenJS-compatible `GradientFillProps` object.
63
+ *
64
+ * Returns `null` if the string does not contain a recognized gradient.
65
+ */
66
+ export function parseCssGradient(gradientStr) {
67
+ const colorToHex = (colorStr) => {
68
+ colorStr = colorStr.trim();
69
+ if (colorStr.startsWith('#')) {
70
+ let hex = colorStr.slice(1);
71
+ if (hex.length === 3)
72
+ hex = hex.split('').map((c) => c + c).join('');
73
+ return hex.toUpperCase();
74
+ }
75
+ const rgbMatch = colorStr.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
76
+ if (rgbMatch) {
77
+ return rgbMatch
78
+ .slice(1)
79
+ .map((n) => parseInt(n).toString(16).padStart(2, '0'))
80
+ .join('')
81
+ .toUpperCase();
82
+ }
83
+ return 'FFFFFF';
84
+ };
85
+ const extractTransparency = (colorStr) => {
86
+ colorStr = colorStr.trim();
87
+ const rgbaMatch = colorStr.match(/rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*([\d.]+)\s*\)/);
88
+ if (rgbaMatch) {
89
+ const alpha = parseFloat(rgbaMatch[1]);
90
+ if (alpha < 1) {
91
+ return Math.round((1 - alpha) * 100);
92
+ }
93
+ }
94
+ return null;
95
+ };
96
+ const splitGradientParts = (str) => {
97
+ const parts = [];
98
+ let current = '';
99
+ let depth = 0;
100
+ for (const char of str) {
101
+ if (char === '(')
102
+ depth++;
103
+ else if (char === ')')
104
+ depth--;
105
+ if (char === ',' && depth === 0) {
106
+ parts.push(current.trim());
107
+ current = '';
108
+ }
109
+ else {
110
+ current += char;
111
+ }
112
+ }
113
+ if (current.trim())
114
+ parts.push(current.trim());
115
+ return parts;
116
+ };
117
+ const extractGradientContent = (str, prefix) => {
118
+ const startIdx = str.indexOf(prefix);
119
+ if (startIdx === -1)
120
+ return null;
121
+ let depth = 0;
122
+ const start = startIdx + prefix.length;
123
+ for (let i = start; i < str.length; i++) {
124
+ if (str[i] === '(')
125
+ depth++;
126
+ else if (str[i] === ')') {
127
+ if (depth === 0)
128
+ return str.substring(start, i);
129
+ depth--;
130
+ }
131
+ }
132
+ return null;
133
+ };
134
+ // Try linear-gradient
135
+ const linearContent = extractGradientContent(gradientStr, 'linear-gradient(');
136
+ if (linearContent) {
137
+ const parts = splitGradientParts(linearContent);
138
+ let cssAngle = 180;
139
+ let colorStops = parts;
140
+ const angleMatch = parts[0].match(/^([\d.]+)deg$/);
141
+ if (angleMatch) {
142
+ cssAngle = parseFloat(angleMatch[1]);
143
+ colorStops = parts.slice(1);
144
+ }
145
+ else if (parts[0].startsWith('to ')) {
146
+ const dir = parts[0].replace('to ', '');
147
+ const dirMap = {
148
+ 'top': 0,
149
+ 'right': 90,
150
+ 'bottom': 180,
151
+ 'left': 270,
152
+ 'top right': 45,
153
+ 'right top': 45,
154
+ 'bottom right': 135,
155
+ 'right bottom': 135,
156
+ 'bottom left': 225,
157
+ 'left bottom': 225,
158
+ 'top left': 315,
159
+ 'left top': 315,
160
+ };
161
+ cssAngle = dirMap[dir] ?? 180;
162
+ colorStops = parts.slice(1);
163
+ }
164
+ const stops = colorStops.map((stop, idx) => {
165
+ const posMatch = stop.match(/([\d.]+)%\s*$/);
166
+ const position = posMatch
167
+ ? parseFloat(posMatch[1])
168
+ : (idx / (colorStops.length - 1)) * 100;
169
+ const colorPart = posMatch
170
+ ? stop.replace(/([\d.]+)%\s*$/, '').trim()
171
+ : stop.trim();
172
+ const stopData = { color: colorToHex(colorPart), position };
173
+ const transparency = extractTransparency(colorPart);
174
+ if (transparency !== null)
175
+ stopData.transparency = transparency;
176
+ return stopData;
177
+ });
178
+ return { type: 'linear', angle: cssAngle, stops };
179
+ }
180
+ // Try radial-gradient
181
+ const radialContent = extractGradientContent(gradientStr, 'radial-gradient(');
182
+ if (radialContent) {
183
+ const parts = splitGradientParts(radialContent);
184
+ let centerX = 50;
185
+ let centerY = 50;
186
+ let colorStops = parts;
187
+ const posMatch = parts[0].match(/at\s+([\d.]+)%?\s+([\d.]+)%?/);
188
+ if (posMatch) {
189
+ centerX = parseFloat(posMatch[1]);
190
+ centerY = parseFloat(posMatch[2]);
191
+ colorStops = parts.slice(1);
192
+ }
193
+ else if (parts[0].includes('circle') || parts[0].includes('ellipse')) {
194
+ colorStops = parts.slice(1);
195
+ }
196
+ const stops = colorStops.map((stop, idx) => {
197
+ const posMatch2 = stop.match(/([\d.]+)%\s*$/);
198
+ const position = posMatch2
199
+ ? parseFloat(posMatch2[1])
200
+ : (idx / (colorStops.length - 1)) * 100;
201
+ const colorPart = posMatch2
202
+ ? stop.replace(/([\d.]+)%\s*$/, '').trim()
203
+ : stop.trim();
204
+ const stopData = { color: colorToHex(colorPart), position };
205
+ const transparency = extractTransparency(colorPart);
206
+ if (transparency !== null)
207
+ stopData.transparency = transparency;
208
+ return stopData;
209
+ });
210
+ return { type: 'radial', centerX, centerY, stops };
211
+ }
212
+ return null;
213
+ }
214
+ // ---------------------------------------------------------------------------
215
+ // Internal helpers (not exported)
216
+ // ---------------------------------------------------------------------------
217
+ function shouldSkipBold(fontFamily) {
218
+ if (!fontFamily)
219
+ return false;
220
+ const normalizedFont = fontFamily.toLowerCase().replace(/['"]/g, '').split(',')[0].trim();
221
+ return SINGLE_WEIGHT_FONTS.includes(normalizedFont);
222
+ }
223
+ function applyTextTransform(text, textTransform) {
224
+ if (textTransform === 'uppercase')
225
+ return text.toUpperCase();
226
+ if (textTransform === 'lowercase')
227
+ return text.toLowerCase();
228
+ if (textTransform === 'capitalize') {
229
+ return text.replace(/\b\w/g, (c) => c.toUpperCase());
230
+ }
231
+ return text;
232
+ }
233
+ function getRotation(transform, writingMode) {
234
+ let angle = 0;
235
+ if (writingMode === 'vertical-rl') {
236
+ angle = 90;
237
+ }
238
+ else if (writingMode === 'vertical-lr') {
239
+ angle = 270;
240
+ }
241
+ if (transform && transform !== 'none') {
242
+ const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
243
+ if (rotateMatch) {
244
+ angle += parseFloat(rotateMatch[1]);
245
+ }
246
+ else {
247
+ const matrixMatch = transform.match(/matrix\(([^)]+)\)/);
248
+ if (matrixMatch) {
249
+ const values = matrixMatch[1].split(',').map(parseFloat);
250
+ const matrixAngle = Math.atan2(values[1], values[0]) * (180 / Math.PI);
251
+ angle += Math.round(matrixAngle);
252
+ }
253
+ }
254
+ }
255
+ angle = angle % 360;
256
+ if (angle < 0)
257
+ angle += 360;
258
+ return angle === 0 ? null : angle;
259
+ }
260
+ function getPositionAndSize(el, rect, rotation) {
261
+ if (rotation === null) {
262
+ return { x: rect.left, y: rect.top, w: rect.width, h: rect.height };
263
+ }
264
+ const isVertical = rotation === 90 || rotation === 270;
265
+ if (isVertical) {
266
+ const centerX = rect.left + rect.width / 2;
267
+ const centerY = rect.top + rect.height / 2;
268
+ return {
269
+ x: centerX - rect.height / 2,
270
+ y: centerY - rect.width / 2,
271
+ w: rect.height,
272
+ h: rect.width,
273
+ };
274
+ }
275
+ const centerX = rect.left + rect.width / 2;
276
+ const centerY = rect.top + rect.height / 2;
277
+ return {
278
+ x: centerX - el.offsetWidth / 2,
279
+ y: centerY - el.offsetHeight / 2,
280
+ w: el.offsetWidth,
281
+ h: el.offsetHeight,
282
+ };
283
+ }
284
+ function parseBoxShadow(boxShadow) {
285
+ if (!boxShadow || boxShadow === 'none')
286
+ return null;
287
+ const insetMatch = boxShadow.match(/inset/);
288
+ if (insetMatch)
289
+ return null;
290
+ const colorMatch = boxShadow.match(/rgba?\([^)]+\)/);
291
+ const parts = boxShadow.match(/([-\d.]+)(px|pt)/g);
292
+ if (!parts || parts.length < 2)
293
+ return null;
294
+ const offsetX = parseFloat(parts[0]);
295
+ const offsetY = parseFloat(parts[1]);
296
+ const blur = parts.length > 2 ? parseFloat(parts[2]) : 0;
297
+ let angle = 0;
298
+ if (offsetX !== 0 || offsetY !== 0) {
299
+ angle = Math.atan2(offsetY, offsetX) * (180 / Math.PI);
300
+ if (angle < 0)
301
+ angle += 360;
302
+ }
303
+ const offset = Math.sqrt(offsetX * offsetX + offsetY * offsetY) * PT_PER_PX;
304
+ let opacity = 0.5;
305
+ if (colorMatch) {
306
+ const opacityMatch = colorMatch[0].match(/[\d.]+\)$/);
307
+ if (opacityMatch) {
308
+ opacity = parseFloat(opacityMatch[0].replace(')', ''));
309
+ }
310
+ }
311
+ return {
312
+ type: 'outer',
313
+ angle: Math.round(angle),
314
+ blur: blur * 0.75,
315
+ color: colorMatch ? rgbToHex(colorMatch[0]) : '000000',
316
+ offset: offset,
317
+ opacity,
318
+ };
319
+ }
320
+ function parseInlineFormatting(element, baseOptions, runs, baseTextTransform, win) {
321
+ let prevNodeIsText = false;
322
+ let pendingSoftBreak = false;
323
+ element.childNodes.forEach((node) => {
324
+ let textTransform = baseTextTransform;
325
+ if (node.tagName === 'BR') {
326
+ pendingSoftBreak = true;
327
+ prevNodeIsText = false;
328
+ }
329
+ else if (node.nodeType === Node.TEXT_NODE) {
330
+ const text = textTransform(node.textContent.replace(/\s+/g, ' '));
331
+ const prevRun = runs[runs.length - 1];
332
+ if (prevNodeIsText && prevRun && !pendingSoftBreak) {
333
+ prevRun.text += text;
334
+ }
335
+ else {
336
+ const runOptions = { ...baseOptions };
337
+ if (pendingSoftBreak) {
338
+ runOptions.softBreakBefore = true;
339
+ pendingSoftBreak = false;
340
+ }
341
+ runs.push({ text, options: runOptions });
342
+ }
343
+ prevNodeIsText = true;
344
+ }
345
+ else if (node.nodeType === Node.ELEMENT_NODE && node.textContent.trim()) {
346
+ const el = node;
347
+ const options = { ...baseOptions };
348
+ const computed = win.getComputedStyle(el);
349
+ if (el.tagName === 'SPAN' ||
350
+ el.tagName === 'B' ||
351
+ el.tagName === 'STRONG' ||
352
+ el.tagName === 'I' ||
353
+ el.tagName === 'EM' ||
354
+ el.tagName === 'U') {
355
+ const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600;
356
+ if (isBold && !shouldSkipBold(computed.fontFamily))
357
+ options.bold = true;
358
+ if (computed.fontStyle === 'italic')
359
+ options.italic = true;
360
+ if (computed.textDecoration && computed.textDecoration.includes('underline'))
361
+ options.underline = true;
362
+ if (computed.color && computed.color !== 'rgb(0, 0, 0)') {
363
+ options.color = rgbToHex(computed.color);
364
+ const transparency = extractAlpha(computed.color);
365
+ if (transparency !== null)
366
+ options.transparency = transparency;
367
+ }
368
+ if (computed.fontSize)
369
+ options.fontSize = pxToPoints(computed.fontSize);
370
+ if (computed.textTransform && computed.textTransform !== 'none') {
371
+ const transformStr = computed.textTransform;
372
+ textTransform = (text) => applyTextTransform(text, transformStr);
373
+ }
374
+ parseInlineFormatting(el, options, runs, textTransform, win);
375
+ }
376
+ prevNodeIsText = false;
377
+ }
378
+ });
379
+ if (runs.length > 0) {
380
+ runs[0].text = runs[0].text.replace(/^\s+/, '');
381
+ runs[runs.length - 1].text = runs[runs.length - 1].text.replace(/\s+$/, '');
382
+ }
383
+ return runs.filter((r) => r.text.length > 0);
384
+ }
385
+ // ---------------------------------------------------------------------------
386
+ // Main export
387
+ // ---------------------------------------------------------------------------
388
+ /**
389
+ * Parse a live browser DOM `Document` into a `ParsedSlide` intermediate
390
+ * representation.
391
+ *
392
+ * This is the TypeScript equivalent of `extractSlideData()` from html2pptx.js.
393
+ * It walks all elements in the document, extracts position, style, text, images,
394
+ * shapes, and lists, and returns a typed `ParsedSlide`.
395
+ *
396
+ * @param doc - A live browser `Document` object (typically from an iframe).
397
+ */
398
+ export function parseSlideHtml(doc) {
399
+ const win = doc.defaultView || window;
400
+ const errors = [];
401
+ // -------------------------------------------------------------------------
402
+ // Extract background
403
+ // -------------------------------------------------------------------------
404
+ const body = doc.body;
405
+ const bodyStyle = win.getComputedStyle(body);
406
+ const bgImage = bodyStyle.backgroundImage;
407
+ const bgColor = bodyStyle.backgroundColor;
408
+ let background;
409
+ if (bgImage && bgImage !== 'none') {
410
+ if (bgImage.includes('linear-gradient') || bgImage.includes('radial-gradient')) {
411
+ const gradient = parseCssGradient(bgImage);
412
+ if (gradient) {
413
+ background = { type: 'gradient', gradient };
414
+ }
415
+ else {
416
+ background = { type: 'color', value: rgbToHex(bgColor) };
417
+ }
418
+ }
419
+ else {
420
+ const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
421
+ if (urlMatch) {
422
+ background = { type: 'image', path: urlMatch[1] };
423
+ }
424
+ else {
425
+ background = { type: 'color', value: rgbToHex(bgColor) };
426
+ }
427
+ }
428
+ }
429
+ else {
430
+ background = { type: 'color', value: rgbToHex(bgColor) };
431
+ }
432
+ // -------------------------------------------------------------------------
433
+ // Process all elements
434
+ // -------------------------------------------------------------------------
435
+ const elements = [];
436
+ const placeholders = [];
437
+ const textTags = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'SPAN'];
438
+ const processed = new Set();
439
+ doc.querySelectorAll('*').forEach((el) => {
440
+ if (processed.has(el))
441
+ return;
442
+ const htmlEl = el;
443
+ // Validate text elements
444
+ if (textTags.includes(el.tagName) && el.tagName !== 'SPAN') {
445
+ const computed = win.getComputedStyle(el);
446
+ const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)';
447
+ const hasBorder = (computed.borderWidth && parseFloat(computed.borderWidth) > 0) ||
448
+ (computed.borderTopWidth && parseFloat(computed.borderTopWidth) > 0) ||
449
+ (computed.borderRightWidth && parseFloat(computed.borderRightWidth) > 0) ||
450
+ (computed.borderBottomWidth && parseFloat(computed.borderBottomWidth) > 0) ||
451
+ (computed.borderLeftWidth && parseFloat(computed.borderLeftWidth) > 0);
452
+ const hasShadow = computed.boxShadow && computed.boxShadow !== 'none';
453
+ if (hasBg || hasBorder || hasShadow) {
454
+ errors.push(`Text element <${el.tagName.toLowerCase()}> has ${hasBg ? 'background' : hasBorder ? 'border' : 'shadow'}. ` +
455
+ 'Backgrounds, borders, and shadows are only supported on <div> elements, not text elements.');
456
+ return;
457
+ }
458
+ }
459
+ // Handle SPAN elements with backgrounds/borders as shapes
460
+ if (el.tagName === 'SPAN') {
461
+ const computed = win.getComputedStyle(el);
462
+ const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)';
463
+ const hasBorder = (computed.borderWidth && parseFloat(computed.borderWidth) > 0) ||
464
+ (computed.borderTopWidth && parseFloat(computed.borderTopWidth) > 0) ||
465
+ (computed.borderRightWidth && parseFloat(computed.borderRightWidth) > 0) ||
466
+ (computed.borderBottomWidth && parseFloat(computed.borderBottomWidth) > 0) ||
467
+ (computed.borderLeftWidth && parseFloat(computed.borderLeftWidth) > 0);
468
+ if (hasBg || hasBorder) {
469
+ const rect = htmlEl.getBoundingClientRect();
470
+ if (rect.width > 0 && rect.height > 0) {
471
+ const text = el.textContent.trim();
472
+ const bgGradient = parseCssGradient(computed.backgroundImage);
473
+ const borderRadius = computed.borderRadius;
474
+ const radiusValue = parseFloat(borderRadius);
475
+ let rectRadius = 0;
476
+ if (radiusValue > 0) {
477
+ if (borderRadius.includes('%')) {
478
+ const minDim = Math.min(rect.width, rect.height);
479
+ rectRadius = (radiusValue / 100) * pxToInch(minDim);
480
+ }
481
+ else {
482
+ rectRadius = pxToInch(radiusValue);
483
+ }
484
+ }
485
+ const borderTop = computed.borderTopWidth;
486
+ const borderRight = computed.borderRightWidth;
487
+ const borderBottom = computed.borderBottomWidth;
488
+ const borderLeft = computed.borderLeftWidth;
489
+ const hasUniformBorder = hasBorder &&
490
+ borderTop === borderRight &&
491
+ borderRight === borderBottom &&
492
+ borderBottom === borderLeft;
493
+ const shapeElement = {
494
+ type: 'shape',
495
+ position: {
496
+ x: pxToInch(rect.left),
497
+ y: pxToInch(rect.top),
498
+ w: pxToInch(rect.width),
499
+ h: pxToInch(rect.height),
500
+ },
501
+ text: text,
502
+ textRuns: null,
503
+ style: {
504
+ fontSize: pxToPoints(computed.fontSize),
505
+ fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(),
506
+ color: rgbToHex(computed.color),
507
+ bold: parseInt(computed.fontWeight) >= 600,
508
+ align: 'center',
509
+ valign: 'middle',
510
+ },
511
+ shape: {
512
+ fill: hasBg ? rgbToHex(computed.backgroundColor) : null,
513
+ gradient: bgGradient,
514
+ transparency: null,
515
+ line: hasUniformBorder
516
+ ? {
517
+ color: rgbToHex(computed.borderColor),
518
+ width: pxToPoints(borderTop),
519
+ }
520
+ : null,
521
+ rectRadius: rectRadius,
522
+ shadow: null,
523
+ },
524
+ };
525
+ elements.push(shapeElement);
526
+ processed.add(el);
527
+ return;
528
+ }
529
+ }
530
+ // Handle plain SPANs that are direct children of DIV elements
531
+ const parent = el.parentElement;
532
+ if (parent && parent.tagName === 'DIV') {
533
+ const rect = htmlEl.getBoundingClientRect();
534
+ const text = el.textContent.trim();
535
+ if (rect.width > 0 && rect.height > 0 && text) {
536
+ const computed2 = win.getComputedStyle(el);
537
+ const fontSizePx = parseFloat(computed2.fontSize);
538
+ const lineHeightPx = parseFloat(computed2.lineHeight);
539
+ const lineHeightMultiplier = fontSizePx > 0 && !isNaN(lineHeightPx) ? lineHeightPx / fontSizePx : 1.0;
540
+ const textElement = {
541
+ type: 'p',
542
+ text: [{ text: text, options: {} }],
543
+ position: {
544
+ x: pxToInch(rect.left),
545
+ y: pxToInch(rect.top),
546
+ w: pxToInch(rect.width),
547
+ h: pxToInch(rect.height),
548
+ },
549
+ style: {
550
+ fontSize: pxToPoints(computed2.fontSize),
551
+ fontFace: computed2.fontFamily.split(',')[0].replace(/['"]/g, '').trim(),
552
+ color: rgbToHex(computed2.color),
553
+ bold: parseInt(computed2.fontWeight) >= 600,
554
+ italic: computed2.fontStyle === 'italic',
555
+ align: computed2.textAlign === 'center'
556
+ ? 'center'
557
+ : computed2.textAlign === 'right'
558
+ ? 'right'
559
+ : 'left',
560
+ valign: 'middle',
561
+ lineSpacing: lineHeightMultiplier * pxToPoints(computed2.fontSize),
562
+ },
563
+ };
564
+ elements.push(textElement);
565
+ processed.add(el);
566
+ return;
567
+ }
568
+ }
569
+ }
570
+ // Extract placeholder elements
571
+ if (el.className &&
572
+ typeof el.className === 'string' &&
573
+ el.className.includes('placeholder')) {
574
+ const rect = htmlEl.getBoundingClientRect();
575
+ if (rect.width === 0 || rect.height === 0) {
576
+ errors.push(`Placeholder "${el.id || 'unnamed'}" has ${rect.width === 0 ? 'width: 0' : 'height: 0'}. Check the layout CSS.`);
577
+ }
578
+ else {
579
+ placeholders.push({
580
+ id: el.id || `placeholder-${placeholders.length}`,
581
+ x: pxToInch(rect.left),
582
+ y: pxToInch(rect.top),
583
+ w: pxToInch(rect.width),
584
+ h: pxToInch(rect.height),
585
+ });
586
+ }
587
+ processed.add(el);
588
+ return;
589
+ }
590
+ // Extract images
591
+ if (el.tagName === 'IMG') {
592
+ const rect = htmlEl.getBoundingClientRect();
593
+ if (rect.width > 0 && rect.height > 0) {
594
+ const imgComputed = win.getComputedStyle(el);
595
+ const bodyRect = doc.body.getBoundingClientRect();
596
+ const coversWidth = rect.width >= bodyRect.width * 0.95;
597
+ const coversHeight = rect.height >= bodyRect.height * 0.95;
598
+ const nearOrigin = rect.left <= 10 && rect.top <= 10;
599
+ const isFullSlideImage = coversWidth && coversHeight && nearOrigin;
600
+ const objectFit = imgComputed.objectFit;
601
+ // Check for ancestor with overflow:hidden and border-radius
602
+ let imgRectRadius = null;
603
+ let ancestor = el.parentElement;
604
+ while (ancestor && ancestor !== doc.body) {
605
+ const ancestorComputed = win.getComputedStyle(ancestor);
606
+ const ancestorOverflow = ancestorComputed.overflow;
607
+ const ancestorBorderRadius = ancestorComputed.borderRadius;
608
+ if ((ancestorOverflow === 'hidden' || ancestorOverflow === 'clip') &&
609
+ ancestorBorderRadius) {
610
+ const radiusValue = parseFloat(ancestorBorderRadius);
611
+ if (radiusValue > 0) {
612
+ if (ancestorBorderRadius.includes('%')) {
613
+ const ancestorRect = ancestor.getBoundingClientRect();
614
+ const minDim = Math.min(ancestorRect.width, ancestorRect.height);
615
+ imgRectRadius = (radiusValue / 100) * pxToInch(minDim);
616
+ }
617
+ else if (ancestorBorderRadius.includes('pt')) {
618
+ imgRectRadius = radiusValue / 72;
619
+ }
620
+ else {
621
+ imgRectRadius = pxToInch(radiusValue);
622
+ }
623
+ break;
624
+ }
625
+ }
626
+ ancestor = ancestor.parentElement;
627
+ }
628
+ const imageElement = {
629
+ type: isFullSlideImage ? 'slideBackgroundImage' : 'image',
630
+ src: el.src,
631
+ position: {
632
+ x: pxToInch(rect.left),
633
+ y: pxToInch(rect.top),
634
+ w: pxToInch(rect.width),
635
+ h: pxToInch(rect.height),
636
+ },
637
+ sizing: objectFit === 'cover' ? { type: 'cover' } : null,
638
+ };
639
+ if (imgRectRadius !== null) {
640
+ imageElement.rectRadius = imgRectRadius;
641
+ }
642
+ elements.push(imageElement);
643
+ processed.add(el);
644
+ return;
645
+ }
646
+ }
647
+ // Extract DIVs with backgrounds/borders as shapes
648
+ const isContainer = el.tagName === 'DIV' && !textTags.includes(el.tagName);
649
+ if (isContainer) {
650
+ const computed = win.getComputedStyle(el);
651
+ const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)';
652
+ // Check for background images or gradients
653
+ const elBgImage = computed.backgroundImage;
654
+ let bgImageUrl = null;
655
+ let bgImageSize = null;
656
+ let bgImagePosition = null;
657
+ let bgGradient = null;
658
+ if (elBgImage && elBgImage !== 'none') {
659
+ if (elBgImage.includes('linear-gradient') ||
660
+ elBgImage.includes('radial-gradient')) {
661
+ bgGradient = parseCssGradient(elBgImage);
662
+ }
663
+ else {
664
+ const urlMatch = elBgImage.match(/url\(["']?([^"')]+)["']?\)/);
665
+ if (urlMatch) {
666
+ bgImageUrl = urlMatch[1];
667
+ bgImageSize = computed.backgroundSize || 'auto';
668
+ bgImagePosition = computed.backgroundPosition || '0% 0%';
669
+ }
670
+ }
671
+ }
672
+ // Check for borders
673
+ const borderTop = computed.borderTopWidth;
674
+ const borderRight = computed.borderRightWidth;
675
+ const borderBottom = computed.borderBottomWidth;
676
+ const borderLeft = computed.borderLeftWidth;
677
+ const borders = [borderTop, borderRight, borderBottom, borderLeft].map((b) => parseFloat(b) || 0);
678
+ const hasBorder = borders.some((b) => b > 0);
679
+ const hasUniformBorder = hasBorder && borders.every((b) => b === borders[0]);
680
+ const borderLines = [];
681
+ if (hasBorder && !hasUniformBorder) {
682
+ const rect = htmlEl.getBoundingClientRect();
683
+ const x = pxToInch(rect.left);
684
+ const y = pxToInch(rect.top);
685
+ const w = pxToInch(rect.width);
686
+ const h = pxToInch(rect.height);
687
+ if (parseFloat(borderTop) > 0) {
688
+ const widthPt = pxToPoints(borderTop);
689
+ const inset = widthPt / 72 / 2;
690
+ borderLines.push({
691
+ type: 'line',
692
+ x1: x,
693
+ y1: y + inset,
694
+ x2: x + w,
695
+ y2: y + inset,
696
+ width: widthPt,
697
+ color: rgbToHex(computed.borderTopColor),
698
+ });
699
+ }
700
+ if (parseFloat(borderRight) > 0) {
701
+ const widthPt = pxToPoints(borderRight);
702
+ const inset = widthPt / 72 / 2;
703
+ borderLines.push({
704
+ type: 'line',
705
+ x1: x + w - inset,
706
+ y1: y,
707
+ x2: x + w - inset,
708
+ y2: y + h,
709
+ width: widthPt,
710
+ color: rgbToHex(computed.borderRightColor),
711
+ });
712
+ }
713
+ if (parseFloat(borderBottom) > 0) {
714
+ const widthPt = pxToPoints(borderBottom);
715
+ const inset = widthPt / 72 / 2;
716
+ borderLines.push({
717
+ type: 'line',
718
+ x1: x,
719
+ y1: y + h - inset,
720
+ x2: x + w,
721
+ y2: y + h - inset,
722
+ width: widthPt,
723
+ color: rgbToHex(computed.borderBottomColor),
724
+ });
725
+ }
726
+ if (parseFloat(borderLeft) > 0) {
727
+ const widthPt = pxToPoints(borderLeft);
728
+ const inset = widthPt / 72 / 2;
729
+ borderLines.push({
730
+ type: 'line',
731
+ x1: x + inset,
732
+ y1: y,
733
+ x2: x + inset,
734
+ y2: y + h,
735
+ width: widthPt,
736
+ color: rgbToHex(computed.borderLeftColor),
737
+ });
738
+ }
739
+ }
740
+ if (hasBg || hasBorder || bgImageUrl || bgGradient) {
741
+ const rect = htmlEl.getBoundingClientRect();
742
+ if (rect.width > 0 && rect.height > 0) {
743
+ const shadow = parseBoxShadow(computed.boxShadow);
744
+ // Add background image element first
745
+ if (bgImageUrl) {
746
+ const bgImgElement = {
747
+ type: 'backgroundImage',
748
+ src: bgImageUrl,
749
+ position: {
750
+ x: pxToInch(rect.left),
751
+ y: pxToInch(rect.top),
752
+ w: pxToInch(rect.width),
753
+ h: pxToInch(rect.height),
754
+ },
755
+ sizing: {
756
+ type: bgImageSize === 'cover'
757
+ ? 'cover'
758
+ : bgImageSize === 'contain'
759
+ ? 'contain'
760
+ : 'cover',
761
+ position: bgImagePosition,
762
+ },
763
+ };
764
+ elements.push(bgImgElement);
765
+ }
766
+ // Check for text children
767
+ const textChildren = Array.from(el.children).filter((child) => ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SPAN'].includes(child.tagName));
768
+ const nonTextChildren = Array.from(el.children).filter((child) => !['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SPAN'].includes(child.tagName));
769
+ const isSingleTextChild = textChildren.length === 1 &&
770
+ textChildren[0].children.length === 0 &&
771
+ nonTextChildren.length === 0;
772
+ // Detect flexbox alignment
773
+ const display = computed.display;
774
+ const isFlexContainer = display === 'flex' || display === 'inline-flex';
775
+ const alignItems = computed.alignItems;
776
+ const justifyContent = computed.justifyContent;
777
+ let valign = 'top';
778
+ let align = 'left';
779
+ if (isFlexContainer) {
780
+ const flexDirection = computed.flexDirection || 'row';
781
+ if (flexDirection === 'row' || flexDirection === 'row-reverse') {
782
+ if (alignItems === 'center')
783
+ valign = 'middle';
784
+ else if (alignItems === 'flex-end' || alignItems === 'end')
785
+ valign = 'bottom';
786
+ else
787
+ valign = 'top';
788
+ if (justifyContent === 'center')
789
+ align = 'center';
790
+ else if (justifyContent === 'flex-end' || justifyContent === 'end')
791
+ align = 'right';
792
+ else
793
+ align = 'left';
794
+ }
795
+ else {
796
+ if (alignItems === 'center')
797
+ align = 'center';
798
+ else if (alignItems === 'flex-end' || alignItems === 'end')
799
+ align = 'right';
800
+ else
801
+ align = 'left';
802
+ if (justifyContent === 'center')
803
+ valign = 'middle';
804
+ else if (justifyContent === 'flex-end' || justifyContent === 'end')
805
+ valign = 'bottom';
806
+ else
807
+ valign = 'top';
808
+ }
809
+ }
810
+ else {
811
+ const textAlign = computed.textAlign;
812
+ if (textAlign === 'center') {
813
+ align = 'center';
814
+ }
815
+ else if (textAlign === 'right' || textAlign === 'end') {
816
+ align = 'right';
817
+ }
818
+ else {
819
+ const paddingLeft = parseFloat(computed.paddingLeft) || 0;
820
+ const paddingRight = parseFloat(computed.paddingRight) || 0;
821
+ const paddingDiff = Math.abs(paddingLeft - paddingRight);
822
+ if (paddingLeft > 0 && paddingDiff < 2) {
823
+ align = 'center';
824
+ }
825
+ else {
826
+ align = 'left';
827
+ }
828
+ }
829
+ }
830
+ let shapeText = '';
831
+ let shapeTextRuns = null;
832
+ let shapeStyle = null;
833
+ const hasTextChildren = textChildren.length > 0 && nonTextChildren.length === 0;
834
+ if (hasTextChildren) {
835
+ if (isSingleTextChild) {
836
+ const textEl = textChildren[0];
837
+ const textComputed = win.getComputedStyle(textEl);
838
+ shapeText = textEl.textContent.trim();
839
+ let fontFill = null;
840
+ const textBgClip = textComputed.webkitBackgroundClip ||
841
+ textComputed.backgroundClip;
842
+ const textFillColor = textComputed.webkitTextFillColor;
843
+ const textIsTextClip = textBgClip === 'text';
844
+ const textFillTransparent = textFillColor === 'transparent' ||
845
+ textFillColor === 'rgba(0, 0, 0, 0)' ||
846
+ (textFillColor &&
847
+ textFillColor.includes('rgba') &&
848
+ textFillColor.endsWith(', 0)'));
849
+ const parentBgClip = computed.webkitBackgroundClip || computed.backgroundClip;
850
+ const parentFillColor = computed.webkitTextFillColor;
851
+ const parentIsTextClip = parentBgClip === 'text';
852
+ const parentFillTransparent = parentFillColor === 'transparent' ||
853
+ parentFillColor === 'rgba(0, 0, 0, 0)' ||
854
+ (parentFillColor &&
855
+ parentFillColor.includes('rgba') &&
856
+ parentFillColor.endsWith(', 0)'));
857
+ if ((textIsTextClip && textFillTransparent) ||
858
+ (parentIsTextClip && parentFillTransparent)) {
859
+ const gradientSource = textIsTextClip && textFillTransparent
860
+ ? textComputed.backgroundImage
861
+ : computed.backgroundImage;
862
+ if (gradientSource &&
863
+ gradientSource !== 'none' &&
864
+ (gradientSource.includes('linear-gradient') ||
865
+ gradientSource.includes('radial-gradient'))) {
866
+ fontFill = {
867
+ type: 'gradient',
868
+ gradient: parseCssGradient(gradientSource) || undefined,
869
+ };
870
+ bgGradient = null;
871
+ }
872
+ }
873
+ const effectiveColor = textComputed.color !== 'rgb(0, 0, 0)' ? textComputed.color : computed.color;
874
+ const isBold = textComputed.fontWeight === 'bold' ||
875
+ parseInt(textComputed.fontWeight) >= 600 ||
876
+ computed.fontWeight === 'bold' ||
877
+ parseInt(computed.fontWeight) >= 600;
878
+ let effectiveAlign = align;
879
+ const childTextAlign = textComputed.textAlign;
880
+ if (childTextAlign === 'center' ||
881
+ childTextAlign === 'right' ||
882
+ childTextAlign === 'end') {
883
+ effectiveAlign =
884
+ childTextAlign === 'end' ? 'right' : childTextAlign;
885
+ }
886
+ const whiteSpace = computed.whiteSpace || textComputed.whiteSpace;
887
+ const shouldNotWrap = whiteSpace === 'nowrap' ||
888
+ whiteSpace === 'pre' ||
889
+ rect.width < 100 ||
890
+ rect.height < 50;
891
+ shapeStyle = {
892
+ fontSize: pxToPoints(textComputed.fontSize || computed.fontSize),
893
+ fontFace: (textComputed.fontFamily || computed.fontFamily)
894
+ .split(',')[0]
895
+ .replace(/['"]/g, '')
896
+ .trim(),
897
+ color: fontFill ? null : rgbToHex(effectiveColor),
898
+ fontFill: fontFill,
899
+ bold: isBold,
900
+ align: effectiveAlign,
901
+ valign: valign,
902
+ inset: 0,
903
+ wrap: !shouldNotWrap,
904
+ };
905
+ processed.add(textEl);
906
+ }
907
+ else {
908
+ shapeTextRuns = [];
909
+ textChildren.forEach((textChild, idx) => {
910
+ const textEl = textChild;
911
+ const textComputed = win.getComputedStyle(textEl);
912
+ const text = textEl.textContent.trim();
913
+ if (!text)
914
+ return;
915
+ const isBold = textComputed.fontWeight === 'bold' ||
916
+ parseInt(textComputed.fontWeight) >= 600;
917
+ const isItalic = textComputed.fontStyle === 'italic';
918
+ const isUnderline = textComputed.textDecoration &&
919
+ textComputed.textDecoration.includes('underline');
920
+ const runText = idx > 0 && shapeTextRuns.length > 0 ? ' ' + text : text;
921
+ shapeTextRuns.push({
922
+ text: runText,
923
+ options: {
924
+ fontSize: pxToPoints(textComputed.fontSize),
925
+ fontFace: textComputed.fontFamily
926
+ .split(',')[0]
927
+ .replace(/['"]/g, '')
928
+ .trim(),
929
+ color: rgbToHex(textComputed.color),
930
+ bold: isBold,
931
+ italic: isItalic,
932
+ underline: isUnderline || false,
933
+ },
934
+ });
935
+ processed.add(textEl);
936
+ });
937
+ shapeStyle = {
938
+ align: align,
939
+ valign: valign,
940
+ inset: 0,
941
+ };
942
+ }
943
+ }
944
+ if (hasBg || hasUniformBorder || bgGradient) {
945
+ const shapeElement = {
946
+ type: 'shape',
947
+ text: shapeText,
948
+ textRuns: shapeTextRuns,
949
+ style: shapeStyle,
950
+ position: {
951
+ x: pxToInch(rect.left),
952
+ y: pxToInch(rect.top),
953
+ w: pxToInch(rect.width),
954
+ h: pxToInch(rect.height),
955
+ },
956
+ shape: {
957
+ fill: hasBg ? rgbToHex(computed.backgroundColor) : null,
958
+ gradient: bgGradient,
959
+ transparency: hasBg ? extractAlpha(computed.backgroundColor) : null,
960
+ line: hasUniformBorder
961
+ ? {
962
+ color: rgbToHex(computed.borderColor),
963
+ width: pxToPoints(computed.borderWidth),
964
+ }
965
+ : null,
966
+ rectRadius: (() => {
967
+ const radius = computed.borderRadius;
968
+ const radiusValue = parseFloat(radius);
969
+ if (radiusValue === 0)
970
+ return 0;
971
+ if (radius.includes('%')) {
972
+ if (radiusValue >= 50)
973
+ return 1;
974
+ const minDim = Math.min(rect.width, rect.height);
975
+ return (radiusValue / 100) * pxToInch(minDim);
976
+ }
977
+ if (radius.includes('pt'))
978
+ return radiusValue / 72;
979
+ return radiusValue / PX_PER_IN;
980
+ })(),
981
+ shadow: shadow,
982
+ },
983
+ };
984
+ elements.push(shapeElement);
985
+ }
986
+ else if (shapeStyle && shapeStyle.fontFill) {
987
+ const fontFillTextElement = {
988
+ type: 'p',
989
+ text: shapeText,
990
+ position: {
991
+ x: pxToInch(rect.left),
992
+ y: pxToInch(rect.top),
993
+ w: pxToInch(rect.width),
994
+ h: pxToInch(rect.height),
995
+ },
996
+ style: shapeStyle,
997
+ };
998
+ elements.push(fontFillTextElement);
999
+ }
1000
+ else if (isSingleTextChild) {
1001
+ processed.delete(textChildren[0]);
1002
+ }
1003
+ elements.push(...borderLines);
1004
+ processed.add(el);
1005
+ return;
1006
+ }
1007
+ }
1008
+ }
1009
+ // Extract bullet lists
1010
+ if (el.tagName === 'UL' || el.tagName === 'OL') {
1011
+ const rect = htmlEl.getBoundingClientRect();
1012
+ if (rect.width === 0 || rect.height === 0)
1013
+ return;
1014
+ const liElements = Array.from(el.querySelectorAll('li'));
1015
+ const items = [];
1016
+ const ulComputed = win.getComputedStyle(el);
1017
+ const ulPaddingLeftPt = pxToPoints(ulComputed.paddingLeft);
1018
+ const marginLeft = ulPaddingLeftPt * 0.5;
1019
+ const textIndent = ulPaddingLeftPt * 0.5;
1020
+ liElements.forEach((li, idx) => {
1021
+ const isLast = idx === liElements.length - 1;
1022
+ const runs = parseInlineFormatting(li, { breakLine: false }, [], (x) => x, win);
1023
+ if (runs.length > 0) {
1024
+ runs[0].text = runs[0].text.replace(/^[•\-\*\u25AA\u25B8]\s*/, '');
1025
+ runs[0].options.bullet = { indent: textIndent };
1026
+ }
1027
+ if (runs.length > 0 && !isLast) {
1028
+ runs[runs.length - 1].options.breakLine = true;
1029
+ }
1030
+ items.push(...runs);
1031
+ });
1032
+ const liComputed = win.getComputedStyle(liElements[0] || el);
1033
+ const listFontSizePx = parseFloat(liComputed.fontSize);
1034
+ const listLineHeightPx = parseFloat(liComputed.lineHeight);
1035
+ const listLineHeightMultiplier = listFontSizePx > 0 && !isNaN(listLineHeightPx)
1036
+ ? listLineHeightPx / listFontSizePx
1037
+ : 1.0;
1038
+ const listElement = {
1039
+ type: 'list',
1040
+ items: items,
1041
+ position: {
1042
+ x: pxToInch(rect.left),
1043
+ y: pxToInch(rect.top),
1044
+ w: pxToInch(rect.width),
1045
+ h: pxToInch(rect.height),
1046
+ },
1047
+ style: {
1048
+ fontSize: pxToPoints(liComputed.fontSize),
1049
+ fontFace: liComputed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(),
1050
+ color: rgbToHex(liComputed.color),
1051
+ transparency: extractAlpha(liComputed.color),
1052
+ align: liComputed.textAlign === 'start'
1053
+ ? 'left'
1054
+ : liComputed.textAlign,
1055
+ lineSpacing: pxToPoints(liComputed.fontSize) * listLineHeightMultiplier,
1056
+ paraSpaceBefore: 0,
1057
+ paraSpaceAfter: pxToPoints(liComputed.marginBottom),
1058
+ margin: [marginLeft, 0, 0, 0],
1059
+ },
1060
+ };
1061
+ elements.push(listElement);
1062
+ liElements.forEach((li) => processed.add(li));
1063
+ processed.add(el);
1064
+ return;
1065
+ }
1066
+ // Extract text elements
1067
+ if (!textTags.includes(el.tagName) || el.tagName === 'SPAN')
1068
+ return;
1069
+ const rect = htmlEl.getBoundingClientRect();
1070
+ const text = el.textContent.trim();
1071
+ if (rect.width === 0 || rect.height === 0 || !text)
1072
+ return;
1073
+ if (el.tagName !== 'LI' &&
1074
+ /^[•\-\*\u25AA\u25B8\u25CB\u25CF\u25C6\u25C7\u25A0\u25A1]\s/.test(text.trimStart())) {
1075
+ errors.push(`Text element <${el.tagName.toLowerCase()}> starts with bullet symbol "${text.substring(0, 20)}...". ` +
1076
+ 'Use <ul> or <ol> lists instead of manual bullet symbols.');
1077
+ return;
1078
+ }
1079
+ const computed = win.getComputedStyle(el);
1080
+ const rotation = getRotation(computed.transform, computed.writingMode);
1081
+ const { x, y, w, h } = getPositionAndSize(htmlEl, rect, rotation);
1082
+ const fontSizePx = parseFloat(computed.fontSize);
1083
+ const lineHeightPx = parseFloat(computed.lineHeight);
1084
+ const lineHeightMultiplier = fontSizePx > 0 && !isNaN(lineHeightPx) ? lineHeightPx / fontSizePx : 1.0;
1085
+ let textAlign = computed.textAlign === 'start' ? 'left' : computed.textAlign;
1086
+ let valign = null;
1087
+ const checkFlexParent = (parent) => {
1088
+ if (!parent)
1089
+ return null;
1090
+ const parentComputed = win.getComputedStyle(parent);
1091
+ const parentDisplay = parentComputed.display;
1092
+ if (parentDisplay === 'flex' || parentDisplay === 'inline-flex') {
1093
+ const flexDirection = parentComputed.flexDirection || 'row';
1094
+ const alignItems = parentComputed.alignItems;
1095
+ if ((flexDirection === 'row' || flexDirection === 'row-reverse') &&
1096
+ parent.children.length > 1) {
1097
+ return { alignItems, flexDirection };
1098
+ }
1099
+ }
1100
+ return null;
1101
+ };
1102
+ const parentEl = el.parentElement;
1103
+ let flexInfo = checkFlexParent(parentEl);
1104
+ if (!flexInfo && parentEl) {
1105
+ flexInfo = checkFlexParent(parentEl.parentElement);
1106
+ }
1107
+ if (flexInfo) {
1108
+ if (flexInfo.alignItems === 'center') {
1109
+ valign = 'middle';
1110
+ }
1111
+ }
1112
+ const baseStyle = {
1113
+ fontSize: pxToPoints(computed.fontSize),
1114
+ fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(),
1115
+ color: rgbToHex(computed.color),
1116
+ align: textAlign,
1117
+ valign: valign,
1118
+ lineSpacing: pxToPoints(computed.fontSize) * lineHeightMultiplier,
1119
+ paraSpaceBefore: pxToPoints(computed.marginTop),
1120
+ paraSpaceAfter: pxToPoints(computed.marginBottom),
1121
+ margin: [
1122
+ pxToPoints(computed.paddingLeft),
1123
+ pxToPoints(computed.paddingRight),
1124
+ pxToPoints(computed.paddingBottom),
1125
+ pxToPoints(computed.paddingTop),
1126
+ ],
1127
+ };
1128
+ const transparency = extractAlpha(computed.color);
1129
+ if (transparency !== null)
1130
+ baseStyle.transparency = transparency;
1131
+ if (rotation !== null)
1132
+ baseStyle.rotate = rotation;
1133
+ const bgClip = computed.webkitBackgroundClip || computed.backgroundClip;
1134
+ const textFillColor = computed.webkitTextFillColor;
1135
+ const isTextFillTransparent = textFillColor === 'transparent' ||
1136
+ textFillColor === 'rgba(0, 0, 0, 0)' ||
1137
+ (textFillColor &&
1138
+ textFillColor.includes('rgba') &&
1139
+ textFillColor.endsWith(', 0)'));
1140
+ if (bgClip === 'text' && isTextFillTransparent) {
1141
+ const elBgImg = computed.backgroundImage;
1142
+ if (elBgImg &&
1143
+ elBgImg !== 'none' &&
1144
+ (elBgImg.includes('linear-gradient') || elBgImg.includes('radial-gradient'))) {
1145
+ const textGradient = parseCssGradient(elBgImg);
1146
+ if (textGradient) {
1147
+ baseStyle.fontFill = { type: 'gradient', gradient: textGradient };
1148
+ delete baseStyle.color;
1149
+ }
1150
+ }
1151
+ }
1152
+ const hasFormatting = el.querySelector('b, i, u, strong, em, span, br');
1153
+ if (hasFormatting) {
1154
+ const transformStr = computed.textTransform;
1155
+ const runs = parseInlineFormatting(el, {}, [], (str) => applyTextTransform(str, transformStr), win);
1156
+ const textElement = {
1157
+ type: el.tagName.toLowerCase(),
1158
+ text: runs,
1159
+ position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) },
1160
+ style: baseStyle,
1161
+ };
1162
+ elements.push(textElement);
1163
+ }
1164
+ else {
1165
+ const textTransformVal = computed.textTransform;
1166
+ const transformedText = applyTextTransform(text, textTransformVal);
1167
+ const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600;
1168
+ const textElement = {
1169
+ type: el.tagName.toLowerCase(),
1170
+ text: transformedText,
1171
+ position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) },
1172
+ style: {
1173
+ ...baseStyle,
1174
+ bold: isBold && !shouldSkipBold(computed.fontFamily),
1175
+ italic: computed.fontStyle === 'italic',
1176
+ underline: computed.textDecoration.includes('underline'),
1177
+ },
1178
+ };
1179
+ elements.push(textElement);
1180
+ }
1181
+ processed.add(el);
1182
+ });
1183
+ return { background, elements, placeholders, errors };
1184
+ }
1185
+ //# sourceMappingURL=parse.js.map