@meistrari/minuta-editor 1.0.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.
@@ -0,0 +1,1282 @@
1
+ import { f as footnoteHtmlToRuns, p as pocLoneCellIndex } from '../shared/minuta-editor.b2512170.mjs';
2
+ import { BorderStyle, TextRun, ExternalHyperlink, Paragraph, Document, Header, Packer, AlignmentType, PageBreak, LineRuleType, Table, TableRow, TableCell, WidthType, TableLayoutType, Footer, FootnoteReferenceRun, VerticalAlign, ImageRun, HorizontalPositionRelativeFrom, VerticalPositionRelativeFrom, TextWrappingType, FrameAnchorType, PageNumber, HeadingLevel } from 'docx';
3
+ import '@tiptap/pm/state';
4
+ import '@tiptap/pm/view';
5
+
6
+ const A4 = { width: 11906, height: 16838 };
7
+ const PX_TO_TWIP = 15;
8
+ const PX_PER_PT = 96 / 72;
9
+ function getProp(style, prop) {
10
+ if (!style)
11
+ return void 0;
12
+ const match = style.match(new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]+)`, "i"));
13
+ return match?.[1]?.trim();
14
+ }
15
+ function cssColorToHex(value) {
16
+ if (!value)
17
+ return void 0;
18
+ if (value.startsWith("#"))
19
+ return value.length === 4 ? value.slice(1).split("").map((c) => c + c).join("") : value.slice(1);
20
+ const rgb = value.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
21
+ if (rgb) {
22
+ if (rgb[4] !== void 0 && Number.parseFloat(rgb[4]) === 0)
23
+ return void 0;
24
+ return [rgb[1], rgb[2], rgb[3]].map((n) => Number(n).toString(16).padStart(2, "0")).join("");
25
+ }
26
+ return void 0;
27
+ }
28
+ function cssLengthToTwips(value) {
29
+ if (!value)
30
+ return void 0;
31
+ const num = Number.parseFloat(value);
32
+ if (!Number.isFinite(num))
33
+ return void 0;
34
+ if (value.endsWith("cm"))
35
+ return Math.round(num * 567);
36
+ if (value.endsWith("pt"))
37
+ return Math.round(num * 20);
38
+ if (value.endsWith("rem"))
39
+ return Math.round(num * 16 * PX_TO_TWIP);
40
+ if (value.endsWith("px"))
41
+ return Math.round(num * PX_TO_TWIP);
42
+ return void 0;
43
+ }
44
+ function cssSizeToHalfPoints(value) {
45
+ if (!value)
46
+ return void 0;
47
+ const num = Number.parseFloat(value);
48
+ if (!Number.isFinite(num))
49
+ return void 0;
50
+ if (value.endsWith("pt"))
51
+ return Math.round(num * 2);
52
+ if (value.endsWith("px"))
53
+ return Math.round(num / PX_PER_PT * 2);
54
+ return void 0;
55
+ }
56
+ function parseStyle(style) {
57
+ const info = {};
58
+ const weight = getProp(style, "font-weight");
59
+ if (weight && (weight === "bold" || Number.parseInt(weight) >= 600))
60
+ info.bold = true;
61
+ if (getProp(style, "font-style")?.includes("italic"))
62
+ info.italics = true;
63
+ const deco = getProp(style, "text-decoration");
64
+ if (deco?.includes("underline"))
65
+ info.underline = true;
66
+ if (deco?.includes("line-through"))
67
+ info.strike = true;
68
+ info.color = cssColorToHex(getProp(style, "color"));
69
+ const bg = cssColorToHex(getProp(style, "background-color"));
70
+ if (bg && bg.toLowerCase() !== "ffffff")
71
+ info.highlight = bg;
72
+ info.size = cssSizeToHalfPoints(getProp(style, "font-size"));
73
+ const family = getProp(style, "font-family");
74
+ if (family)
75
+ info.font = family.split(",")[0].trim().replace(/["']/g, "");
76
+ return info;
77
+ }
78
+ function cssAlignToDocx(align) {
79
+ switch (align) {
80
+ case "center":
81
+ return AlignmentType.CENTER;
82
+ case "right":
83
+ return AlignmentType.RIGHT;
84
+ case "justify":
85
+ return AlignmentType.JUSTIFIED;
86
+ case "left":
87
+ return AlignmentType.LEFT;
88
+ default:
89
+ return void 0;
90
+ }
91
+ }
92
+ function parseAlignment(style) {
93
+ return cssAlignToDocx(getProp(style, "text-align"));
94
+ }
95
+ function domAt(ctx, pos) {
96
+ try {
97
+ const dom = ctx.view.nodeDOM(pos);
98
+ return dom instanceof HTMLElement ? dom : null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function measureEl(el) {
104
+ const cs = getComputedStyle(el);
105
+ const fontPx = Number.parseFloat(cs.fontSize) || 16;
106
+ const lineHpx = cs.lineHeight === "normal" ? null : Number.parseFloat(cs.lineHeight);
107
+ return {
108
+ el,
109
+ cs,
110
+ fontPx,
111
+ sizeHalf: Math.round(fontPx / PX_PER_PT * 2),
112
+ // line EXACT MENOR que a fonte CLIPA verticalmente no Word (o preview
113
+ // deixa o texto transbordar o box; o Word não tem overflow) — ex.: os
114
+ // títulos de ementa (h3 com line-height 14px e fonte maior).
115
+ // Piso = 1.0em: protege o caso patológico (0.7em) SEM inflar entrelinha
116
+ // justa (1.0em, comum em citações) — com 1.15em o export ficava mais
117
+ // alto que o preview mediu, o conteúdo derramava no Word e a quebra
118
+ // explícita seguinte abria página quase vazia (TLF ¶36/¶52).
119
+ lineExactTw: lineHpx && Number.isFinite(lineHpx) ? Math.round(Math.max(lineHpx, fontPx) * PX_TO_TWIP) : null,
120
+ afterTw: Math.round((Number.parseFloat(cs.marginBottom) || 0) * PX_TO_TWIP),
121
+ beforeTw: Math.round((Number.parseFloat(cs.marginTop) || 0) * PX_TO_TWIP),
122
+ marginLeftTw: Math.round((Number.parseFloat(cs.marginLeft) || 0) * PX_TO_TWIP),
123
+ color: cssColorToHex(cs.color),
124
+ bold: (Number.parseInt(cs.fontWeight) || 400) >= 600,
125
+ italics: cs.fontStyle.includes("italic"),
126
+ align: cssAlignToDocx(cs.textAlign)
127
+ };
128
+ }
129
+ function measureAt(ctx, pos) {
130
+ const el = domAt(ctx, pos);
131
+ return el ? measureEl(el) : null;
132
+ }
133
+ function cleanUndefined(obj) {
134
+ const out = {};
135
+ for (const key of Object.keys(obj)) {
136
+ if (obj[key] !== void 0)
137
+ out[key] = obj[key];
138
+ }
139
+ return out;
140
+ }
141
+ const TRACKING_COMPENSATION_TW = -4;
142
+ function runOptions(info) {
143
+ return {
144
+ bold: info.bold,
145
+ italics: info.italics,
146
+ underline: info.underline ? {} : void 0,
147
+ strike: info.strike,
148
+ color: info.color,
149
+ size: info.size,
150
+ font: info.font,
151
+ shading: info.highlight ? { fill: info.highlight } : void 0,
152
+ characterSpacing: TRACKING_COMPENSATION_TW + (info.trackingTw ?? 0)
153
+ };
154
+ }
155
+ let measureCanvas2d = null;
156
+ function measureTextPx(text, font) {
157
+ if (!measureCanvas2d)
158
+ measureCanvas2d = document.createElement("canvas").getContext("2d");
159
+ if (!measureCanvas2d)
160
+ return 0;
161
+ measureCanvas2d.font = font;
162
+ return measureCanvas2d.measureText(text).width;
163
+ }
164
+ function primaryFont(cs) {
165
+ const family = cs.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
166
+ return family || void 0;
167
+ }
168
+ function inlineToRuns(node, base, ctx) {
169
+ const runs = [];
170
+ node.forEach((child) => {
171
+ if (child.type.name === "footnoteRef") {
172
+ const text2 = String(child.attrs.text ?? "").trim();
173
+ if (ctx && text2) {
174
+ ctx.footnotes.push({ html: text2, sizePt: child.attrs.size ?? null });
175
+ runs.push(new FootnoteReferenceRun(ctx.footnotes.length));
176
+ } else if (child.attrs.number) {
177
+ runs.push(new TextRun({ text: `[${child.attrs.number}]`, ...runOptions(base) }));
178
+ }
179
+ return;
180
+ }
181
+ if (child.type.name === "templateVariable") {
182
+ const label = child.attrs.label || child.attrs.varKey || "";
183
+ if (label)
184
+ runs.push(new TextRun({ text: `[${label}]`, ...runOptions(base) }));
185
+ return;
186
+ }
187
+ if (child.type.name === "variableChip") {
188
+ if (child.attrs.text)
189
+ runs.push(new TextRun({ text: child.attrs.text, ...runOptions(base) }));
190
+ return;
191
+ }
192
+ if (child.type.name === "hardBreak") {
193
+ runs.push(new TextRun({ break: 1 }));
194
+ return;
195
+ }
196
+ if (!child.isText || !child.text)
197
+ return;
198
+ const info = { ...base };
199
+ let text = child.text;
200
+ for (const mark of child.marks) {
201
+ if (mark.type.name === "bold")
202
+ info.bold = true;
203
+ if (mark.type.name === "italic")
204
+ info.italics = true;
205
+ if (mark.type.name === "underline")
206
+ info.underline = true;
207
+ if (mark.type.name === "strike")
208
+ info.strike = true;
209
+ if (mark.type.name === "inlineStyle") {
210
+ Object.assign(info, cleanUndefined(parseStyle(mark.attrs.style)));
211
+ const mr = Number.parseFloat(getProp(mark.attrs.style, "margin-right") ?? "");
212
+ if (Number.isFinite(mr) && mr > 0 && !text.endsWith(" "))
213
+ text = `${text} `;
214
+ const ml = Number.parseFloat(getProp(mark.attrs.style, "margin-left") ?? "");
215
+ if (Number.isFinite(ml) && ml > 0 && !text.startsWith(" "))
216
+ text = ` ${text}`;
217
+ }
218
+ }
219
+ runs.push(new TextRun({ text, ...runOptions(info) }));
220
+ });
221
+ return runs;
222
+ }
223
+ function domInlineToRuns(root, fallback = {}) {
224
+ const runs = [];
225
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
226
+ while (true) {
227
+ const node = walker.nextNode();
228
+ if (!node)
229
+ break;
230
+ const text = node.textContent || "";
231
+ if (!text.trim().length && !text.includes(" "))
232
+ continue;
233
+ const parent = node.parentElement;
234
+ if (!parent)
235
+ continue;
236
+ const cs = getComputedStyle(parent);
237
+ runs.push(new TextRun({
238
+ text,
239
+ // sem rFonts o run cai no default Arial do Document — a fonte
240
+ // REAL do preview (ex.: Calibri do TLF) tem que viajar no run
241
+ font: primaryFont(cs) ?? fallback.font,
242
+ size: Math.round((Number.parseFloat(cs.fontSize) || 16) / PX_PER_PT * 2),
243
+ color: cssColorToHex(cs.color) ?? fallback.color,
244
+ bold: (Number.parseInt(cs.fontWeight) || 400) >= 600 || void 0,
245
+ italics: cs.fontStyle.includes("italic") || void 0,
246
+ underline: cs.textDecorationLine?.includes("underline") ? {} : void 0,
247
+ strike: cs.textDecorationLine?.includes("line-through") || void 0,
248
+ characterSpacing: TRACKING_COMPENSATION_TW
249
+ }));
250
+ }
251
+ return runs;
252
+ }
253
+ function headingLevelFor(level) {
254
+ return [HeadingLevel.HEADING_1, HeadingLevel.HEADING_2, HeadingLevel.HEADING_3, HeadingLevel.HEADING_4, HeadingLevel.HEADING_5, HeadingLevel.HEADING_6][level - 1];
255
+ }
256
+ function consumeBreak(ctx) {
257
+ if (ctx.pending.breakBefore) {
258
+ ctx.pending.breakBefore = false;
259
+ return true;
260
+ }
261
+ return false;
262
+ }
263
+ function tinyBreakParagraph() {
264
+ return new Paragraph({
265
+ children: [],
266
+ pageBreakBefore: true,
267
+ spacing: { after: 0, before: 0, line: 20, lineRule: LineRuleType.EXACT }
268
+ });
269
+ }
270
+ function spacingFrom(m, style) {
271
+ if (m) {
272
+ const spacing2 = { after: m.afterTw, before: m.beforeTw };
273
+ if (m.lineExactTw) {
274
+ spacing2.line = m.lineExactTw;
275
+ spacing2.lineRule = LineRuleType.EXACT;
276
+ }
277
+ return spacing2;
278
+ }
279
+ const spacing = { after: 160, line: 276 };
280
+ const mb = getProp(style, "margin-bottom");
281
+ if (mb?.endsWith("px"))
282
+ spacing.after = Math.round(Number.parseFloat(mb) * PX_TO_TWIP);
283
+ else if (mb?.endsWith("pt"))
284
+ spacing.after = Math.round(Number.parseFloat(mb) * 20);
285
+ const mt = getProp(style, "margin-top");
286
+ if (mt?.endsWith("px"))
287
+ spacing.before = Math.round(Number.parseFloat(mt) * PX_TO_TWIP);
288
+ const lh = getProp(style, "line-height");
289
+ if (lh) {
290
+ const num = Number.parseFloat(lh);
291
+ if (Number.isFinite(num) && !lh.endsWith("px"))
292
+ spacing.line = Math.round(num * 240);
293
+ }
294
+ return spacing;
295
+ }
296
+ function textBlockToParagraph(node, pos, ctx, opts = {}) {
297
+ const m = measureAt(ctx, pos);
298
+ const style = node.attrs.style;
299
+ const base = m ? { bold: m.bold, italics: m.italics, color: m.color, size: m.sizeHalf, font: primaryFont(m.cs) } : parseStyle(style);
300
+ if (ctx.extraTrackingTw)
301
+ base.trackingTw = ctx.extraTrackingTw;
302
+ const ownIndent = m ? m.marginLeftTw + Math.round((Number.parseFloat(m.cs.paddingLeft) || 0) * PX_TO_TWIP) : cssLengthToTwips(getProp(style, "margin-left")) ?? 0;
303
+ const total = (ctx.indentLeft ?? 0) + ownIndent;
304
+ const breakBefore = consumeBreak(ctx);
305
+ const spacing = spacingFrom(m, style);
306
+ if (spacing.before && !breakBefore)
307
+ spacing.before = Math.max(0, spacing.before - ctx.lastAfterTw);
308
+ ctx.lastAfterTw = spacing.after ?? 0;
309
+ const runs = inlineToRuns(node, base, ctx);
310
+ const paragraph = new Paragraph({
311
+ children: runs,
312
+ // sem alinhamento explícito, corpo de peça vai justificado (o Word
313
+ // usaria esquerda e o .docx sairia diferente da praxe e do preview)
314
+ alignment: m?.align ?? parseAlignment(style) ?? (opts.heading ? void 0 : AlignmentType.JUSTIFIED),
315
+ spacing,
316
+ indent: total ? { left: total } : void 0,
317
+ heading: opts.heading ? headingLevelFor(opts.heading) : void 0,
318
+ pageBreakBefore: breakBefore
319
+ });
320
+ ctx.meta.set(paragraph, { spacer: runs.length === 0 && !opts.heading, breakBefore });
321
+ return paragraph;
322
+ }
323
+ async function srcToDataUrl(src) {
324
+ if (src.startsWith("data:"))
325
+ return src;
326
+ if (!src.startsWith("blob:") && !src.startsWith("http"))
327
+ return null;
328
+ try {
329
+ const blob = await (await fetch(src)).blob();
330
+ return await new Promise((resolve, reject) => {
331
+ const reader = new FileReader();
332
+ reader.onload = () => resolve(String(reader.result));
333
+ reader.onerror = reject;
334
+ reader.readAsDataURL(blob);
335
+ });
336
+ } catch {
337
+ return null;
338
+ }
339
+ }
340
+ async function dataUrlToImageRun(src, widthPx) {
341
+ const match = src.match(/^data:image\/([\w+.-]+);base64,(.+)$/);
342
+ if (!match)
343
+ return null;
344
+ let [, mime, base64] = match;
345
+ if (mime === "svg+xml") {
346
+ try {
347
+ const png = await rasterizeToPng(src);
348
+ const m2 = png.match(/^data:image\/(\w+);base64,(.+)$/);
349
+ if (!m2)
350
+ return null;
351
+ mime = m2[1];
352
+ base64 = m2[2];
353
+ } catch {
354
+ return null;
355
+ }
356
+ }
357
+ const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
358
+ const natural = await new Promise((resolve) => {
359
+ const probe = new Image();
360
+ probe.onload = () => resolve({ w: probe.naturalWidth || 400, h: probe.naturalHeight || 300 });
361
+ probe.onerror = () => resolve({ w: 400, h: 300 });
362
+ probe.src = src;
363
+ });
364
+ const height = Math.round(widthPx * (natural.h / natural.w));
365
+ return new ImageRun({
366
+ data: bytes,
367
+ type: mime === "jpeg" ? "jpg" : mime,
368
+ transformation: { width: Math.round(widthPx), height }
369
+ });
370
+ }
371
+ function rasterizeToPng(src) {
372
+ return new Promise((resolve, reject) => {
373
+ const probe = new Image();
374
+ probe.onload = () => {
375
+ const w = probe.naturalWidth || 100;
376
+ const h = probe.naturalHeight || 100;
377
+ const canvas = document.createElement("canvas");
378
+ canvas.width = w * 2;
379
+ canvas.height = h * 2;
380
+ canvas.getContext("2d").drawImage(probe, 0, 0, canvas.width, canvas.height);
381
+ resolve(canvas.toDataURL("image/png"));
382
+ };
383
+ probe.onerror = reject;
384
+ probe.src = src;
385
+ });
386
+ }
387
+ function islandImageAlignment(islandEl, imgEl) {
388
+ if (!islandEl || !imgEl)
389
+ return AlignmentType.CENTER;
390
+ const left = imgEl.offsetLeft;
391
+ const right = islandEl.clientWidth - (imgEl.offsetLeft + imgEl.offsetWidth);
392
+ if (Math.abs(left - right) <= 3)
393
+ return AlignmentType.CENTER;
394
+ return left < right ? AlignmentType.LEFT : AlignmentType.RIGHT;
395
+ }
396
+ async function islandToDocx(node, pos, contentWidthPx, ctx) {
397
+ const html = node.attrs.html || "";
398
+ const islandEl = domAt(ctx, pos);
399
+ const tpl = document.createElement("template");
400
+ tpl.innerHTML = html;
401
+ const el = tpl.content.firstElementChild;
402
+ if (!el)
403
+ return [];
404
+ const tag = el.tagName.toLowerCase();
405
+ const breakBefore = consumeBreak(ctx);
406
+ if (tag === "ul" || tag === "ol") {
407
+ const paragraphs = [];
408
+ const liveLis = islandEl ? [...islandEl.querySelectorAll(":scope > ul > li, :scope > ol > li, :scope > li")] : [];
409
+ const parsedLis = [...el.querySelectorAll(":scope > li")];
410
+ let n = 1;
411
+ for (let i = 0; i < parsedLis.length; i++) {
412
+ const live = liveLis[i] ?? null;
413
+ const source = live ?? parsedLis[i];
414
+ const m = live ? measureEl(live) : null;
415
+ const marker = tag === "ol" ? `${n++}. ` : "\u2022 ";
416
+ const markerColor = m?.color;
417
+ const runs = live ? domInlineToRuns(live) : [new TextRun({ text: source.textContent?.trim() || "", ...runOptions(parseStyle(source.getAttribute("style"))) })];
418
+ const indentPx = live && islandEl ? live.offsetLeft : 24;
419
+ paragraphs.push(new Paragraph({
420
+ children: [
421
+ new TextRun({ text: marker, color: markerColor, size: m?.sizeHalf, font: m ? primaryFont(m.cs) : void 0 }),
422
+ ...runs
423
+ ],
424
+ spacing: m ? { after: m.afterTw, before: m.beforeTw, ...m.lineExactTw ? { line: m.lineExactTw, lineRule: LineRuleType.EXACT } : {} } : { after: 80 },
425
+ indent: { left: Math.round(indentPx * PX_TO_TWIP) },
426
+ alignment: m?.align,
427
+ pageBreakBefore: paragraphs.length === 0 && breakBefore
428
+ }));
429
+ }
430
+ if (breakBefore && paragraphs.length)
431
+ ctx.meta.set(paragraphs[0], { breakBefore: true });
432
+ return paragraphs;
433
+ }
434
+ if (tag === "img" || el.querySelector("img")) {
435
+ const parsedImg = tag === "img" ? el : el.querySelector("img");
436
+ const liveImg = islandEl?.querySelector("img");
437
+ const src = await srcToDataUrl(parsedImg.getAttribute("src") || "");
438
+ if (!src)
439
+ return [];
440
+ const styleW = getProp(parsedImg.getAttribute("style"), "width");
441
+ const widthPx = Math.min(
442
+ liveImg?.offsetWidth || (styleW?.endsWith("px") ? Number.parseFloat(styleW) : contentWidthPx),
443
+ contentWidthPx
444
+ );
445
+ const run = await dataUrlToImageRun(src, widthPx);
446
+ if (!run)
447
+ return [];
448
+ const islandM = islandEl ? measureEl(islandEl) : null;
449
+ const before = breakBefore ? islandM?.beforeTw ?? 0 : Math.max(0, (islandM?.beforeTw ?? 0) - ctx.lastAfterTw);
450
+ ctx.lastAfterTw = islandM?.afterTw ?? 0;
451
+ const imgParagraph = new Paragraph({
452
+ alignment: islandImageAlignment(islandEl, liveImg),
453
+ children: [run],
454
+ spacing: { after: islandM?.afterTw ?? 0, before },
455
+ pageBreakBefore: breakBefore
456
+ });
457
+ ctx.meta.set(imgParagraph, { breakBefore });
458
+ return [imgParagraph];
459
+ }
460
+ if (tag === "svg") {
461
+ const liveSvg = islandEl?.querySelector("svg");
462
+ const rectW = liveSvg ? liveSvg.getBoundingClientRect().width / zoomFactor(islandEl) : 0;
463
+ const wAttr = Number.parseFloat(el.getAttribute("width") || "") || 0;
464
+ const widthPx = Math.min(rectW || wAttr || contentWidthPx, contentWidthPx);
465
+ try {
466
+ let svgHtml = html;
467
+ if (!svgHtml.includes("xmlns"))
468
+ svgHtml = svgHtml.replace("<svg", '<svg xmlns="http://www.w3.org/2000/svg"');
469
+ if (liveSvg) {
470
+ try {
471
+ const bbox = liveSvg.getBBox();
472
+ const vb = (el.getAttribute("viewBox") || "").trim().split(/[\s,]+/).map(Number);
473
+ const declaredH = Number.parseFloat(el.getAttribute("height") || "") || 0;
474
+ const neededH = Math.ceil(bbox.y + bbox.height) + 2;
475
+ if (vb.length === 4 && Number.isFinite(vb[3]) && neededH > vb[1] + vb[3]) {
476
+ const newVbH = neededH - vb[1];
477
+ const scale = declaredH && vb[3] ? declaredH / vb[3] : 1;
478
+ svgHtml = svgHtml.replace(/viewBox="[^"]*"/, `viewBox="${vb[0]} ${vb[1]} ${vb[2]} ${newVbH}"`).replace(/height="[^"]*"/, `height="${Math.ceil(newVbH * scale)}"`);
479
+ } else if (vb.length !== 4 && declaredH && neededH > declaredH) {
480
+ svgHtml = svgHtml.replace(/height="[^"]*"/, `height="${neededH}"`);
481
+ }
482
+ } catch {
483
+ }
484
+ }
485
+ const dataUrl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgHtml)))}`;
486
+ const run = await dataUrlToImageRun(dataUrl, widthPx);
487
+ if (!run)
488
+ return [];
489
+ const islandM = islandEl ? measureEl(islandEl) : null;
490
+ const before = breakBefore ? islandM?.beforeTw ?? 0 : Math.max(0, (islandM?.beforeTw ?? 0) - ctx.lastAfterTw);
491
+ ctx.lastAfterTw = islandM?.afterTw ?? 0;
492
+ const svgParagraph = new Paragraph({
493
+ alignment: AlignmentType.CENTER,
494
+ children: [run],
495
+ spacing: { after: islandM?.afterTw ?? 0, before },
496
+ pageBreakBefore: breakBefore
497
+ });
498
+ ctx.meta.set(svgParagraph, { breakBefore });
499
+ return [svgParagraph];
500
+ } catch {
501
+ return [];
502
+ }
503
+ }
504
+ const text = el.textContent?.trim();
505
+ return text ? [new Paragraph({ children: [new TextRun({ text })], pageBreakBefore: breakBefore })] : [];
506
+ }
507
+ function zoomFactor(el) {
508
+ const sheet = el.closest(".poc-sheet");
509
+ if (!sheet || !sheet.offsetWidth)
510
+ return 1;
511
+ return sheet.getBoundingClientRect().width / sheet.offsetWidth || 1;
512
+ }
513
+ const NO_BORDER = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" };
514
+ const INVISIBLE_BORDERS = { top: NO_BORDER, bottom: NO_BORDER, left: NO_BORDER, right: NO_BORDER, insideHorizontal: NO_BORDER, insideVertical: NO_BORDER };
515
+ function bordersFrom(m, style) {
516
+ if (m) {
517
+ const side2 = (w, s, c) => {
518
+ const px = Number.parseFloat(w);
519
+ if (!px || s === "none" || s === "hidden")
520
+ return void 0;
521
+ return { style: BorderStyle.SINGLE, size: Math.max(2, Math.round(px / PX_PER_PT * 8)), color: cssColorToHex(c) ?? "000000" };
522
+ };
523
+ const top = side2(m.cs.borderTopWidth, m.cs.borderTopStyle, m.cs.borderTopColor);
524
+ const bottom = side2(m.cs.borderBottomWidth, m.cs.borderBottomStyle, m.cs.borderBottomColor);
525
+ const left = side2(m.cs.borderLeftWidth, m.cs.borderLeftStyle, m.cs.borderLeftColor);
526
+ const right = side2(m.cs.borderRightWidth, m.cs.borderRightStyle, m.cs.borderRightColor);
527
+ if (!top && !bottom && !left && !right)
528
+ return void 0;
529
+ return {
530
+ top: top ?? NO_BORDER,
531
+ bottom: bottom ?? NO_BORDER,
532
+ left: left ?? NO_BORDER,
533
+ right: right ?? NO_BORDER
534
+ };
535
+ }
536
+ const border = getProp(style, "border");
537
+ if (!border)
538
+ return void 0;
539
+ const width = border.match(/(\d+(?:\.\d+)?)px/)?.[1];
540
+ const color = cssColorToHex(border.match(/#[0-9a-f]{3,6}|rgba?\([^)]*\)/i)?.[0] || "");
541
+ if (border.includes("none") || !width && !color)
542
+ return void 0;
543
+ const sz = Math.max(4, Math.round(Number.parseFloat(width || "1") / PX_PER_PT * 8));
544
+ const side = { style: BorderStyle.SINGLE, size: sz, color: color || "000000" };
545
+ return { top: side, bottom: side, left: side, right: side };
546
+ }
547
+ async function walkChildren(node, pos, widthPx, ctx) {
548
+ const out = [];
549
+ let childPos = pos + 1;
550
+ for (let i = 0; i < node.childCount; i++) {
551
+ const child = node.child(i);
552
+ out.push(...await nodeToDocx(child, childPos, widthPx, ctx));
553
+ childPos += child.nodeSize;
554
+ }
555
+ return out;
556
+ }
557
+ async function flexRowToDocx(node, pos, contentWidthPx, ctx) {
558
+ const cellsIn = [];
559
+ let childPos = pos + 1;
560
+ for (let i = 0; i < node.childCount; i++) {
561
+ const dom = domAt(ctx, childPos);
562
+ if (!dom || dom.offsetWidth > 0)
563
+ cellsIn.push({ node: node.child(i), pos: childPos, dom });
564
+ childPos += node.child(i).nodeSize;
565
+ }
566
+ if (!cellsIn.length)
567
+ return [];
568
+ const rowDom = domAt(ctx, pos);
569
+ const parent = rowDom?.parentElement;
570
+ const zoom = rowDom ? zoomFactor(rowDom) : 1;
571
+ const parentRect = parent?.getBoundingClientRect();
572
+ const columnWidthPx = parent?.clientWidth || contentWidthPx;
573
+ const rectOf = (el) => el ? el.getBoundingClientRect() : null;
574
+ const lines = [];
575
+ for (const cell of cellsIn) {
576
+ const top = rectOf(cell.dom)?.top ?? 0;
577
+ const line = lines[lines.length - 1];
578
+ const lineTop = line ? rectOf(line[0].dom)?.top ?? 0 : 0;
579
+ if (line && Math.abs(top - lineTop) <= 8 * zoom)
580
+ line.push(cell);
581
+ else
582
+ lines.push([cell]);
583
+ }
584
+ const tables = [];
585
+ for (const line of lines) {
586
+ const rawPx = line.map((cell) => cell.dom?.offsetWidth || contentWidthPx / line.length);
587
+ const totalPx = rawPx.reduce((a, b) => a + b, 0);
588
+ const scale = totalPx > contentWidthPx ? contentWidthPx / totalPx : 1;
589
+ const cells = [];
590
+ const widths = [];
591
+ for (let i = 0; i < line.length; i++) {
592
+ const cell = line[i];
593
+ const cellWidthPx = rawPx[i] * scale;
594
+ widths.push(Math.round(cellWidthPx * PX_TO_TWIP));
595
+ const cellCs = cell.dom ? getComputedStyle(cell.dom) : null;
596
+ const cellCtx = { ...ctx, insideCell: true, indentLeft: 0, lastAfterTw: 0, pending: { breakBefore: false } };
597
+ const cellBorders = bordersFrom(measureAt(ctx, cell.pos), cell.node.attrs.style);
598
+ const cellBg = cellShading(cellCs);
599
+ const hoistBox = Boolean(cellBorders || cellBg) && isPlainContainer(cell.node);
600
+ const children = hoistBox ? await walkChildren(cell.node, cell.pos, cellWidthPx, cellCtx) : await nodeToDocx(cell.node, cell.pos, cellWidthPx, cellCtx);
601
+ cells.push(new TableCell({
602
+ children: children.length ? children : [new Paragraph({ children: [] })],
603
+ width: { size: Math.round(cellWidthPx * PX_TO_TWIP), type: WidthType.DXA },
604
+ borders: hoistBox && cellBorders || { top: NO_BORDER, bottom: NO_BORDER, left: NO_BORDER, right: NO_BORDER },
605
+ shading: hoistBox ? cellBg : void 0,
606
+ // o padding da coluna vira margem de célula (era do container
607
+ // que deixou de existir ao subir a caixa para cá)
608
+ margins: hoistBox && cellCs ? {
609
+ top: Math.round((Number.parseFloat(cellCs.paddingTop) || 0) * PX_TO_TWIP),
610
+ bottom: Math.round((Number.parseFloat(cellCs.paddingBottom) || 0) * PX_TO_TWIP),
611
+ left: Math.round((Number.parseFloat(cellCs.paddingLeft) || 0) * PX_TO_TWIP),
612
+ right: Math.round((Number.parseFloat(cellCs.paddingRight) || 0) * PX_TO_TWIP)
613
+ } : void 0,
614
+ verticalAlign: cellCs?.display.includes("flex") && cellCs.flexDirection === "column" && cellCs.justifyContent === "center" ? VerticalAlign.CENTER : void 0
615
+ }));
616
+ }
617
+ let alignment;
618
+ const firstRect = rectOf(line[0].dom);
619
+ const lastRect = rectOf(line[line.length - 1].dom);
620
+ if (firstRect && lastRect && parentRect && scale === 1) {
621
+ const leftGap = (firstRect.left - parentRect.left) / zoom;
622
+ const rightGap = columnWidthPx - (lastRect.right - parentRect.left) / zoom;
623
+ if (leftGap > 8 || rightGap > 8) {
624
+ alignment = Math.abs(leftGap - rightGap) <= 8 ? AlignmentType.CENTER : leftGap > rightGap + 8 ? AlignmentType.RIGHT : void 0;
625
+ }
626
+ }
627
+ tables.push(new Table({
628
+ rows: [new TableRow({ children: cells })],
629
+ width: { size: widths.reduce((a, b) => a + b, 0), type: WidthType.DXA },
630
+ borders: INVISIBLE_BORDERS,
631
+ columnWidths: widths,
632
+ alignment,
633
+ layout: TableLayoutType.FIXED
634
+ }));
635
+ }
636
+ return tables;
637
+ }
638
+ function isPlainContainer(node) {
639
+ return node.type.name === "styledDiv" && !isFlexRow(node);
640
+ }
641
+ function cellShading(cs) {
642
+ const bg = cs ? cssColorToHex(cs.backgroundColor) : null;
643
+ return bg && bg.toLowerCase() !== "ffffff" ? { fill: bg } : void 0;
644
+ }
645
+ function isFlexRow(node) {
646
+ if (node.type.name !== "styledDiv")
647
+ return false;
648
+ const display = getProp(node.attrs.style, "display");
649
+ if (display !== "flex" && display !== "table")
650
+ return false;
651
+ if (getProp(node.attrs.style, "flex-direction") === "column")
652
+ return false;
653
+ return node.childCount > 1;
654
+ }
655
+ async function tableToDocx(node, pos, contentWidthPx, ctx) {
656
+ const tableDomRaw = domAt(ctx, pos);
657
+ const tableEl = tableDomRaw?.tagName === "TABLE" ? tableDomRaw : tableDomRaw?.querySelector("table") ?? null;
658
+ const breakBefore = consumeBreak(ctx);
659
+ const rows = [];
660
+ const colWidths = [];
661
+ let rowPos = pos + 1;
662
+ for (let r = 0; r < node.childCount; r++) {
663
+ const rowNode = node.child(r);
664
+ const cellsOut = [];
665
+ let cellPos = rowPos + 1;
666
+ for (let c = 0; c < rowNode.childCount; c++) {
667
+ const cellNode = rowNode.child(c);
668
+ const cellM = measureAt(ctx, cellPos);
669
+ const cellContent = [];
670
+ let extraTrackingTw = 0;
671
+ if (cellM?.el) {
672
+ const innerPx = cellM.el.clientWidth - (Number.parseFloat(cellM.cs.paddingLeft) || 0) - (Number.parseFloat(cellM.cs.paddingRight) || 0);
673
+ const longest = (cellNode.textContent || "").split(/\s+/).reduce((a, b) => b.length > a.length ? b : a, "");
674
+ if (longest.length >= 4 && innerPx > 0) {
675
+ const font = `${cellM.cs.fontWeight} ${cellM.cs.fontSize} ${cellM.cs.fontFamily}`;
676
+ const projectedPx = measureTextPx(longest, font) * 1.04;
677
+ if (projectedPx > innerPx) {
678
+ const deficitPt = (projectedPx - innerPx) / PX_PER_PT;
679
+ extraTrackingTw = Math.max(-20, -Math.ceil(deficitPt * 20 / longest.length) - 1);
680
+ }
681
+ }
682
+ }
683
+ const cellCtx = { ...ctx, insideCell: true, indentLeft: 0, lastAfterTw: 0, pending: { breakBefore: false }, extraTrackingTw: extraTrackingTw || void 0 };
684
+ let childPos = cellPos + 1;
685
+ for (let k = 0; k < cellNode.childCount; k++) {
686
+ const child = cellNode.child(k);
687
+ if (child.isTextblock)
688
+ cellContent.push(textBlockToParagraph(child, childPos, cellCtx));
689
+ else
690
+ cellContent.push(...await nodeToDocx(child, childPos, cellM?.el.clientWidth || contentWidthPx / 2, cellCtx));
691
+ childPos += child.nodeSize;
692
+ }
693
+ if (!cellContent.length)
694
+ cellContent.push(new Paragraph({ children: [new TextRun({ text: cellNode.textContent || "" })] }));
695
+ const colspan = cellNode.attrs.colspan > 1 ? cellNode.attrs.colspan : 1;
696
+ if (r === 0 && cellM?.el) {
697
+ const per = Math.round(cellM.el.offsetWidth * PX_TO_TWIP / colspan);
698
+ for (let s = 0; s < colspan; s++)
699
+ colWidths.push(per);
700
+ }
701
+ const bg = cellM ? cssColorToHex(cellM.cs.backgroundColor) : cssColorToHex(getProp(cellNode.attrs.style, "background-color"));
702
+ cellsOut.push(new TableCell({
703
+ children: cellContent,
704
+ width: cellM?.el ? { size: Math.round(cellM.el.offsetWidth * PX_TO_TWIP), type: WidthType.DXA } : void 0,
705
+ columnSpan: colspan > 1 ? colspan : void 0,
706
+ rowSpan: cellNode.attrs.rowspan > 1 ? cellNode.attrs.rowspan : void 0,
707
+ shading: bg && bg.toLowerCase() !== "ffffff" ? { fill: bg } : void 0,
708
+ borders: bordersFrom(cellM, cellNode.attrs.style),
709
+ verticalAlign: cellM?.cs.verticalAlign === "middle" ? VerticalAlign.CENTER : void 0,
710
+ margins: cellM ? {
711
+ top: Math.round((Number.parseFloat(cellM.cs.paddingTop) || 0) * PX_TO_TWIP),
712
+ bottom: Math.round((Number.parseFloat(cellM.cs.paddingBottom) || 0) * PX_TO_TWIP),
713
+ left: Math.round((Number.parseFloat(cellM.cs.paddingLeft) || 0) * PX_TO_TWIP),
714
+ right: Math.round((Number.parseFloat(cellM.cs.paddingRight) || 0) * PX_TO_TWIP)
715
+ } : void 0
716
+ }));
717
+ cellPos += cellNode.nodeSize;
718
+ }
719
+ if (cellsOut.length)
720
+ rows.push(new TableRow({ children: cellsOut }));
721
+ rowPos += rowNode.nodeSize;
722
+ }
723
+ if (!rows.length)
724
+ return [];
725
+ const tableWidthTw = tableEl?.offsetWidth ? Math.round(tableEl.offsetWidth * PX_TO_TWIP) : Math.round(contentWidthPx * PX_TO_TWIP);
726
+ let nCols = colWidths.length;
727
+ if (!nCols) {
728
+ const firstRow = node.child(0);
729
+ nCols = 0;
730
+ for (let c = 0; c < firstRow.childCount; c++)
731
+ nCols += Math.max(1, firstRow.child(c).attrs.colspan || 1);
732
+ const per = Math.floor(tableWidthTw / Math.max(1, nCols));
733
+ for (let s = 0; s < nCols; s++)
734
+ colWidths.push(per);
735
+ }
736
+ const tableM = tableEl ? measureEl(tableEl) : null;
737
+ const out = [];
738
+ if (breakBefore) {
739
+ const tiny = tinyBreakParagraph();
740
+ ctx.meta.set(tiny, { breakBefore: true });
741
+ out.push(tiny);
742
+ }
743
+ out.push(new Table({
744
+ rows,
745
+ width: { size: tableWidthTw, type: WidthType.DXA },
746
+ columnWidths: colWidths,
747
+ layout: TableLayoutType.FIXED
748
+ }));
749
+ if (tableM && tableM.afterTw > 0) {
750
+ const spacer = new Paragraph({
751
+ children: [],
752
+ spacing: { after: 0, before: 0, line: Math.max(20, tableM.afterTw), lineRule: LineRuleType.EXACT }
753
+ });
754
+ ctx.meta.set(spacer, { spacer: true });
755
+ out.push(spacer);
756
+ }
757
+ ctx.lastAfterTw = tableM?.afterTw ?? 0;
758
+ return out;
759
+ }
760
+ async function nodeToDocx(node, pos, contentWidthPx, ctx) {
761
+ if (ctx.breakSet.has(pos))
762
+ ctx.pending.breakBefore = true;
763
+ switch (node.type.name) {
764
+ case "minutaBlock":
765
+ return walkChildren(node, pos, contentWidthPx, ctx);
766
+ case "blockquote": {
767
+ const m = measureAt(ctx, pos);
768
+ const styleIndent = m ? m.marginLeftTw : cssLengthToTwips(getProp(node.attrs.style, "margin-left"));
769
+ const indentLeft = styleIndent ?? Math.round(contentWidthPx * 0.35 * PX_TO_TWIP);
770
+ const remaining = Math.max(120, m?.el.clientWidth || contentWidthPx - indentLeft / PX_TO_TWIP);
771
+ return walkChildren(node, pos, remaining, { ...ctx, indentLeft: (ctx.indentLeft ?? 0) + indentLeft });
772
+ }
773
+ case "styledDiv": {
774
+ if (isFlexRow(node)) {
775
+ const loneIndex = pocLoneCellIndex(node);
776
+ if (loneIndex != null) {
777
+ let childPos = pos + 1;
778
+ for (let i = 0; i < loneIndex; i++)
779
+ childPos += node.child(i).nodeSize;
780
+ return nodeToDocx(node.child(loneIndex), childPos, contentWidthPx, ctx);
781
+ }
782
+ const rowM = measureAt(ctx, pos);
783
+ const tables = await flexRowToDocx(node, pos, contentWidthPx, ctx);
784
+ if (tables.length) {
785
+ const breakBefore = consumeBreak(ctx);
786
+ const out2 = [...tables];
787
+ if (breakBefore) {
788
+ const tiny = tinyBreakParagraph();
789
+ ctx.meta.set(tiny, { breakBefore: true });
790
+ out2.unshift(tiny);
791
+ }
792
+ if (rowM && rowM.afterTw > 0) {
793
+ const spacer = new Paragraph({
794
+ children: [],
795
+ spacing: { after: 0, before: 0, line: Math.max(20, rowM.afterTw), lineRule: LineRuleType.EXACT }
796
+ });
797
+ ctx.meta.set(spacer, { spacer: true });
798
+ out2.push(spacer);
799
+ }
800
+ ctx.lastAfterTw = rowM?.afterTw ?? 0;
801
+ return out2;
802
+ }
803
+ }
804
+ const m = measureAt(ctx, pos);
805
+ const boxBorders = bordersFrom(m, node.attrs.style);
806
+ const boxBg = m ? cssColorToHex(m.cs.backgroundColor) : cssColorToHex(getProp(node.attrs.style, "background-color"));
807
+ const padLeftTw = m && !boxBorders ? Math.round((Number.parseFloat(m.cs.paddingLeft) || 0) * PX_TO_TWIP) : 0;
808
+ const prevIndent = ctx.indentLeft;
809
+ if (padLeftTw)
810
+ ctx.indentLeft = (ctx.indentLeft ?? 0) + padLeftTw;
811
+ const out = await walkChildren(node, pos, m?.el.clientWidth || contentWidthPx, ctx);
812
+ ctx.indentLeft = prevIndent;
813
+ if ((boxBorders || boxBg && boxBg.toLowerCase() !== "ffffff") && out.length) {
814
+ return [new Table({
815
+ rows: [new TableRow({
816
+ children: [new TableCell({
817
+ children: out,
818
+ borders: boxBorders,
819
+ shading: boxBg && boxBg.toLowerCase() !== "ffffff" ? { fill: boxBg } : void 0,
820
+ width: { size: Math.round((m?.el.offsetWidth || contentWidthPx) * PX_TO_TWIP), type: WidthType.DXA }
821
+ })]
822
+ })],
823
+ width: { size: Math.round((m?.el.offsetWidth || contentWidthPx) * PX_TO_TWIP), type: WidthType.DXA },
824
+ columnWidths: [Math.round((m?.el.offsetWidth || contentWidthPx) * PX_TO_TWIP)],
825
+ layout: TableLayoutType.FIXED
826
+ })];
827
+ }
828
+ return out;
829
+ }
830
+ case "paragraph": {
831
+ if (node.textContent.trim() === "PAGEBREAK") {
832
+ if (ctx.breakSet.size)
833
+ return [];
834
+ return [new Paragraph({ children: [new PageBreak()] })];
835
+ }
836
+ return [textBlockToParagraph(node, pos, ctx)];
837
+ }
838
+ case "heading":
839
+ return [textBlockToParagraph(node, pos, ctx, { heading: node.attrs.level || 2 })];
840
+ case "htmlIsland":
841
+ return islandToDocx(node, pos, contentWidthPx, ctx);
842
+ case "table":
843
+ return tableToDocx(node, pos, contentWidthPx, ctx);
844
+ case "missingImage": {
845
+ const aviso = new Paragraph({
846
+ children: [new TextRun({
847
+ text: `A imagem ${node.attrs.variableId ?? ""} est\xE1 faltando`,
848
+ italics: true,
849
+ color: "773613"
850
+ })],
851
+ alignment: AlignmentType.CENTER,
852
+ border: {
853
+ top: { style: BorderStyle.DASHED, size: 4, color: "DF8B0D" },
854
+ bottom: { style: BorderStyle.DASHED, size: 4, color: "DF8B0D" },
855
+ left: { style: BorderStyle.DASHED, size: 4, color: "DF8B0D" },
856
+ right: { style: BorderStyle.DASHED, size: 4, color: "DF8B0D" }
857
+ },
858
+ pageBreakBefore: consumeBreak(ctx)
859
+ });
860
+ return [aviso];
861
+ }
862
+ case "horizontalRule":
863
+ return [new Paragraph({ children: [], border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: "AAAAAA" } } })];
864
+ case "bulletList":
865
+ case "orderedList": {
866
+ const paragraphs = [];
867
+ let n = 1;
868
+ let liPos = pos + 1;
869
+ for (let i = 0; i < node.childCount; i++) {
870
+ const li = node.child(i);
871
+ let childPos = liPos + 1;
872
+ li.forEach((child) => {
873
+ if (child.isTextblock) {
874
+ const m = measureAt(ctx, childPos);
875
+ const base = m ? { bold: m.bold, italics: m.italics, color: m.color, size: m.sizeHalf } : parseStyle(child.attrs.style);
876
+ const marker = node.type.name === "orderedList" ? `${n}. ` : "\u2022 ";
877
+ const liDom = domAt(ctx, liPos);
878
+ const indentPx = liDom?.offsetLeft ?? 48;
879
+ const liBreak = consumeBreak(ctx);
880
+ const liParagraph = new Paragraph({
881
+ children: [new TextRun({ text: marker, ...runOptions(base) }), ...inlineToRuns(child, base, ctx)],
882
+ spacing: spacingFrom(m, child.attrs.style),
883
+ indent: { left: Math.round(indentPx * PX_TO_TWIP) },
884
+ pageBreakBefore: liBreak
885
+ });
886
+ ctx.meta.set(liParagraph, { breakBefore: liBreak });
887
+ paragraphs.push(liParagraph);
888
+ }
889
+ childPos += child.nodeSize;
890
+ });
891
+ n++;
892
+ liPos += li.nodeSize;
893
+ }
894
+ return paragraphs;
895
+ }
896
+ default: {
897
+ if (node.isTextblock)
898
+ return [textBlockToParagraph(node, pos, ctx)];
899
+ return walkChildren(node, pos, contentWidthPx, ctx);
900
+ }
901
+ }
902
+ }
903
+ async function chromeHtmlToRuns(html) {
904
+ const runs = [];
905
+ if (!html)
906
+ return runs;
907
+ const tpl = document.createElement("template");
908
+ tpl.innerHTML = html;
909
+ const walker = document.createTreeWalker(tpl.content, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
910
+ while (true) {
911
+ const node = walker.nextNode();
912
+ if (!node)
913
+ break;
914
+ if (node.nodeType === Node.TEXT_NODE) {
915
+ const text = node.textContent || "";
916
+ if (!text.trim())
917
+ continue;
918
+ const closest = (tag) => !!node.parentElement?.closest(tag);
919
+ runs.push(new TextRun({
920
+ text,
921
+ size: 17,
922
+ // 11px do preview ≈ 8.25pt
923
+ color: "64748B",
924
+ bold: closest("b") || closest("strong"),
925
+ italics: closest("i") || closest("em"),
926
+ underline: closest("u") ? {} : void 0
927
+ }));
928
+ } else if (node instanceof HTMLImageElement) {
929
+ const src = node.getAttribute("src") || "";
930
+ const [, mime = "", base64 = ""] = src.match(/^data:image\/(\w+);base64,(.+)$/) || [];
931
+ if (!base64 || mime === "svg+xml")
932
+ continue;
933
+ const style = node.getAttribute("style");
934
+ const styleW = Number.parseFloat(getProp(style, "width") ?? "");
935
+ const styleH = Number.parseFloat(getProp(style, "height") ?? "");
936
+ let width;
937
+ let height;
938
+ if (Number.isFinite(styleW) && Number.isFinite(styleH) && styleW > 0 && styleH > 0) {
939
+ width = Math.round(styleW);
940
+ height = Math.round(styleH);
941
+ } else {
942
+ const dims = await new Promise((resolve) => {
943
+ const probe = new Image();
944
+ probe.onload = () => resolve({ w: probe.naturalWidth || 120, h: probe.naturalHeight || 40 });
945
+ probe.onerror = () => resolve({ w: 120, h: 40 });
946
+ probe.src = src;
947
+ });
948
+ const scale = Math.min(1, 44 / dims.h);
949
+ width = Math.round(dims.w * scale);
950
+ height = Math.round(dims.h * scale);
951
+ }
952
+ runs.push(new ImageRun({
953
+ data: Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)),
954
+ type: mime === "jpeg" ? "jpg" : mime,
955
+ transformation: { width, height }
956
+ }));
957
+ }
958
+ }
959
+ return runs;
960
+ }
961
+ const PX_TO_EMU = 9525;
962
+ const A4_PAGE_HEIGHT_PX = 1123;
963
+ function isChromeCanvasHtml(html) {
964
+ return !!html && html.includes("data-el=");
965
+ }
966
+ function rectToPngDataUrl(widthPx, heightPx, color) {
967
+ const canvas = document.createElement("canvas");
968
+ canvas.width = Math.max(2, Math.round(widthPx * 2));
969
+ canvas.height = Math.max(2, Math.round(heightPx * 2));
970
+ const ctx2d = canvas.getContext("2d");
971
+ if (!ctx2d)
972
+ return null;
973
+ ctx2d.fillStyle = color || "#000";
974
+ ctx2d.fillRect(0, 0, canvas.width, canvas.height);
975
+ return canvas.toDataURL("image/png");
976
+ }
977
+ function styleFontHalfPt(fontSize) {
978
+ if (!fontSize)
979
+ return void 0;
980
+ const num = Number.parseFloat(fontSize);
981
+ if (!Number.isFinite(num))
982
+ return void 0;
983
+ return Math.round(fontSize.endsWith("pt") ? num * 2 : num * 1.5);
984
+ }
985
+ function canvasTextRuns(root) {
986
+ const firstFamily = (value) => value.split(",")[0]?.replace(/["']/g, "").trim() || void 0;
987
+ const base = {
988
+ font: root.style.fontFamily ? firstFamily(root.style.fontFamily) : void 0,
989
+ size: styleFontHalfPt(root.style.fontSize) ?? 17,
990
+ color: cssColorToHex(root.style.color) ?? "64748B",
991
+ bold: root.style.fontWeight === "bold" || Number.parseInt(root.style.fontWeight) >= 600 || void 0,
992
+ italics: root.style.fontStyle === "italic" || void 0,
993
+ underline: root.style.textDecoration?.includes("underline") || void 0
994
+ };
995
+ const runs = [];
996
+ const walk = (node, info) => {
997
+ if (node.nodeType === Node.TEXT_NODE) {
998
+ const text = node.textContent ?? "";
999
+ if (text)
1000
+ runs.push(new TextRun({ text, ...runOptions(info) }));
1001
+ return;
1002
+ }
1003
+ if (!(node instanceof HTMLElement))
1004
+ return;
1005
+ if (node.tagName === "BR") {
1006
+ runs.push(new TextRun({ break: 1 }));
1007
+ return;
1008
+ }
1009
+ if (node instanceof HTMLImageElement) {
1010
+ const src = node.getAttribute("src") || "";
1011
+ const match = src.match(/^data:image\/([\w+.-]+);base64,(.+)$/);
1012
+ if (match && match[1] !== "svg+xml") {
1013
+ const w = Number.parseFloat(node.style.width) || node.width || 120;
1014
+ const h = Number.parseFloat(node.style.height) || node.height || 28;
1015
+ runs.push(new ImageRun({
1016
+ data: Uint8Array.from(atob(match[2]), (c) => c.charCodeAt(0)),
1017
+ type: match[1] === "jpeg" ? "jpg" : match[1],
1018
+ transformation: { width: Math.round(w), height: Math.round(h) }
1019
+ }));
1020
+ }
1021
+ return;
1022
+ }
1023
+ const next = { ...info };
1024
+ if (node.tagName === "B" || node.tagName === "STRONG")
1025
+ next.bold = true;
1026
+ if (node.tagName === "I" || node.tagName === "EM")
1027
+ next.italics = true;
1028
+ if (node.tagName === "U")
1029
+ next.underline = true;
1030
+ if (node.style.fontFamily)
1031
+ next.font = firstFamily(node.style.fontFamily);
1032
+ const half = styleFontHalfPt(node.style.fontSize);
1033
+ if (half)
1034
+ next.size = half;
1035
+ const color = cssColorToHex(node.style.color);
1036
+ if (color)
1037
+ next.color = color;
1038
+ if (node.style.fontWeight === "bold" || Number.parseInt(node.style.fontWeight) >= 600)
1039
+ next.bold = true;
1040
+ if ((node.tagName === "DIV" || node.tagName === "P") && runs.length)
1041
+ runs.push(new TextRun({ break: 1 }));
1042
+ node.childNodes.forEach((child) => walk(child, next));
1043
+ };
1044
+ root.childNodes.forEach((child) => walk(child, base));
1045
+ return runs;
1046
+ }
1047
+ async function chromeCanvasToDocx(html, bandTopPx) {
1048
+ const anchors = [];
1049
+ const frames = [];
1050
+ const tpl = document.createElement("template");
1051
+ tpl.innerHTML = html;
1052
+ let zIndex = 0;
1053
+ for (const el of [...tpl.content.children]) {
1054
+ const kind = el.getAttribute("data-el");
1055
+ if (!kind)
1056
+ continue;
1057
+ const left = Number.parseFloat(el.style.left) || 0;
1058
+ const top = Number.parseFloat(el.style.top) || 0;
1059
+ const width = Number.parseFloat(el.style.width) || 0;
1060
+ const height = Number.parseFloat(el.style.height) || 0;
1061
+ if (kind === "img" || kind === "rect") {
1062
+ let src = null;
1063
+ let w = width;
1064
+ let h = height;
1065
+ if (kind === "rect") {
1066
+ w = Math.max(2, width || 100);
1067
+ h = Math.max(2, height || 8);
1068
+ src = rectToPngDataUrl(w, h, el.style.background || el.style.backgroundColor || "#000");
1069
+ } else {
1070
+ src = await srcToDataUrl(el.getAttribute("src") || "");
1071
+ if ((!w || !h) && src) {
1072
+ const dims = await new Promise((resolve) => {
1073
+ const probe = new Image();
1074
+ probe.onload = () => resolve({ w: probe.naturalWidth || 120, h: probe.naturalHeight || 40 });
1075
+ probe.onerror = () => resolve({ w: 120, h: 40 });
1076
+ probe.src = src;
1077
+ });
1078
+ w = w || dims.w;
1079
+ h = h || dims.h;
1080
+ }
1081
+ }
1082
+ const match = src?.match(/^data:image\/([\w+.-]+);base64,(.+)$/);
1083
+ if (!match || match[1] === "svg+xml")
1084
+ continue;
1085
+ anchors.push(new ImageRun({
1086
+ data: Uint8Array.from(atob(match[2]), (c) => c.charCodeAt(0)),
1087
+ type: match[1] === "jpeg" ? "jpg" : match[1],
1088
+ transformation: { width: Math.round(w), height: Math.round(h) },
1089
+ floating: {
1090
+ zIndex: zIndex++,
1091
+ horizontalPosition: { relative: HorizontalPositionRelativeFrom.PAGE, offset: Math.round(left * PX_TO_EMU) },
1092
+ verticalPosition: { relative: VerticalPositionRelativeFrom.PAGE, offset: Math.round((bandTopPx + top) * PX_TO_EMU) },
1093
+ wrap: { type: TextWrappingType.NONE },
1094
+ behindDocument: kind === "rect",
1095
+ allowOverlap: true
1096
+ }
1097
+ }));
1098
+ } else if (kind === "text") {
1099
+ const runs = canvasTextRuns(el);
1100
+ if (!runs.length)
1101
+ continue;
1102
+ const font = `${el.style.fontWeight || "normal"} ${el.style.fontSize || "11px"} ${el.style.fontFamily || "Arial"}`;
1103
+ const longestLinePx = Math.max(24, ...(el.textContent ?? "").split("\n").map((line) => measureTextPx(line, font)));
1104
+ const frameWidthPx = width || Math.min(794 - left, longestLinePx * 1.08 + 8);
1105
+ frames.push(new Paragraph({
1106
+ frame: {
1107
+ type: "absolute",
1108
+ position: {
1109
+ x: Math.round(left * PX_TO_TWIP),
1110
+ y: Math.round((bandTopPx + top) * PX_TO_TWIP)
1111
+ },
1112
+ width: Math.round(frameWidthPx * PX_TO_TWIP),
1113
+ height: Math.round(Math.max(height, 14) * PX_TO_TWIP),
1114
+ anchor: { horizontal: FrameAnchorType.PAGE, vertical: FrameAnchorType.PAGE }
1115
+ },
1116
+ children: runs
1117
+ }));
1118
+ }
1119
+ }
1120
+ return { anchors, frames };
1121
+ }
1122
+ async function buildLogoRun(logoUrl, marginsPx) {
1123
+ try {
1124
+ const response = await fetch(logoUrl);
1125
+ const buffer = await response.arrayBuffer();
1126
+ const mime = response.headers.get("content-type") || "image/png";
1127
+ const type = mime.includes("jpeg") || mime.includes("jpg") ? "jpg" : "png";
1128
+ const blobUrl = URL.createObjectURL(new Blob([buffer], { type: mime }));
1129
+ const dims = await new Promise((resolve) => {
1130
+ const probe = new Image();
1131
+ probe.onload = () => resolve({ w: probe.naturalWidth || 140, h: probe.naturalHeight || 46 });
1132
+ probe.onerror = () => resolve({ w: 140, h: 46 });
1133
+ probe.src = blobUrl;
1134
+ });
1135
+ URL.revokeObjectURL(blobUrl);
1136
+ const maxH = Math.max(24, marginsPx.top - 36);
1137
+ const scale = Math.min(1, maxH / dims.h);
1138
+ return new ImageRun({
1139
+ data: new Uint8Array(buffer),
1140
+ type,
1141
+ transformation: { width: Math.round(dims.w * scale), height: Math.round(dims.h * scale) }
1142
+ });
1143
+ } catch {
1144
+ return null;
1145
+ }
1146
+ }
1147
+ async function buildDocxHeader(chrome, marginsPx) {
1148
+ if (!chrome || !chrome.headerText && !chrome.headerLogoUrl)
1149
+ return void 0;
1150
+ if (isChromeCanvasHtml(chrome.headerText)) {
1151
+ const { anchors, frames } = await chromeCanvasToDocx(chrome.headerText, 0);
1152
+ const anchorRuns = [...anchors];
1153
+ if (chrome.headerLogoUrl) {
1154
+ const logo = await buildLogoRun(chrome.headerLogoUrl, marginsPx);
1155
+ if (logo)
1156
+ anchorRuns.unshift(logo);
1157
+ }
1158
+ if (!anchorRuns.length && !frames.length)
1159
+ return void 0;
1160
+ return new Header({ children: [...frames, new Paragraph({ children: anchorRuns })] });
1161
+ }
1162
+ const runs = [];
1163
+ if (chrome.headerLogoUrl) {
1164
+ const logo = await buildLogoRun(chrome.headerLogoUrl, marginsPx);
1165
+ if (logo)
1166
+ runs.push(logo);
1167
+ }
1168
+ runs.push(...await chromeHtmlToRuns(chrome.headerText || ""));
1169
+ if (!runs.length)
1170
+ return void 0;
1171
+ const alignment = chrome.headerPosition === "center" ? AlignmentType.CENTER : chrome.headerPosition === "right" ? AlignmentType.RIGHT : AlignmentType.LEFT;
1172
+ return new Header({ children: [new Paragraph({ children: runs, alignment })] });
1173
+ }
1174
+ async function buildDocxFooter(chrome, withCustom, marginsPx) {
1175
+ const showPageNumber = chrome?.showPageNumber !== false;
1176
+ const pageNumberParagraph = (extra = [], spacer = false) => new Paragraph({
1177
+ alignment: AlignmentType.RIGHT,
1178
+ children: [
1179
+ ...extra,
1180
+ ...showPageNumber ? [
1181
+ new TextRun({ text: spacer ? " " : "", size: 17, color: "94A3B8" }),
1182
+ new TextRun({ children: ["P\xE1gina ", PageNumber.CURRENT, " de ", PageNumber.TOTAL_PAGES], size: 17, color: "94A3B8" })
1183
+ ] : []
1184
+ ]
1185
+ });
1186
+ if (withCustom && isChromeCanvasHtml(chrome?.footerText)) {
1187
+ const { anchors, frames } = await chromeCanvasToDocx(chrome.footerText, A4_PAGE_HEIGHT_PX - marginsPx.bottom);
1188
+ return new Footer({ children: [...frames, pageNumberParagraph(anchors)] });
1189
+ }
1190
+ const custom = withCustom ? await chromeHtmlToRuns(chrome?.footerText || "") : [];
1191
+ return new Footer({ children: [pageNumberParagraph(custom, custom.length > 0)] });
1192
+ }
1193
+ async function pocExportDocx(editor, margins, chrome, options = {}) {
1194
+ const contentWidthPx = 794 - margins.left - margins.right;
1195
+ const doc = editor.state.doc;
1196
+ const ctx = {
1197
+ view: editor.view,
1198
+ // quebras `soft` (dentro de um parágrafo) não viram pageBreakBefore:
1199
+ // partir o parágrafo em dois no Word entregaria ao advogado um arquivo
1200
+ // que não reflui ao editar. Ali o Word quebra sozinho, no mesmo lugar
1201
+ // ou a uma linha de distância.
1202
+ breakSet: new Set((options.breaks ?? []).filter((b) => !b.soft).map((b) => b.pos)),
1203
+ pending: { breakBefore: false },
1204
+ lastAfterTw: 0,
1205
+ meta: /* @__PURE__ */ new WeakMap(),
1206
+ footnotes: []
1207
+ };
1208
+ const children = [];
1209
+ let pos = 0;
1210
+ for (let i = 0; i < doc.childCount; i++) {
1211
+ const child = doc.child(i);
1212
+ children.push(...await nodeToDocx(child, pos, contentWidthPx, ctx));
1213
+ pos += child.nodeSize;
1214
+ }
1215
+ const cleaned = [];
1216
+ for (const el of children) {
1217
+ if (ctx.meta.get(el)?.breakBefore) {
1218
+ while (cleaned.length && ctx.meta.get(cleaned[cleaned.length - 1])?.spacer)
1219
+ cleaned.pop();
1220
+ }
1221
+ cleaned.push(el);
1222
+ }
1223
+ const header = await buildDocxHeader(chrome, margins);
1224
+ const headerFirstOnly = chrome?.headerScope === "first";
1225
+ const footerFirstOnly = chrome?.footerScope === "first";
1226
+ const footerFull = await buildDocxFooter(chrome, true, margins);
1227
+ const footerPlain = footerFirstOnly ? await buildDocxFooter(chrome, false, margins) : footerFull;
1228
+ const useTitlePage = headerFirstOnly || footerFirstOnly;
1229
+ const footnotes = Object.fromEntries(ctx.footnotes.map((note, i) => {
1230
+ const size = Math.round((note.sizePt ?? 8) * 2);
1231
+ const children2 = footnoteHtmlToRuns(note.html).map((run) => {
1232
+ const textRun = new TextRun({
1233
+ text: run.text,
1234
+ bold: run.bold,
1235
+ italics: run.italics,
1236
+ // link herda a cara de link do Word (sublinhado + azul)
1237
+ underline: run.underline || run.href ? {} : void 0,
1238
+ color: run.href ? "0563C1" : run.color,
1239
+ break: run.break ? 1 : void 0,
1240
+ // personalização por trecho vence o corpo base da nota
1241
+ font: run.font,
1242
+ size: run.sizePt ? Math.round(run.sizePt * 2) : size
1243
+ });
1244
+ return run.href ? new ExternalHyperlink({ children: [textRun], link: run.href }) : textRun;
1245
+ });
1246
+ return [String(i + 1), { children: [new Paragraph({ children: children2 })] }];
1247
+ }));
1248
+ const document2 = new Document({
1249
+ styles: {
1250
+ default: {
1251
+ document: { run: { font: "Arial", size: 20 } }
1252
+ }
1253
+ },
1254
+ footnotes: Object.keys(footnotes).length ? footnotes : void 0,
1255
+ sections: [{
1256
+ properties: {
1257
+ page: {
1258
+ size: { width: A4.width, height: A4.height },
1259
+ margin: {
1260
+ top: margins.top * PX_TO_TWIP,
1261
+ right: margins.right * PX_TO_TWIP,
1262
+ bottom: margins.bottom * PX_TO_TWIP,
1263
+ left: margins.left * PX_TO_TWIP,
1264
+ // distâncias de header/footer DENTRO da margem: o default do
1265
+ // Word (708tw = 1,25cm) é MAIOR que margens pequenas (ex.:
1266
+ // 0,5cm) e faz o rodapé "Página N" EMPURRAR o corpo — a
1267
+ // página perde altura e a paginação diverge do preview
1268
+ header: Math.min(708, Math.max(30, margins.top * PX_TO_TWIP - 280)),
1269
+ footer: Math.min(708, Math.max(30, margins.bottom * PX_TO_TWIP - 280))
1270
+ }
1271
+ },
1272
+ titlePage: useTitlePage || void 0
1273
+ },
1274
+ headers: header ? headerFirstOnly ? { first: header, default: new Header({ children: [] }) } : useTitlePage ? { first: header, default: header } : { default: header } : void 0,
1275
+ footers: useTitlePage ? { first: footerFull, default: footerPlain } : { default: footerFull },
1276
+ children: cleaned
1277
+ }]
1278
+ });
1279
+ return Packer.toBlob(document2);
1280
+ }
1281
+
1282
+ export { pocExportDocx };