@sofereditor/export-docx 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,19 +31,32 @@ var import_docx = require("docx");
31
31
  var BULLET_REF = "ed-bullet";
32
32
  var ORDERED_REF = "ed-ordered";
33
33
  async function documentToDocxBlob(doc, options = {}) {
34
- const built = buildDocument(normalize(doc), options);
35
- return import_docx.Packer.toBlob(built);
34
+ const normalized = normalize(doc);
35
+ const { images, skipped } = await resolveAllImages(
36
+ normalized,
37
+ options.resolveImage ?? defaultResolveImage
38
+ );
39
+ const built = buildDocument(normalized, options, images);
40
+ return { blob: await import_docx.Packer.toBlob(built), skippedImages: skipped };
36
41
  }
37
42
  async function documentToDocxBuffer(doc, options = {}) {
38
- const built = buildDocument(normalize(doc), options);
43
+ const normalized = normalize(doc);
44
+ const { images, skipped } = await resolveAllImages(
45
+ normalized,
46
+ options.resolveImage ?? defaultResolveImage
47
+ );
48
+ const built = buildDocument(normalized, options, images);
39
49
  const buf = await import_docx.Packer.toBuffer(built);
40
- return buf instanceof Uint8Array ? buf : new Uint8Array(buf);
50
+ return {
51
+ buffer: buf instanceof Uint8Array ? buf : new Uint8Array(buf),
52
+ skippedImages: skipped
53
+ };
41
54
  }
42
55
  function normalize(doc) {
43
56
  return (0, import_core.isLegacySerializedDocument)(doc) ? { blocks: doc } : doc;
44
57
  }
45
- function buildDocument(doc, options) {
46
- const children = blocksToDocxChildren(doc);
58
+ function buildDocument(doc, options, images) {
59
+ const children = blocksToDocxChildren(doc, images);
47
60
  const settings = doc.pageSettings ?? import_core.DEFAULT_PAGE_SETTINGS;
48
61
  return new import_docx.Document({
49
62
  creator: options.creator,
@@ -85,53 +98,53 @@ function sectionPropertiesFor(settings) {
85
98
  }
86
99
  };
87
100
  }
88
- function blocksToDocxChildren(doc) {
101
+ function blocksToDocxChildren(doc, images) {
89
102
  const out = [];
90
103
  const blocks = doc.blocks;
91
104
  for (let i = 0; i < blocks.length; i++) {
92
105
  const block = blocks[i];
93
106
  switch (block.type) {
94
107
  case "paragraph":
95
- out.push(makeParagraph(block));
108
+ out.push(makeParagraph(block, images));
96
109
  break;
97
110
  case "heading":
98
- out.push(makeHeading(block));
111
+ out.push(makeHeading(block, images));
99
112
  break;
100
113
  case "blockquote":
101
- out.push(makeBlockquote(block));
114
+ out.push(makeBlockquote(block, images));
102
115
  break;
103
116
  case "codeBlock":
104
- out.push(makeCodeBlock(block));
117
+ out.push(makeCodeBlock(block, images));
105
118
  break;
106
119
  case "listItem":
107
- out.push(makeListItem(block));
120
+ out.push(makeListItem(block, images));
108
121
  break;
109
122
  case "table":
110
- out.push(makeTable(block));
123
+ out.push(makeTable(block, images));
111
124
  break;
112
125
  default:
113
- out.push(makeParagraph(block));
126
+ out.push(makeParagraph(block, images));
114
127
  }
115
128
  }
116
129
  return out;
117
130
  }
118
131
  var ARIAL = { font: "Arial" };
119
- function makeParagraph(block) {
132
+ function makeParagraph(block, images) {
120
133
  return new import_docx.Paragraph({
121
134
  alignment: alignFor(block.attrs.align),
122
135
  bidirectional: block.attrs.dir === "rtl" ? true : void 0,
123
- children: deltaToRuns(block.delta, ARIAL)
136
+ children: deltaToRuns(block.delta, ARIAL, images)
124
137
  });
125
138
  }
126
- function makeHeading(block) {
139
+ function makeHeading(block, images) {
127
140
  const level = clampHeadingLevel(block.attrs.level);
128
141
  return new import_docx.Paragraph({
129
142
  heading: HEADING_LEVELS[level - 1],
130
143
  alignment: alignFor(block.attrs.align),
131
- children: deltaToRuns(block.delta, ARIAL)
144
+ children: deltaToRuns(block.delta, ARIAL, images)
132
145
  });
133
146
  }
134
- function makeBlockquote(block) {
147
+ function makeBlockquote(block, images) {
135
148
  return new import_docx.Paragraph({
136
149
  alignment: alignFor(block.attrs.align),
137
150
  indent: { left: 480 },
@@ -139,27 +152,27 @@ function makeBlockquote(block) {
139
152
  border: {
140
153
  left: { color: "CBD5E1", space: 12, style: import_docx.BorderStyle.SINGLE, size: 12 }
141
154
  },
142
- children: deltaToRuns(block.delta, { ...ARIAL, italics: true })
155
+ children: deltaToRuns(block.delta, { ...ARIAL, italics: true }, images)
143
156
  });
144
157
  }
145
- function makeCodeBlock(block) {
158
+ function makeCodeBlock(block, images) {
146
159
  return new import_docx.Paragraph({
147
160
  spacing: { before: 100, after: 100 },
148
161
  shading: { type: import_docx.ShadingType.CLEAR, color: "auto", fill: "F1F5F9" },
149
- children: deltaToRuns(block.delta, { font: "Consolas", size: 20 })
162
+ children: deltaToRuns(block.delta, { font: "Consolas", size: 20 }, images)
150
163
  });
151
164
  }
152
- function makeListItem(block) {
165
+ function makeListItem(block, images) {
153
166
  const kind = block.attrs.listKind === "ordered" ? "ordered" : "bullet";
154
167
  const reference = kind === "ordered" ? ORDERED_REF : BULLET_REF;
155
168
  const level = clampListLevel(block.attrs.listLevel);
156
169
  return new import_docx.Paragraph({
157
170
  numbering: { reference, level },
158
171
  alignment: alignFor(block.attrs.align),
159
- children: deltaToRuns(block.delta, ARIAL)
172
+ children: deltaToRuns(block.delta, ARIAL, images)
160
173
  });
161
174
  }
162
- function makeTable(block) {
175
+ function makeTable(block, images) {
163
176
  const rows = typeof block.attrs.rows === "number" ? block.attrs.rows : 0;
164
177
  const cols = typeof block.attrs.cols === "number" ? block.attrs.cols : 0;
165
178
  const cells = block.cells ?? [];
@@ -173,29 +186,46 @@ function makeTable(block) {
173
186
  continue;
174
187
  }
175
188
  if (cell.attrs?.covered) continue;
176
- cellsOut.push(makeCell(cell));
189
+ cellsOut.push(makeCell(cell, images));
177
190
  }
178
191
  rowsOut.push(new import_docx.TableRow({ children: cellsOut }));
179
192
  }
193
+ const rawColWidths = block.attrs.colWidths;
194
+ const isValidColWidths = Array.isArray(rawColWidths) && cols > 0 && rawColWidths.length === cols && rawColWidths.every((w) => typeof w === "number" && w > 0);
195
+ const columnWidths = isValidColWidths ? rawColWidths.map((px) => (0, import_docx.convertMillimetersToTwip)((0, import_core.pxToMm)(px))) : void 0;
180
196
  return new import_docx.Table({
181
197
  rows: rowsOut,
182
- width: { size: 100, type: import_docx.WidthType.PERCENTAGE }
198
+ ...columnWidths ? {
199
+ columnWidths,
200
+ // Fix the layout so Word honors the column widths verbatim instead
201
+ // of autofitting to content (the OOXML default when `tblLayout` is
202
+ // absent, which would make `columnWidths` a mere hint).
203
+ layout: import_docx.TableLayoutType.FIXED,
204
+ width: { size: columnWidths.reduce((a, b) => a + b, 0), type: import_docx.WidthType.DXA }
205
+ } : { width: { size: 100, type: import_docx.WidthType.PERCENTAGE } }
183
206
  });
184
207
  }
185
- function makeCell(cell) {
208
+ function makeCell(cell, images) {
186
209
  const span = cell.attrs?.colspan && cell.attrs.colspan > 1 ? cell.attrs.colspan : 1;
187
210
  const rowSpan = cell.attrs?.rowspan && cell.attrs.rowspan > 1 ? cell.attrs.rowspan : 1;
211
+ const fill = cssColorToDocxHex(cell.attrs?.bgColor);
188
212
  return new import_docx.TableCell({
189
213
  columnSpan: span,
190
214
  rowSpan,
191
215
  verticalAlign: import_docx.VerticalAlign.TOP,
192
- children: [new import_docx.Paragraph({ children: deltaToRuns(cell.delta, ARIAL) })]
216
+ shading: fill ? { type: import_docx.ShadingType.CLEAR, color: "auto", fill } : void 0,
217
+ children: [
218
+ new import_docx.Paragraph({
219
+ alignment: alignFor(cell.attrs?.align),
220
+ children: deltaToRuns(cell.delta, ARIAL, images)
221
+ })
222
+ ]
193
223
  });
194
224
  }
195
225
  function emptyCell() {
196
226
  return new import_docx.TableCell({ children: [new import_docx.Paragraph("")] });
197
227
  }
198
- function deltaToRuns(delta, defaults = {}) {
228
+ function deltaToRuns(delta, defaults = {}, images) {
199
229
  if (delta.length === 0) return [new import_docx.TextRun("")];
200
230
  const out = [];
201
231
  for (const op of delta) {
@@ -204,7 +234,7 @@ function deltaToRuns(delta, defaults = {}) {
204
234
  out.push(makeTextRun(op.insert, op.attributes, defaults));
205
235
  } else if ((0, import_core.isImageEmbed)(op.insert)) {
206
236
  const embed = op.insert;
207
- const img = makeImageRun(embed);
237
+ const img = makeImageRun(embed, images);
208
238
  if (img) out.push(img);
209
239
  if (embed.caption && embed.caption.length > 0) {
210
240
  out.push(
@@ -249,12 +279,12 @@ function makeTextRun(text, marks, defaults) {
249
279
  size: fontSize ?? defaults.size
250
280
  });
251
281
  }
252
- function makeImageRun(embed) {
253
- const decoded = decodeDataUrl(embed.src);
254
- if (!decoded) return null;
282
+ function makeImageRun(embed, images) {
283
+ const resolved = images.get(embed.src);
284
+ if (!resolved) return null;
255
285
  return new import_docx.ImageRun({
256
- data: decoded.bytes,
257
- type: decoded.kind,
286
+ data: resolved.data,
287
+ type: resolved.type,
258
288
  transformation: {
259
289
  width: Math.max(1, Math.round(embed.width)),
260
290
  height: Math.max(1, Math.round(embed.height))
@@ -270,6 +300,61 @@ function decodeDataUrl(src) {
270
300
  const bytes = base64ToBytes(base64);
271
301
  return { bytes, kind };
272
302
  }
303
+ async function defaultResolveImage(src) {
304
+ const decoded = decodeDataUrl(src);
305
+ if (decoded) return { data: decoded.bytes, type: decoded.kind };
306
+ if (/^https?:\/\//i.test(src) && typeof fetch === "function") {
307
+ try {
308
+ const res = await fetch(src);
309
+ if (!res.ok) return null;
310
+ const ct = (res.headers.get("content-type") ?? "").toLowerCase();
311
+ const type = ct.includes("png") ? "png" : ct.includes("jpeg") || ct.includes("jpg") ? "jpg" : ct.includes("gif") ? "gif" : ct.includes("bmp") ? "bmp" : ct.includes("svg") ? "svg" : typeFromExtension(src) ?? "png";
312
+ return { data: new Uint8Array(await res.arrayBuffer()), type };
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+ return null;
318
+ }
319
+ function typeFromExtension(src) {
320
+ const m = /\.(png|jpe?g|gif|bmp|svg)(\?|#|$)/i.exec(src);
321
+ if (!m) return null;
322
+ const ext = m[1].toLowerCase();
323
+ return ext === "jpeg" || ext === "jpg" ? "jpg" : ext;
324
+ }
325
+ async function resolveAllImages(doc, resolve) {
326
+ const srcs = /* @__PURE__ */ new Set();
327
+ const collect = (delta) => {
328
+ for (const op of delta) if ((0, import_core.isImageEmbed)(op.insert)) srcs.add(op.insert.src);
329
+ };
330
+ for (const block of doc.blocks) {
331
+ collect(block.delta);
332
+ for (const cell of block.cells ?? []) collect(cell.delta);
333
+ }
334
+ const images = /* @__PURE__ */ new Map();
335
+ await Promise.all(
336
+ [...srcs].map(async (src) => {
337
+ let resolved = null;
338
+ try {
339
+ resolved = await resolve(src);
340
+ } catch {
341
+ resolved = null;
342
+ }
343
+ if (resolved?.type === "svg") resolved = null;
344
+ images.set(src, resolved);
345
+ })
346
+ );
347
+ let skipped = 0;
348
+ const count = (delta) => {
349
+ for (const op of delta)
350
+ if ((0, import_core.isImageEmbed)(op.insert) && !images.get(op.insert.src)) skipped++;
351
+ };
352
+ for (const block of doc.blocks) {
353
+ count(block.delta);
354
+ for (const cell of block.cells ?? []) count(cell.delta);
355
+ }
356
+ return { images, skipped };
357
+ }
273
358
  function base64ToBytes(b64) {
274
359
  if (typeof atob === "function") {
275
360
  const bin = atob(b64);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/docx.ts"],"sourcesContent":["export { documentToDocxBlob, documentToDocxBuffer } from \"./docx\";\nexport type { DocumentToDocxOptions } from \"./docx\";\n","import type {\n AlignValue,\n DeltaOp,\n ImageEmbed,\n LegacySerializedDocument,\n ListKind,\n MarkAttrs,\n SerializedBlock,\n SerializedCell,\n SerializedDocument,\n} from \"@sofereditor/core\";\nimport {\n DEFAULT_PAGE_SETTINGS,\n isImageEmbed,\n isLegacySerializedDocument,\n pxToMm,\n type PageSettings,\n} from \"@sofereditor/core\";\nimport {\n AlignmentType,\n BorderStyle,\n Document,\n HeadingLevel,\n ImageRun,\n LevelFormat,\n Packer,\n Paragraph,\n PageOrientation,\n ShadingType,\n Table,\n TableCell,\n TableRow,\n TextRun,\n UnderlineType,\n VerticalAlign,\n WidthType,\n convertMillimetersToTwip,\n} from \"docx\";\n\nconst BULLET_REF = \"ed-bullet\";\nconst ORDERED_REF = \"ed-ordered\";\n\nexport interface DocumentToDocxOptions {\n /** Document title; embedded in core properties. */\n title?: string;\n /** Author for core properties. */\n creator?: string;\n}\n\n/**\n * Build a docx `Document` from a `SerializedDocument`. Output is a Blob you can\n * download with `downloadBlob` from `@sofereditor/export-pdf`.\n *\n * Mappings:\n * - paragraph / heading / blockquote / codeBlock → Paragraph (with `heading`,\n * shading, monospace font for code)\n * - listItem → Paragraph with `numbering: { reference, level }`\n * - table → docx `Table`. `rowspan`/`colspan` map to TableCell's `rowSpan`/\n * `columnSpan`; \"covered\" cells are skipped (docx infers cover from spans)\n * - inline marks → TextRun props (bold, italics, underline, strike, color,\n * font, size in half-points)\n * - image embeds → ImageRun with the base64 payload decoded to Uint8Array\n */\nexport async function documentToDocxBlob(\n doc: SerializedDocument | LegacySerializedDocument,\n options: DocumentToDocxOptions = {},\n): Promise<Blob> {\n const built = buildDocument(normalize(doc), options);\n return Packer.toBlob(built);\n}\n\n/** Same as `documentToDocxBlob` but returns a Uint8Array (useful in Node/tests). */\nexport async function documentToDocxBuffer(\n doc: SerializedDocument | LegacySerializedDocument,\n options: DocumentToDocxOptions = {},\n): Promise<Uint8Array> {\n const built = buildDocument(normalize(doc), options);\n const buf = await Packer.toBuffer(built);\n // `Packer.toBuffer` returns a Node Buffer at runtime — coerce for consumers.\n return buf instanceof Uint8Array ? buf : new Uint8Array(buf);\n}\n\nfunction normalize(doc: SerializedDocument | LegacySerializedDocument): SerializedDocument {\n return isLegacySerializedDocument(doc) ? { blocks: doc } : doc;\n}\n\nfunction buildDocument(doc: SerializedDocument, options: DocumentToDocxOptions): Document {\n const children = blocksToDocxChildren(doc);\n const settings = doc.pageSettings ?? DEFAULT_PAGE_SETTINGS;\n return new Document({\n creator: options.creator,\n title: options.title,\n numbering: {\n config: [\n {\n reference: BULLET_REF,\n levels: buildBulletLevels(),\n },\n {\n reference: ORDERED_REF,\n levels: buildOrderedLevels(),\n },\n ],\n },\n sections: [\n {\n properties: sectionPropertiesFor(settings),\n children,\n },\n ],\n });\n}\n\nfunction sectionPropertiesFor(settings: PageSettings) {\n return {\n page: {\n size: {\n width: convertMillimetersToTwip(pxToMm(settings.width)),\n height: convertMillimetersToTwip(pxToMm(settings.height)),\n orientation: PageOrientation.PORTRAIT,\n },\n margin: {\n top: convertMillimetersToTwip(pxToMm(settings.marginTop)),\n bottom: convertMillimetersToTwip(pxToMm(settings.marginBottom)),\n left: convertMillimetersToTwip(pxToMm(settings.marginLeft)),\n right: convertMillimetersToTwip(pxToMm(settings.marginRight)),\n },\n },\n };\n}\n\nfunction blocksToDocxChildren(doc: SerializedDocument): Array<Paragraph | Table> {\n const out: Array<Paragraph | Table> = [];\n const blocks = doc.blocks;\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i];\n switch (block.type) {\n case \"paragraph\":\n out.push(makeParagraph(block));\n break;\n case \"heading\":\n out.push(makeHeading(block));\n break;\n case \"blockquote\":\n out.push(makeBlockquote(block));\n break;\n case \"codeBlock\":\n out.push(makeCodeBlock(block));\n break;\n case \"listItem\":\n out.push(makeListItem(block));\n break;\n case \"table\":\n out.push(makeTable(block));\n break;\n default:\n out.push(makeParagraph(block));\n }\n }\n return out;\n}\n\n// ---------- block builders ----------\n\n// School standard: Arial for all body text. Code blocks override with\n// Consolas (monospace is more legible for code). Italics on blockquote is\n// kept as a stylistic default.\nconst ARIAL: RunDefaults = { font: \"Arial\" };\n\nfunction makeParagraph(block: SerializedBlock): Paragraph {\n return new Paragraph({\n alignment: alignFor(block.attrs.align),\n bidirectional: block.attrs.dir === \"rtl\" ? true : undefined,\n children: deltaToRuns(block.delta, ARIAL),\n });\n}\n\nfunction makeHeading(block: SerializedBlock): Paragraph {\n const level = clampHeadingLevel(block.attrs.level);\n return new Paragraph({\n heading: HEADING_LEVELS[level - 1],\n alignment: alignFor(block.attrs.align),\n children: deltaToRuns(block.delta, ARIAL),\n });\n}\n\nfunction makeBlockquote(block: SerializedBlock): Paragraph {\n return new Paragraph({\n alignment: alignFor(block.attrs.align),\n indent: { left: 480 }, // ~0.25\"\n border: {\n left: { color: \"CBD5E1\", space: 12, style: BorderStyle.SINGLE, size: 12 },\n },\n children: deltaToRuns(block.delta, { ...ARIAL, italics: true }),\n });\n}\n\nfunction makeCodeBlock(block: SerializedBlock): Paragraph {\n return new Paragraph({\n spacing: { before: 100, after: 100 },\n shading: { type: ShadingType.CLEAR, color: \"auto\", fill: \"F1F5F9\" },\n children: deltaToRuns(block.delta, { font: \"Consolas\", size: 20 }),\n });\n}\n\nfunction makeListItem(block: SerializedBlock): Paragraph {\n const kind: ListKind = block.attrs.listKind === \"ordered\" ? \"ordered\" : \"bullet\";\n const reference = kind === \"ordered\" ? ORDERED_REF : BULLET_REF;\n const level = clampListLevel(block.attrs.listLevel);\n return new Paragraph({\n numbering: { reference, level },\n alignment: alignFor(block.attrs.align),\n children: deltaToRuns(block.delta, ARIAL),\n });\n}\n\nfunction makeTable(block: SerializedBlock): Table {\n const rows = typeof block.attrs.rows === \"number\" ? block.attrs.rows : 0;\n const cols = typeof block.attrs.cols === \"number\" ? block.attrs.cols : 0;\n const cells = block.cells ?? [];\n\n const rowsOut: TableRow[] = [];\n for (let r = 0; r < rows; r++) {\n const cellsOut: TableCell[] = [];\n for (let c = 0; c < cols; c++) {\n const cell = cells[r * cols + c];\n if (!cell) {\n cellsOut.push(emptyCell());\n continue;\n }\n if (cell.attrs?.covered) continue;\n cellsOut.push(makeCell(cell));\n }\n rowsOut.push(new TableRow({ children: cellsOut }));\n }\n\n return new Table({\n rows: rowsOut,\n width: { size: 100, type: WidthType.PERCENTAGE },\n });\n}\n\nfunction makeCell(cell: SerializedCell): TableCell {\n const span = cell.attrs?.colspan && cell.attrs.colspan > 1 ? cell.attrs.colspan : 1;\n const rowSpan = cell.attrs?.rowspan && cell.attrs.rowspan > 1 ? cell.attrs.rowspan : 1;\n return new TableCell({\n columnSpan: span,\n rowSpan,\n verticalAlign: VerticalAlign.TOP,\n children: [new Paragraph({ children: deltaToRuns(cell.delta, ARIAL) })],\n });\n}\n\nfunction emptyCell(): TableCell {\n return new TableCell({ children: [new Paragraph(\"\")] });\n}\n\n// ---------- inline ----------\n\ninterface RunDefaults {\n bold?: boolean;\n italics?: boolean;\n font?: string;\n size?: number;\n}\n\nfunction deltaToRuns(\n delta: DeltaOp[],\n defaults: RunDefaults = {},\n): Array<TextRun | ImageRun> {\n if (delta.length === 0) return [new TextRun(\"\")];\n const out: Array<TextRun | ImageRun> = [];\n for (const op of delta) {\n if (typeof op.insert === \"string\") {\n if (op.insert.length === 0) continue;\n out.push(makeTextRun(op.insert, op.attributes, defaults));\n } else if (isImageEmbed(op.insert)) {\n const embed = op.insert;\n const img = makeImageRun(embed);\n if (img) out.push(img);\n // Captions are emitted as italic-smaller text on a new line right after\n // the image. Word-style \"Caption\" paragraph would need a paragraph\n // break, which we can't do mid-delta — visually correct but loses the\n // semantic \"Caption\" style.\n if (embed.caption && embed.caption.length > 0) {\n out.push(\n new TextRun({\n text: embed.caption,\n italics: true,\n size: 18, // 9pt — typical caption size\n break: 1,\n }),\n );\n }\n }\n }\n if (out.length === 0) out.push(new TextRun(\"\"));\n return out;\n}\n\nfunction makeTextRun(\n text: string,\n marks: MarkAttrs | undefined,\n defaults: RunDefaults,\n): TextRun {\n const m = marks ?? {};\n const fontSize = parseFontSizeToHalfPoints(m.fontSize);\n // TextRun reads newlines via `break`; convert \\n into break runs.\n const segments = text.split(\"\\n\");\n if (segments.length === 1) {\n return new TextRun({\n text,\n bold: m.bold || defaults.bold,\n italics: m.italic || defaults.italics,\n underline: m.underline ? { type: UnderlineType.SINGLE } : undefined,\n strike: m.strike,\n color: cssColorToDocxHex(m.color),\n font: m.fontFamily ?? defaults.font,\n size: fontSize ?? defaults.size,\n });\n }\n // Multi-line: rejoin via TextRun children — but TextRun expects a single text\n // string. Approximate with non-printing line breaks: produce a sequence of\n // TextRuns separated by `new TextRun({ break: 1 })` packed into the parent.\n // Simpler: collapse to a single TextRun with the string, treating \\n as a\n // soft-break placeholder. Word renders \\n inside a w:t as a literal space,\n // so we replace with a small symbol-friendly fallback.\n return new TextRun({\n text: text.replace(/\\n/g, \" \"),\n bold: m.bold || defaults.bold,\n italics: m.italic || defaults.italics,\n underline: m.underline ? { type: UnderlineType.SINGLE } : undefined,\n strike: m.strike,\n color: cssColorToDocxHex(m.color),\n font: m.fontFamily ?? defaults.font,\n size: fontSize ?? defaults.size,\n });\n}\n\nfunction makeImageRun(embed: ImageEmbed): ImageRun | null {\n const decoded = decodeDataUrl(embed.src);\n if (!decoded) return null;\n return new ImageRun({\n data: decoded.bytes,\n type: decoded.kind,\n transformation: {\n width: Math.max(1, Math.round(embed.width)),\n height: Math.max(1, Math.round(embed.height)),\n },\n } as ConstructorParameters<typeof ImageRun>[0]);\n}\n\ninterface DecodedImage {\n bytes: Uint8Array;\n kind: \"png\" | \"jpg\" | \"gif\" | \"bmp\" | \"svg\";\n}\n\nfunction decodeDataUrl(src: string): DecodedImage | null {\n const m = /^data:image\\/(png|jpe?g|gif|bmp|svg\\+xml);base64,(.+)$/i.exec(src);\n if (!m) return null;\n const fmt = m[1].toLowerCase();\n const kind: DecodedImage[\"kind\"] =\n fmt === \"jpeg\" || fmt === \"jpg\"\n ? \"jpg\"\n : fmt === \"svg+xml\"\n ? \"svg\"\n : (fmt as \"png\" | \"gif\" | \"bmp\");\n const base64 = m[2];\n const bytes = base64ToBytes(base64);\n return { bytes, kind };\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n if (typeof atob === \"function\") {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n // Node fallback.\n return new Uint8Array(Buffer.from(b64, \"base64\"));\n}\n\n// ---------- attribute mapping ----------\n\nconst HEADING_LEVELS = [\n HeadingLevel.HEADING_1,\n HeadingLevel.HEADING_2,\n HeadingLevel.HEADING_3,\n HeadingLevel.HEADING_4,\n HeadingLevel.HEADING_5,\n HeadingLevel.HEADING_6,\n];\n\nfunction clampHeadingLevel(l: unknown): 1 | 2 | 3 | 4 | 5 | 6 {\n if (typeof l !== \"number\") return 1;\n const n = Math.trunc(l);\n if (n < 1) return 1;\n if (n > 6) return 6;\n return n as 1 | 2 | 3 | 4 | 5 | 6;\n}\n\nfunction clampListLevel(l: unknown): number {\n if (typeof l !== \"number\" || !Number.isFinite(l)) return 0;\n return Math.max(0, Math.min(5, Math.trunc(l)));\n}\n\nfunction alignFor(a: AlignValue | undefined): (typeof AlignmentType)[keyof typeof AlignmentType] | undefined {\n switch (a) {\n case \"left\":\n return AlignmentType.LEFT;\n case \"center\":\n return AlignmentType.CENTER;\n case \"right\":\n return AlignmentType.RIGHT;\n case \"justify\":\n return AlignmentType.JUSTIFIED;\n default:\n return undefined;\n }\n}\n\nfunction cssColorToDocxHex(color: string | undefined): string | undefined {\n if (!color) return undefined;\n // docx expects RRGGBB without leading '#'.\n const c = color.trim();\n if (/^#[0-9a-f]{6}$/i.test(c)) return c.slice(1).toUpperCase();\n if (/^#[0-9a-f]{3}$/i.test(c)) {\n // expand short hex\n return c\n .slice(1)\n .split(\"\")\n .map((d) => d + d)\n .join(\"\")\n .toUpperCase();\n }\n const rgb = /^rgb\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/i.exec(c);\n if (rgb) {\n const r = Number(rgb[1]);\n const g = Number(rgb[2]);\n const b = Number(rgb[3]);\n return [r, g, b].map((n) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, \"0\")).join(\"\").toUpperCase();\n }\n // Named colors / other forms — let docx fall back to default.\n return undefined;\n}\n\n/**\n * docx sizes are in half-points (24 = 12pt). Parse common CSS sizes.\n * Returns `undefined` when not parseable so callers can pick a default.\n */\nfunction parseFontSizeToHalfPoints(size: string | undefined): number | undefined {\n if (!size) return undefined;\n const s = size.trim();\n const m = /^(-?\\d*\\.?\\d+)\\s*(pt|px|em|rem)?$/i.exec(s);\n if (!m) return undefined;\n const n = Number(m[1]);\n if (!Number.isFinite(n) || n <= 0) return undefined;\n const unit = (m[2] ?? \"pt\").toLowerCase();\n const pt =\n unit === \"pt\"\n ? n\n : unit === \"px\"\n ? n * 0.75 // CSS px → pt at 96dpi\n : n * 12;\n return Math.round(pt * 2);\n}\n\n// ---------- list numbering definitions ----------\n\nfunction buildBulletLevels(): Array<{\n level: number;\n format: (typeof LevelFormat)[keyof typeof LevelFormat];\n text: string;\n alignment: (typeof AlignmentType)[keyof typeof AlignmentType];\n style?: { paragraph?: { indent?: { left: number; hanging: number } } };\n}> {\n const symbols = [\"•\", \"◦\", \"▪\", \"•\", \"◦\", \"▪\"];\n return symbols.map((sym, i) => ({\n level: i,\n format: LevelFormat.BULLET,\n text: sym,\n alignment: AlignmentType.LEFT,\n style: {\n paragraph: { indent: { left: 360 + i * 360, hanging: 260 } },\n },\n }));\n}\n\nfunction buildOrderedLevels(): Array<{\n level: number;\n format: (typeof LevelFormat)[keyof typeof LevelFormat];\n text: string;\n alignment: (typeof AlignmentType)[keyof typeof AlignmentType];\n style?: { paragraph?: { indent?: { left: number; hanging: number } } };\n}> {\n const formats = [\n LevelFormat.DECIMAL,\n LevelFormat.LOWER_LETTER,\n LevelFormat.LOWER_ROMAN,\n LevelFormat.DECIMAL,\n LevelFormat.LOWER_LETTER,\n LevelFormat.LOWER_ROMAN,\n ];\n return formats.map((format, i) => ({\n level: i,\n format,\n text: `%${i + 1}.`,\n alignment: AlignmentType.LEFT,\n style: {\n paragraph: { indent: { left: 360 + i * 360, hanging: 260 } },\n },\n }));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/docx.ts"],"sourcesContent":["export { documentToDocxBlob, documentToDocxBuffer } from \"./docx\";\nexport type { DocumentToDocxOptions, ResolvedImage } from \"./docx\";\n","import type {\n AlignValue,\n DeltaOp,\n ImageEmbed,\n LegacySerializedDocument,\n ListKind,\n MarkAttrs,\n SerializedBlock,\n SerializedCell,\n SerializedDocument,\n} from \"@sofereditor/core\";\nimport {\n DEFAULT_PAGE_SETTINGS,\n isImageEmbed,\n isLegacySerializedDocument,\n pxToMm,\n type PageSettings,\n} from \"@sofereditor/core\";\nimport {\n AlignmentType,\n BorderStyle,\n Document,\n HeadingLevel,\n ImageRun,\n LevelFormat,\n Packer,\n Paragraph,\n PageOrientation,\n ShadingType,\n Table,\n TableCell,\n TableLayoutType,\n TableRow,\n TextRun,\n UnderlineType,\n VerticalAlign,\n WidthType,\n convertMillimetersToTwip,\n} from \"docx\";\n\nconst BULLET_REF = \"ed-bullet\";\nconst ORDERED_REF = \"ed-ordered\";\n\nexport interface ResolvedImage {\n data: Uint8Array;\n type: \"png\" | \"jpg\" | \"gif\" | \"bmp\" | \"svg\";\n}\n\nexport interface DocumentToDocxOptions {\n /** Document title; embedded in core properties. */\n title?: string;\n /** Author for core properties. */\n creator?: string;\n /**\n * Resolve um `src` de imagem para bytes. Default: data URLs (decodificadas\n * localmente) e http(s) via fetch. Retornar null pula a imagem (contada em\n * `skippedImages`) sem abortar o export.\n */\n resolveImage?: (src: string) => Promise<ResolvedImage | null>;\n}\n\n/**\n * Build a docx `Document` from a `SerializedDocument`. Output is a Blob you can\n * download with `downloadBlob` from `@sofereditor/export-pdf`.\n *\n * Mappings:\n * - paragraph / heading / blockquote / codeBlock → Paragraph (with `heading`,\n * shading, monospace font for code)\n * - listItem → Paragraph with `numbering: { reference, level }`\n * - table → docx `Table`. `rowspan`/`colspan` map to TableCell's `rowSpan`/\n * `columnSpan`; \"covered\" cells are skipped (docx infers cover from spans)\n * - inline marks → TextRun props (bold, italics, underline, strike, color,\n * font, size in half-points)\n * - image embeds → ImageRun with bytes resolved via `resolveImage` (default:\n * data URLs decoded locally, http(s) fetched; unresolved images are\n * skipped and counted in `skippedImages` instead of aborting the export)\n */\nexport async function documentToDocxBlob(\n doc: SerializedDocument | LegacySerializedDocument,\n options: DocumentToDocxOptions = {},\n): Promise<{ blob: Blob; skippedImages: number }> {\n const normalized = normalize(doc);\n const { images, skipped } = await resolveAllImages(\n normalized,\n options.resolveImage ?? defaultResolveImage,\n );\n const built = buildDocument(normalized, options, images);\n return { blob: await Packer.toBlob(built), skippedImages: skipped };\n}\n\n/** Same as `documentToDocxBlob` but returns a Uint8Array (useful in Node/tests). */\nexport async function documentToDocxBuffer(\n doc: SerializedDocument | LegacySerializedDocument,\n options: DocumentToDocxOptions = {},\n): Promise<{ buffer: Uint8Array; skippedImages: number }> {\n const normalized = normalize(doc);\n const { images, skipped } = await resolveAllImages(\n normalized,\n options.resolveImage ?? defaultResolveImage,\n );\n const built = buildDocument(normalized, options, images);\n const buf = await Packer.toBuffer(built);\n // `Packer.toBuffer` returns a Node Buffer at runtime — coerce for consumers.\n return {\n buffer: buf instanceof Uint8Array ? buf : new Uint8Array(buf),\n skippedImages: skipped,\n };\n}\n\nfunction normalize(doc: SerializedDocument | LegacySerializedDocument): SerializedDocument {\n return isLegacySerializedDocument(doc) ? { blocks: doc } : doc;\n}\n\nfunction buildDocument(\n doc: SerializedDocument,\n options: DocumentToDocxOptions,\n images: Map<string, ResolvedImage | null>,\n): Document {\n const children = blocksToDocxChildren(doc, images);\n const settings = doc.pageSettings ?? DEFAULT_PAGE_SETTINGS;\n return new Document({\n creator: options.creator,\n title: options.title,\n numbering: {\n config: [\n {\n reference: BULLET_REF,\n levels: buildBulletLevels(),\n },\n {\n reference: ORDERED_REF,\n levels: buildOrderedLevels(),\n },\n ],\n },\n sections: [\n {\n properties: sectionPropertiesFor(settings),\n children,\n },\n ],\n });\n}\n\nfunction sectionPropertiesFor(settings: PageSettings) {\n return {\n page: {\n size: {\n width: convertMillimetersToTwip(pxToMm(settings.width)),\n height: convertMillimetersToTwip(pxToMm(settings.height)),\n orientation: PageOrientation.PORTRAIT,\n },\n margin: {\n top: convertMillimetersToTwip(pxToMm(settings.marginTop)),\n bottom: convertMillimetersToTwip(pxToMm(settings.marginBottom)),\n left: convertMillimetersToTwip(pxToMm(settings.marginLeft)),\n right: convertMillimetersToTwip(pxToMm(settings.marginRight)),\n },\n },\n };\n}\n\nfunction blocksToDocxChildren(\n doc: SerializedDocument,\n images: Map<string, ResolvedImage | null>,\n): Array<Paragraph | Table> {\n const out: Array<Paragraph | Table> = [];\n const blocks = doc.blocks;\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i];\n switch (block.type) {\n case \"paragraph\":\n out.push(makeParagraph(block, images));\n break;\n case \"heading\":\n out.push(makeHeading(block, images));\n break;\n case \"blockquote\":\n out.push(makeBlockquote(block, images));\n break;\n case \"codeBlock\":\n out.push(makeCodeBlock(block, images));\n break;\n case \"listItem\":\n out.push(makeListItem(block, images));\n break;\n case \"table\":\n out.push(makeTable(block, images));\n break;\n default:\n out.push(makeParagraph(block, images));\n }\n }\n return out;\n}\n\n// ---------- block builders ----------\n\n// School standard: Arial for all body text. Code blocks override with\n// Consolas (monospace is more legible for code). Italics on blockquote is\n// kept as a stylistic default.\nconst ARIAL: RunDefaults = { font: \"Arial\" };\n\nfunction makeParagraph(\n block: SerializedBlock,\n images: Map<string, ResolvedImage | null>,\n): Paragraph {\n return new Paragraph({\n alignment: alignFor(block.attrs.align),\n bidirectional: block.attrs.dir === \"rtl\" ? true : undefined,\n children: deltaToRuns(block.delta, ARIAL, images),\n });\n}\n\nfunction makeHeading(\n block: SerializedBlock,\n images: Map<string, ResolvedImage | null>,\n): Paragraph {\n const level = clampHeadingLevel(block.attrs.level);\n return new Paragraph({\n heading: HEADING_LEVELS[level - 1],\n alignment: alignFor(block.attrs.align),\n children: deltaToRuns(block.delta, ARIAL, images),\n });\n}\n\nfunction makeBlockquote(\n block: SerializedBlock,\n images: Map<string, ResolvedImage | null>,\n): Paragraph {\n return new Paragraph({\n alignment: alignFor(block.attrs.align),\n indent: { left: 480 }, // ~0.25\"\n border: {\n left: { color: \"CBD5E1\", space: 12, style: BorderStyle.SINGLE, size: 12 },\n },\n children: deltaToRuns(block.delta, { ...ARIAL, italics: true }, images),\n });\n}\n\nfunction makeCodeBlock(\n block: SerializedBlock,\n images: Map<string, ResolvedImage | null>,\n): Paragraph {\n return new Paragraph({\n spacing: { before: 100, after: 100 },\n shading: { type: ShadingType.CLEAR, color: \"auto\", fill: \"F1F5F9\" },\n children: deltaToRuns(block.delta, { font: \"Consolas\", size: 20 }, images),\n });\n}\n\nfunction makeListItem(\n block: SerializedBlock,\n images: Map<string, ResolvedImage | null>,\n): Paragraph {\n const kind: ListKind = block.attrs.listKind === \"ordered\" ? \"ordered\" : \"bullet\";\n const reference = kind === \"ordered\" ? ORDERED_REF : BULLET_REF;\n const level = clampListLevel(block.attrs.listLevel);\n return new Paragraph({\n numbering: { reference, level },\n alignment: alignFor(block.attrs.align),\n children: deltaToRuns(block.delta, ARIAL, images),\n });\n}\n\nfunction makeTable(block: SerializedBlock, images: Map<string, ResolvedImage | null>): Table {\n const rows = typeof block.attrs.rows === \"number\" ? block.attrs.rows : 0;\n const cols = typeof block.attrs.cols === \"number\" ? block.attrs.cols : 0;\n const cells = block.cells ?? [];\n\n const rowsOut: TableRow[] = [];\n for (let r = 0; r < rows; r++) {\n const cellsOut: TableCell[] = [];\n for (let c = 0; c < cols; c++) {\n const cell = cells[r * cols + c];\n if (!cell) {\n cellsOut.push(emptyCell());\n continue;\n }\n if (cell.attrs?.covered) continue;\n cellsOut.push(makeCell(cell, images));\n }\n rowsOut.push(new TableRow({ children: cellsOut }));\n }\n\n // Validate the RAW array against `cols` — filtering invalid entries first\n // (e.g. dropping a negative width) would silently shift the remaining\n // widths onto the wrong columns while still passing a length check.\n const rawColWidths = block.attrs.colWidths;\n const isValidColWidths =\n Array.isArray(rawColWidths) &&\n cols > 0 &&\n rawColWidths.length === cols &&\n rawColWidths.every((w): w is number => typeof w === \"number\" && w > 0);\n const columnWidths = isValidColWidths\n ? rawColWidths.map((px) => convertMillimetersToTwip(pxToMm(px)))\n : undefined;\n\n return new Table({\n rows: rowsOut,\n ...(columnWidths\n ? {\n columnWidths,\n // Fix the layout so Word honors the column widths verbatim instead\n // of autofitting to content (the OOXML default when `tblLayout` is\n // absent, which would make `columnWidths` a mere hint).\n layout: TableLayoutType.FIXED,\n width: { size: columnWidths.reduce((a, b) => a + b, 0), type: WidthType.DXA },\n }\n : { width: { size: 100, type: WidthType.PERCENTAGE } }),\n });\n}\n\nfunction makeCell(cell: SerializedCell, images: Map<string, ResolvedImage | null>): TableCell {\n const span = cell.attrs?.colspan && cell.attrs.colspan > 1 ? cell.attrs.colspan : 1;\n const rowSpan = cell.attrs?.rowspan && cell.attrs.rowspan > 1 ? cell.attrs.rowspan : 1;\n const fill = cssColorToDocxHex(cell.attrs?.bgColor);\n return new TableCell({\n columnSpan: span,\n rowSpan,\n verticalAlign: VerticalAlign.TOP,\n shading: fill ? { type: ShadingType.CLEAR, color: \"auto\", fill } : undefined,\n children: [\n new Paragraph({\n alignment: alignFor(cell.attrs?.align),\n children: deltaToRuns(cell.delta, ARIAL, images),\n }),\n ],\n });\n}\n\nfunction emptyCell(): TableCell {\n return new TableCell({ children: [new Paragraph(\"\")] });\n}\n\n// ---------- inline ----------\n\ninterface RunDefaults {\n bold?: boolean;\n italics?: boolean;\n font?: string;\n size?: number;\n}\n\nfunction deltaToRuns(\n delta: DeltaOp[],\n defaults: RunDefaults = {},\n images: Map<string, ResolvedImage | null>,\n): Array<TextRun | ImageRun> {\n if (delta.length === 0) return [new TextRun(\"\")];\n const out: Array<TextRun | ImageRun> = [];\n for (const op of delta) {\n if (typeof op.insert === \"string\") {\n if (op.insert.length === 0) continue;\n out.push(makeTextRun(op.insert, op.attributes, defaults));\n } else if (isImageEmbed(op.insert)) {\n const embed = op.insert;\n const img = makeImageRun(embed, images);\n if (img) out.push(img);\n // Captions are emitted as italic-smaller text on a new line right after\n // the image. Word-style \"Caption\" paragraph would need a paragraph\n // break, which we can't do mid-delta — visually correct but loses the\n // semantic \"Caption\" style.\n if (embed.caption && embed.caption.length > 0) {\n out.push(\n new TextRun({\n text: embed.caption,\n italics: true,\n size: 18, // 9pt — typical caption size\n break: 1,\n }),\n );\n }\n }\n }\n if (out.length === 0) out.push(new TextRun(\"\"));\n return out;\n}\n\nfunction makeTextRun(\n text: string,\n marks: MarkAttrs | undefined,\n defaults: RunDefaults,\n): TextRun {\n const m = marks ?? {};\n const fontSize = parseFontSizeToHalfPoints(m.fontSize);\n // TextRun reads newlines via `break`; convert \\n into break runs.\n const segments = text.split(\"\\n\");\n if (segments.length === 1) {\n return new TextRun({\n text,\n bold: m.bold || defaults.bold,\n italics: m.italic || defaults.italics,\n underline: m.underline ? { type: UnderlineType.SINGLE } : undefined,\n strike: m.strike,\n color: cssColorToDocxHex(m.color),\n font: m.fontFamily ?? defaults.font,\n size: fontSize ?? defaults.size,\n });\n }\n // Multi-line: rejoin via TextRun children — but TextRun expects a single text\n // string. Approximate with non-printing line breaks: produce a sequence of\n // TextRuns separated by `new TextRun({ break: 1 })` packed into the parent.\n // Simpler: collapse to a single TextRun with the string, treating \\n as a\n // soft-break placeholder. Word renders \\n inside a w:t as a literal space,\n // so we replace with a small symbol-friendly fallback.\n return new TextRun({\n text: text.replace(/\\n/g, \" \"),\n bold: m.bold || defaults.bold,\n italics: m.italic || defaults.italics,\n underline: m.underline ? { type: UnderlineType.SINGLE } : undefined,\n strike: m.strike,\n color: cssColorToDocxHex(m.color),\n font: m.fontFamily ?? defaults.font,\n size: fontSize ?? defaults.size,\n });\n}\n\nfunction makeImageRun(\n embed: ImageEmbed,\n images: Map<string, ResolvedImage | null>,\n): ImageRun | null {\n const resolved = images.get(embed.src);\n if (!resolved) return null;\n return new ImageRun({\n data: resolved.data,\n type: resolved.type,\n transformation: {\n width: Math.max(1, Math.round(embed.width)),\n height: Math.max(1, Math.round(embed.height)),\n },\n } as ConstructorParameters<typeof ImageRun>[0]);\n}\n\ninterface DecodedImage {\n bytes: Uint8Array;\n kind: \"png\" | \"jpg\" | \"gif\" | \"bmp\" | \"svg\";\n}\n\nfunction decodeDataUrl(src: string): DecodedImage | null {\n const m = /^data:image\\/(png|jpe?g|gif|bmp|svg\\+xml);base64,(.+)$/i.exec(src);\n if (!m) return null;\n const fmt = m[1].toLowerCase();\n const kind: DecodedImage[\"kind\"] =\n fmt === \"jpeg\" || fmt === \"jpg\"\n ? \"jpg\"\n : fmt === \"svg+xml\"\n ? \"svg\"\n : (fmt as \"png\" | \"gif\" | \"bmp\");\n const base64 = m[2];\n const bytes = base64ToBytes(base64);\n return { bytes, kind };\n}\n\n/**\n * Default `resolveImage`: decode data URLs locally, fetch http(s) URLs, and\n * return null for anything else (caller counts it in `skippedImages`).\n */\nasync function defaultResolveImage(src: string): Promise<ResolvedImage | null> {\n const decoded = decodeDataUrl(src);\n if (decoded) return { data: decoded.bytes, type: decoded.kind };\n if (/^https?:\\/\\//i.test(src) && typeof fetch === \"function\") {\n try {\n const res = await fetch(src);\n if (!res.ok) return null;\n const ct = (res.headers.get(\"content-type\") ?? \"\").toLowerCase();\n const type: ResolvedImage[\"type\"] = ct.includes(\"png\")\n ? \"png\"\n : ct.includes(\"jpeg\") || ct.includes(\"jpg\")\n ? \"jpg\"\n : ct.includes(\"gif\")\n ? \"gif\"\n : ct.includes(\"bmp\")\n ? \"bmp\"\n : ct.includes(\"svg\")\n ? \"svg\"\n : (typeFromExtension(src) ?? \"png\");\n return { data: new Uint8Array(await res.arrayBuffer()), type };\n } catch {\n return null;\n }\n }\n return null;\n}\n\nfunction typeFromExtension(src: string): ResolvedImage[\"type\"] | null {\n const m = /\\.(png|jpe?g|gif|bmp|svg)(\\?|#|$)/i.exec(src);\n if (!m) return null;\n const ext = m[1].toLowerCase();\n return ext === \"jpeg\" || ext === \"jpg\" ? \"jpg\" : (ext as ResolvedImage[\"type\"]);\n}\n\n/**\n * Async pre-pass: collect every image `src` referenced by the document\n * (block deltas and table cell deltas), resolve them in parallel via\n * `resolve`, and return a lookup map plus the count of embeds that failed to\n * resolve. The rest of the build pipeline stays synchronous and reads from\n * this map.\n */\nasync function resolveAllImages(\n doc: SerializedDocument,\n resolve: (src: string) => Promise<ResolvedImage | null>,\n): Promise<{ images: Map<string, ResolvedImage | null>; skipped: number }> {\n const srcs = new Set<string>();\n const collect = (delta: DeltaOp[]) => {\n for (const op of delta) if (isImageEmbed(op.insert)) srcs.add(op.insert.src);\n };\n for (const block of doc.blocks) {\n collect(block.delta);\n for (const cell of block.cells ?? []) collect(cell.delta);\n }\n const images = new Map<string, ResolvedImage | null>();\n await Promise.all(\n [...srcs].map(async (src) => {\n let resolved: ResolvedImage | null = null;\n try {\n resolved = await resolve(src);\n } catch {\n resolved = null;\n }\n // docx's ImageRun requires a `fallback` for type \"svg\" (RegularImageOptions);\n // without it the constructor throws synchronously inside buildDocument,\n // which is outside this try/catch and would abort the whole export. Treat\n // svg as unresolved (skip) until we add fallback support.\n if (resolved?.type === \"svg\") resolved = null;\n images.set(src, resolved);\n }),\n );\n // skipped conta OCORRÊNCIAS de embed sem resolução (não srcs únicos).\n let skipped = 0;\n const count = (delta: DeltaOp[]) => {\n for (const op of delta)\n if (isImageEmbed(op.insert) && !images.get(op.insert.src)) skipped++;\n };\n for (const block of doc.blocks) {\n count(block.delta);\n for (const cell of block.cells ?? []) count(cell.delta);\n }\n return { images, skipped };\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n if (typeof atob === \"function\") {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n // Node fallback.\n return new Uint8Array(Buffer.from(b64, \"base64\"));\n}\n\n// ---------- attribute mapping ----------\n\nconst HEADING_LEVELS = [\n HeadingLevel.HEADING_1,\n HeadingLevel.HEADING_2,\n HeadingLevel.HEADING_3,\n HeadingLevel.HEADING_4,\n HeadingLevel.HEADING_5,\n HeadingLevel.HEADING_6,\n];\n\nfunction clampHeadingLevel(l: unknown): 1 | 2 | 3 | 4 | 5 | 6 {\n if (typeof l !== \"number\") return 1;\n const n = Math.trunc(l);\n if (n < 1) return 1;\n if (n > 6) return 6;\n return n as 1 | 2 | 3 | 4 | 5 | 6;\n}\n\nfunction clampListLevel(l: unknown): number {\n if (typeof l !== \"number\" || !Number.isFinite(l)) return 0;\n return Math.max(0, Math.min(5, Math.trunc(l)));\n}\n\nfunction alignFor(a: AlignValue | undefined): (typeof AlignmentType)[keyof typeof AlignmentType] | undefined {\n switch (a) {\n case \"left\":\n return AlignmentType.LEFT;\n case \"center\":\n return AlignmentType.CENTER;\n case \"right\":\n return AlignmentType.RIGHT;\n case \"justify\":\n return AlignmentType.JUSTIFIED;\n default:\n return undefined;\n }\n}\n\nfunction cssColorToDocxHex(color: string | undefined): string | undefined {\n if (!color) return undefined;\n // docx expects RRGGBB without leading '#'.\n const c = color.trim();\n if (/^#[0-9a-f]{6}$/i.test(c)) return c.slice(1).toUpperCase();\n if (/^#[0-9a-f]{3}$/i.test(c)) {\n // expand short hex\n return c\n .slice(1)\n .split(\"\")\n .map((d) => d + d)\n .join(\"\")\n .toUpperCase();\n }\n const rgb = /^rgb\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/i.exec(c);\n if (rgb) {\n const r = Number(rgb[1]);\n const g = Number(rgb[2]);\n const b = Number(rgb[3]);\n return [r, g, b].map((n) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, \"0\")).join(\"\").toUpperCase();\n }\n // Named colors / other forms — let docx fall back to default.\n return undefined;\n}\n\n/**\n * docx sizes are in half-points (24 = 12pt). Parse common CSS sizes.\n * Returns `undefined` when not parseable so callers can pick a default.\n */\nfunction parseFontSizeToHalfPoints(size: string | undefined): number | undefined {\n if (!size) return undefined;\n const s = size.trim();\n const m = /^(-?\\d*\\.?\\d+)\\s*(pt|px|em|rem)?$/i.exec(s);\n if (!m) return undefined;\n const n = Number(m[1]);\n if (!Number.isFinite(n) || n <= 0) return undefined;\n const unit = (m[2] ?? \"pt\").toLowerCase();\n const pt =\n unit === \"pt\"\n ? n\n : unit === \"px\"\n ? n * 0.75 // CSS px → pt at 96dpi\n : n * 12;\n return Math.round(pt * 2);\n}\n\n// ---------- list numbering definitions ----------\n\nfunction buildBulletLevels(): Array<{\n level: number;\n format: (typeof LevelFormat)[keyof typeof LevelFormat];\n text: string;\n alignment: (typeof AlignmentType)[keyof typeof AlignmentType];\n style?: { paragraph?: { indent?: { left: number; hanging: number } } };\n}> {\n const symbols = [\"•\", \"◦\", \"▪\", \"•\", \"◦\", \"▪\"];\n return symbols.map((sym, i) => ({\n level: i,\n format: LevelFormat.BULLET,\n text: sym,\n alignment: AlignmentType.LEFT,\n style: {\n paragraph: { indent: { left: 360 + i * 360, hanging: 260 } },\n },\n }));\n}\n\nfunction buildOrderedLevels(): Array<{\n level: number;\n format: (typeof LevelFormat)[keyof typeof LevelFormat];\n text: string;\n alignment: (typeof AlignmentType)[keyof typeof AlignmentType];\n style?: { paragraph?: { indent?: { left: number; hanging: number } } };\n}> {\n const formats = [\n LevelFormat.DECIMAL,\n LevelFormat.LOWER_LETTER,\n LevelFormat.LOWER_ROMAN,\n LevelFormat.DECIMAL,\n LevelFormat.LOWER_LETTER,\n LevelFormat.LOWER_ROMAN,\n ];\n return formats.map((format, i) => ({\n level: i,\n format,\n text: `%${i + 1}.`,\n alignment: AlignmentType.LEFT,\n style: {\n paragraph: { indent: { left: 360 + i * 360, hanging: 260 } },\n },\n }));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWA,kBAMO;AACP,kBAoBO;AAEP,IAAM,aAAa;AACnB,IAAM,cAAc;AAoCpB,eAAsB,mBACpB,KACA,UAAiC,CAAC,GACc;AAChD,QAAM,aAAa,UAAU,GAAG;AAChC,QAAM,EAAE,QAAQ,QAAQ,IAAI,MAAM;AAAA,IAChC;AAAA,IACA,QAAQ,gBAAgB;AAAA,EAC1B;AACA,QAAM,QAAQ,cAAc,YAAY,SAAS,MAAM;AACvD,SAAO,EAAE,MAAM,MAAM,mBAAO,OAAO,KAAK,GAAG,eAAe,QAAQ;AACpE;AAGA,eAAsB,qBACpB,KACA,UAAiC,CAAC,GACsB;AACxD,QAAM,aAAa,UAAU,GAAG;AAChC,QAAM,EAAE,QAAQ,QAAQ,IAAI,MAAM;AAAA,IAChC;AAAA,IACA,QAAQ,gBAAgB;AAAA,EAC1B;AACA,QAAM,QAAQ,cAAc,YAAY,SAAS,MAAM;AACvD,QAAM,MAAM,MAAM,mBAAO,SAAS,KAAK;AAEvC,SAAO;AAAA,IACL,QAAQ,eAAe,aAAa,MAAM,IAAI,WAAW,GAAG;AAAA,IAC5D,eAAe;AAAA,EACjB;AACF;AAEA,SAAS,UAAU,KAAwE;AACzF,aAAO,wCAA2B,GAAG,IAAI,EAAE,QAAQ,IAAI,IAAI;AAC7D;AAEA,SAAS,cACP,KACA,SACA,QACU;AACV,QAAM,WAAW,qBAAqB,KAAK,MAAM;AACjD,QAAM,WAAW,IAAI,gBAAgB;AACrC,SAAO,IAAI,qBAAS;AAAA,IAClB,SAAS,QAAQ;AAAA,IACjB,OAAO,QAAQ;AAAA,IACf,WAAW;AAAA,MACT,QAAQ;AAAA,QACN;AAAA,UACE,WAAW;AAAA,UACX,QAAQ,kBAAkB;AAAA,QAC5B;AAAA,QACA;AAAA,UACE,WAAW;AAAA,UACX,QAAQ,mBAAmB;AAAA,QAC7B;AAAA,MACF;AAAA,IACF;AAAA,IACA,UAAU;AAAA,MACR;AAAA,QACE,YAAY,qBAAqB,QAAQ;AAAA,QACzC;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,SAAS,qBAAqB,UAAwB;AACpD,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,MAAM;AAAA,QACJ,WAAO,0CAAyB,oBAAO,SAAS,KAAK,CAAC;AAAA,QACtD,YAAQ,0CAAyB,oBAAO,SAAS,MAAM,CAAC;AAAA,QACxD,aAAa,4BAAgB;AAAA,MAC/B;AAAA,MACA,QAAQ;AAAA,QACN,SAAK,0CAAyB,oBAAO,SAAS,SAAS,CAAC;AAAA,QACxD,YAAQ,0CAAyB,oBAAO,SAAS,YAAY,CAAC;AAAA,QAC9D,UAAM,0CAAyB,oBAAO,SAAS,UAAU,CAAC;AAAA,QAC1D,WAAO,0CAAyB,oBAAO,SAAS,WAAW,CAAC;AAAA,MAC9D;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,qBACP,KACA,QAC0B;AAC1B,QAAM,MAAgC,CAAC;AACvC,QAAM,SAAS,IAAI;AACnB,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,UAAM,QAAQ,OAAO,CAAC;AACtB,YAAQ,MAAM,MAAM;AAAA,MAClB,KAAK;AACH,YAAI,KAAK,cAAc,OAAO,MAAM,CAAC;AACrC;AAAA,MACF,KAAK;AACH,YAAI,KAAK,YAAY,OAAO,MAAM,CAAC;AACnC;AAAA,MACF,KAAK;AACH,YAAI,KAAK,eAAe,OAAO,MAAM,CAAC;AACtC;AAAA,MACF,KAAK;AACH,YAAI,KAAK,cAAc,OAAO,MAAM,CAAC;AACrC;AAAA,MACF,KAAK;AACH,YAAI,KAAK,aAAa,OAAO,MAAM,CAAC;AACpC;AAAA,MACF,KAAK;AACH,YAAI,KAAK,UAAU,OAAO,MAAM,CAAC;AACjC;AAAA,MACF;AACE,YAAI,KAAK,cAAc,OAAO,MAAM,CAAC;AAAA,IACzC;AAAA,EACF;AACA,SAAO;AACT;AAOA,IAAM,QAAqB,EAAE,MAAM,QAAQ;AAE3C,SAAS,cACP,OACA,QACW;AACX,SAAO,IAAI,sBAAU;AAAA,IACnB,WAAW,SAAS,MAAM,MAAM,KAAK;AAAA,IACrC,eAAe,MAAM,MAAM,QAAQ,QAAQ,OAAO;AAAA,IAClD,UAAU,YAAY,MAAM,OAAO,OAAO,MAAM;AAAA,EAClD,CAAC;AACH;AAEA,SAAS,YACP,OACA,QACW;AACX,QAAM,QAAQ,kBAAkB,MAAM,MAAM,KAAK;AACjD,SAAO,IAAI,sBAAU;AAAA,IACnB,SAAS,eAAe,QAAQ,CAAC;AAAA,IACjC,WAAW,SAAS,MAAM,MAAM,KAAK;AAAA,IACrC,UAAU,YAAY,MAAM,OAAO,OAAO,MAAM;AAAA,EAClD,CAAC;AACH;AAEA,SAAS,eACP,OACA,QACW;AACX,SAAO,IAAI,sBAAU;AAAA,IACnB,WAAW,SAAS,MAAM,MAAM,KAAK;AAAA,IACrC,QAAQ,EAAE,MAAM,IAAI;AAAA;AAAA,IACpB,QAAQ;AAAA,MACN,MAAM,EAAE,OAAO,UAAU,OAAO,IAAI,OAAO,wBAAY,QAAQ,MAAM,GAAG;AAAA,IAC1E;AAAA,IACA,UAAU,YAAY,MAAM,OAAO,EAAE,GAAG,OAAO,SAAS,KAAK,GAAG,MAAM;AAAA,EACxE,CAAC;AACH;AAEA,SAAS,cACP,OACA,QACW;AACX,SAAO,IAAI,sBAAU;AAAA,IACnB,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI;AAAA,IACnC,SAAS,EAAE,MAAM,wBAAY,OAAO,OAAO,QAAQ,MAAM,SAAS;AAAA,IAClE,UAAU,YAAY,MAAM,OAAO,EAAE,MAAM,YAAY,MAAM,GAAG,GAAG,MAAM;AAAA,EAC3E,CAAC;AACH;AAEA,SAAS,aACP,OACA,QACW;AACX,QAAM,OAAiB,MAAM,MAAM,aAAa,YAAY,YAAY;AACxE,QAAM,YAAY,SAAS,YAAY,cAAc;AACrD,QAAM,QAAQ,eAAe,MAAM,MAAM,SAAS;AAClD,SAAO,IAAI,sBAAU;AAAA,IACnB,WAAW,EAAE,WAAW,MAAM;AAAA,IAC9B,WAAW,SAAS,MAAM,MAAM,KAAK;AAAA,IACrC,UAAU,YAAY,MAAM,OAAO,OAAO,MAAM;AAAA,EAClD,CAAC;AACH;AAEA,SAAS,UAAU,OAAwB,QAAkD;AAC3F,QAAM,OAAO,OAAO,MAAM,MAAM,SAAS,WAAW,MAAM,MAAM,OAAO;AACvE,QAAM,OAAO,OAAO,MAAM,MAAM,SAAS,WAAW,MAAM,MAAM,OAAO;AACvE,QAAM,QAAQ,MAAM,SAAS,CAAC;AAE9B,QAAM,UAAsB,CAAC;AAC7B,WAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC7B,UAAM,WAAwB,CAAC;AAC/B,aAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC7B,YAAM,OAAO,MAAM,IAAI,OAAO,CAAC;AAC/B,UAAI,CAAC,MAAM;AACT,iBAAS,KAAK,UAAU,CAAC;AACzB;AAAA,MACF;AACA,UAAI,KAAK,OAAO,QAAS;AACzB,eAAS,KAAK,SAAS,MAAM,MAAM,CAAC;AAAA,IACtC;AACA,YAAQ,KAAK,IAAI,qBAAS,EAAE,UAAU,SAAS,CAAC,CAAC;AAAA,EACnD;AAKA,QAAM,eAAe,MAAM,MAAM;AACjC,QAAM,mBACJ,MAAM,QAAQ,YAAY,KAC1B,OAAO,KACP,aAAa,WAAW,QACxB,aAAa,MAAM,CAAC,MAAmB,OAAO,MAAM,YAAY,IAAI,CAAC;AACvE,QAAM,eAAe,mBACjB,aAAa,IAAI,CAAC,WAAO,0CAAyB,oBAAO,EAAE,CAAC,CAAC,IAC7D;AAEJ,SAAO,IAAI,kBAAM;AAAA,IACf,MAAM;AAAA,IACN,GAAI,eACA;AAAA,MACE;AAAA;AAAA;AAAA;AAAA,MAIA,QAAQ,4BAAgB;AAAA,MACxB,OAAO,EAAE,MAAM,aAAa,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,GAAG,MAAM,sBAAU,IAAI;AAAA,IAC9E,IACA,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,sBAAU,WAAW,EAAE;AAAA,EACzD,CAAC;AACH;AAEA,SAAS,SAAS,MAAsB,QAAsD;AAC5F,QAAM,OAAO,KAAK,OAAO,WAAW,KAAK,MAAM,UAAU,IAAI,KAAK,MAAM,UAAU;AAClF,QAAM,UAAU,KAAK,OAAO,WAAW,KAAK,MAAM,UAAU,IAAI,KAAK,MAAM,UAAU;AACrF,QAAM,OAAO,kBAAkB,KAAK,OAAO,OAAO;AAClD,SAAO,IAAI,sBAAU;AAAA,IACnB,YAAY;AAAA,IACZ;AAAA,IACA,eAAe,0BAAc;AAAA,IAC7B,SAAS,OAAO,EAAE,MAAM,wBAAY,OAAO,OAAO,QAAQ,KAAK,IAAI;AAAA,IACnE,UAAU;AAAA,MACR,IAAI,sBAAU;AAAA,QACZ,WAAW,SAAS,KAAK,OAAO,KAAK;AAAA,QACrC,UAAU,YAAY,KAAK,OAAO,OAAO,MAAM;AAAA,MACjD,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAEA,SAAS,YAAuB;AAC9B,SAAO,IAAI,sBAAU,EAAE,UAAU,CAAC,IAAI,sBAAU,EAAE,CAAC,EAAE,CAAC;AACxD;AAWA,SAAS,YACP,OACA,WAAwB,CAAC,GACzB,QAC2B;AAC3B,MAAI,MAAM,WAAW,EAAG,QAAO,CAAC,IAAI,oBAAQ,EAAE,CAAC;AAC/C,QAAM,MAAiC,CAAC;AACxC,aAAW,MAAM,OAAO;AACtB,QAAI,OAAO,GAAG,WAAW,UAAU;AACjC,UAAI,GAAG,OAAO,WAAW,EAAG;AAC5B,UAAI,KAAK,YAAY,GAAG,QAAQ,GAAG,YAAY,QAAQ,CAAC;AAAA,IAC1D,eAAW,0BAAa,GAAG,MAAM,GAAG;AAClC,YAAM,QAAQ,GAAG;AACjB,YAAM,MAAM,aAAa,OAAO,MAAM;AACtC,UAAI,IAAK,KAAI,KAAK,GAAG;AAKrB,UAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG;AAC7C,YAAI;AAAA,UACF,IAAI,oBAAQ;AAAA,YACV,MAAM,MAAM;AAAA,YACZ,SAAS;AAAA,YACT,MAAM;AAAA;AAAA,YACN,OAAO;AAAA,UACT,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,MAAI,IAAI,WAAW,EAAG,KAAI,KAAK,IAAI,oBAAQ,EAAE,CAAC;AAC9C,SAAO;AACT;AAEA,SAAS,YACP,MACA,OACA,UACS;AACT,QAAM,IAAI,SAAS,CAAC;AACpB,QAAM,WAAW,0BAA0B,EAAE,QAAQ;AAErD,QAAM,WAAW,KAAK,MAAM,IAAI;AAChC,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO,IAAI,oBAAQ;AAAA,MACjB;AAAA,MACA,MAAM,EAAE,QAAQ,SAAS;AAAA,MACzB,SAAS,EAAE,UAAU,SAAS;AAAA,MAC9B,WAAW,EAAE,YAAY,EAAE,MAAM,0BAAc,OAAO,IAAI;AAAA,MAC1D,QAAQ,EAAE;AAAA,MACV,OAAO,kBAAkB,EAAE,KAAK;AAAA,MAChC,MAAM,EAAE,cAAc,SAAS;AAAA,MAC/B,MAAM,YAAY,SAAS;AAAA,IAC7B,CAAC;AAAA,EACH;AAOA,SAAO,IAAI,oBAAQ;AAAA,IACjB,MAAM,KAAK,QAAQ,OAAO,GAAG;AAAA,IAC7B,MAAM,EAAE,QAAQ,SAAS;AAAA,IACzB,SAAS,EAAE,UAAU,SAAS;AAAA,IAC9B,WAAW,EAAE,YAAY,EAAE,MAAM,0BAAc,OAAO,IAAI;AAAA,IAC1D,QAAQ,EAAE;AAAA,IACV,OAAO,kBAAkB,EAAE,KAAK;AAAA,IAChC,MAAM,EAAE,cAAc,SAAS;AAAA,IAC/B,MAAM,YAAY,SAAS;AAAA,EAC7B,CAAC;AACH;AAEA,SAAS,aACP,OACA,QACiB;AACjB,QAAM,WAAW,OAAO,IAAI,MAAM,GAAG;AACrC,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,IAAI,qBAAS;AAAA,IAClB,MAAM,SAAS;AAAA,IACf,MAAM,SAAS;AAAA,IACf,gBAAgB;AAAA,MACd,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,KAAK,CAAC;AAAA,MAC1C,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,MAAM,CAAC;AAAA,IAC9C;AAAA,EACF,CAA8C;AAChD;AAOA,SAAS,cAAc,KAAkC;AACvD,QAAM,IAAI,0DAA0D,KAAK,GAAG;AAC5E,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,MAAM,EAAE,CAAC,EAAE,YAAY;AAC7B,QAAM,OACJ,QAAQ,UAAU,QAAQ,QACtB,QACA,QAAQ,YACN,QACC;AACT,QAAM,SAAS,EAAE,CAAC;AAClB,QAAM,QAAQ,cAAc,MAAM;AAClC,SAAO,EAAE,OAAO,KAAK;AACvB;AAMA,eAAe,oBAAoB,KAA4C;AAC7E,QAAM,UAAU,cAAc,GAAG;AACjC,MAAI,QAAS,QAAO,EAAE,MAAM,QAAQ,OAAO,MAAM,QAAQ,KAAK;AAC9D,MAAI,gBAAgB,KAAK,GAAG,KAAK,OAAO,UAAU,YAAY;AAC5D,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,UAAI,CAAC,IAAI,GAAI,QAAO;AACpB,YAAM,MAAM,IAAI,QAAQ,IAAI,cAAc,KAAK,IAAI,YAAY;AAC/D,YAAM,OAA8B,GAAG,SAAS,KAAK,IACjD,QACA,GAAG,SAAS,MAAM,KAAK,GAAG,SAAS,KAAK,IACtC,QACA,GAAG,SAAS,KAAK,IACf,QACA,GAAG,SAAS,KAAK,IACf,QACA,GAAG,SAAS,KAAK,IACf,QACC,kBAAkB,GAAG,KAAK;AACvC,aAAO,EAAE,MAAM,IAAI,WAAW,MAAM,IAAI,YAAY,CAAC,GAAG,KAAK;AAAA,IAC/D,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,KAA2C;AACpE,QAAM,IAAI,qCAAqC,KAAK,GAAG;AACvD,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,MAAM,EAAE,CAAC,EAAE,YAAY;AAC7B,SAAO,QAAQ,UAAU,QAAQ,QAAQ,QAAS;AACpD;AASA,eAAe,iBACb,KACA,SACyE;AACzE,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,UAAU,CAAC,UAAqB;AACpC,eAAW,MAAM,MAAO,SAAI,0BAAa,GAAG,MAAM,EAAG,MAAK,IAAI,GAAG,OAAO,GAAG;AAAA,EAC7E;AACA,aAAW,SAAS,IAAI,QAAQ;AAC9B,YAAQ,MAAM,KAAK;AACnB,eAAW,QAAQ,MAAM,SAAS,CAAC,EAAG,SAAQ,KAAK,KAAK;AAAA,EAC1D;AACA,QAAM,SAAS,oBAAI,IAAkC;AACrD,QAAM,QAAQ;AAAA,IACZ,CAAC,GAAG,IAAI,EAAE,IAAI,OAAO,QAAQ;AAC3B,UAAI,WAAiC;AACrC,UAAI;AACF,mBAAW,MAAM,QAAQ,GAAG;AAAA,MAC9B,QAAQ;AACN,mBAAW;AAAA,MACb;AAKA,UAAI,UAAU,SAAS,MAAO,YAAW;AACzC,aAAO,IAAI,KAAK,QAAQ;AAAA,IAC1B,CAAC;AAAA,EACH;AAEA,MAAI,UAAU;AACd,QAAM,QAAQ,CAAC,UAAqB;AAClC,eAAW,MAAM;AACf,cAAI,0BAAa,GAAG,MAAM,KAAK,CAAC,OAAO,IAAI,GAAG,OAAO,GAAG,EAAG;AAAA,EAC/D;AACA,aAAW,SAAS,IAAI,QAAQ;AAC9B,UAAM,MAAM,KAAK;AACjB,eAAW,QAAQ,MAAM,SAAS,CAAC,EAAG,OAAM,KAAK,KAAK;AAAA,EACxD;AACA,SAAO,EAAE,QAAQ,QAAQ;AAC3B;AAEA,SAAS,cAAc,KAAyB;AAC9C,MAAI,OAAO,SAAS,YAAY;AAC9B,UAAM,MAAM,KAAK,GAAG;AACpB,UAAM,MAAM,IAAI,WAAW,IAAI,MAAM;AACrC,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,KAAI,CAAC,IAAI,IAAI,WAAW,CAAC;AAC9D,WAAO;AAAA,EACT;AAEA,SAAO,IAAI,WAAW,OAAO,KAAK,KAAK,QAAQ,CAAC;AAClD;AAIA,IAAM,iBAAiB;AAAA,EACrB,yBAAa;AAAA,EACb,yBAAa;AAAA,EACb,yBAAa;AAAA,EACb,yBAAa;AAAA,EACb,yBAAa;AAAA,EACb,yBAAa;AACf;AAEA,SAAS,kBAAkB,GAAmC;AAC5D,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,QAAM,IAAI,KAAK,MAAM,CAAC;AACtB,MAAI,IAAI,EAAG,QAAO;AAClB,MAAI,IAAI,EAAG,QAAO;AAClB,SAAO;AACT;AAEA,SAAS,eAAe,GAAoB;AAC1C,MAAI,OAAO,MAAM,YAAY,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AACzD,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC;AAC/C;AAEA,SAAS,SAAS,GAA2F;AAC3G,UAAQ,GAAG;AAAA,IACT,KAAK;AACH,aAAO,0BAAc;AAAA,IACvB,KAAK;AACH,aAAO,0BAAc;AAAA,IACvB,KAAK;AACH,aAAO,0BAAc;AAAA,IACvB,KAAK;AACH,aAAO,0BAAc;AAAA,IACvB;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,kBAAkB,OAA+C;AACxE,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,IAAI,MAAM,KAAK;AACrB,MAAI,kBAAkB,KAAK,CAAC,EAAG,QAAO,EAAE,MAAM,CAAC,EAAE,YAAY;AAC7D,MAAI,kBAAkB,KAAK,CAAC,GAAG;AAE7B,WAAO,EACJ,MAAM,CAAC,EACP,MAAM,EAAE,EACR,IAAI,CAAC,MAAM,IAAI,CAAC,EAChB,KAAK,EAAE,EACP,YAAY;AAAA,EACjB;AACA,QAAM,MAAM,0CAA0C,KAAK,CAAC;AAC5D,MAAI,KAAK;AACP,UAAM,IAAI,OAAO,IAAI,CAAC,CAAC;AACvB,UAAM,IAAI,OAAO,IAAI,CAAC,CAAC;AACvB,UAAM,IAAI,OAAO,IAAI,CAAC,CAAC;AACvB,WAAO,CAAC,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,YAAY;AAAA,EAChH;AAEA,SAAO;AACT;AAMA,SAAS,0BAA0B,MAA8C;AAC/E,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,IAAI,KAAK,KAAK;AACpB,QAAM,IAAI,qCAAqC,KAAK,CAAC;AACrD,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,IAAI,OAAO,EAAE,CAAC,CAAC;AACrB,MAAI,CAAC,OAAO,SAAS,CAAC,KAAK,KAAK,EAAG,QAAO;AAC1C,QAAM,QAAQ,EAAE,CAAC,KAAK,MAAM,YAAY;AACxC,QAAM,KACJ,SAAS,OACL,IACA,SAAS,OACP,IAAI,OACJ,IAAI;AACZ,SAAO,KAAK,MAAM,KAAK,CAAC;AAC1B;AAIA,SAAS,oBAMN;AACD,QAAM,UAAU,CAAC,UAAK,UAAK,UAAK,UAAK,UAAK,QAAG;AAC7C,SAAO,QAAQ,IAAI,CAAC,KAAK,OAAO;AAAA,IAC9B,OAAO;AAAA,IACP,QAAQ,wBAAY;AAAA,IACpB,MAAM;AAAA,IACN,WAAW,0BAAc;AAAA,IACzB,OAAO;AAAA,MACL,WAAW,EAAE,QAAQ,EAAE,MAAM,MAAM,IAAI,KAAK,SAAS,IAAI,EAAE;AAAA,IAC7D;AAAA,EACF,EAAE;AACJ;AAEA,SAAS,qBAMN;AACD,QAAM,UAAU;AAAA,IACd,wBAAY;AAAA,IACZ,wBAAY;AAAA,IACZ,wBAAY;AAAA,IACZ,wBAAY;AAAA,IACZ,wBAAY;AAAA,IACZ,wBAAY;AAAA,EACd;AACA,SAAO,QAAQ,IAAI,CAAC,QAAQ,OAAO;AAAA,IACjC,OAAO;AAAA,IACP;AAAA,IACA,MAAM,IAAI,IAAI,CAAC;AAAA,IACf,WAAW,0BAAc;AAAA,IACzB,OAAO;AAAA,MACL,WAAW,EAAE,QAAQ,EAAE,MAAM,MAAM,IAAI,KAAK,SAAS,IAAI,EAAE;AAAA,IAC7D;AAAA,EACF,EAAE;AACJ;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,10 +1,20 @@
1
1
  import { SerializedDocument, LegacySerializedDocument } from '@sofereditor/core';
2
2
 
3
+ interface ResolvedImage {
4
+ data: Uint8Array;
5
+ type: "png" | "jpg" | "gif" | "bmp" | "svg";
6
+ }
3
7
  interface DocumentToDocxOptions {
4
8
  /** Document title; embedded in core properties. */
5
9
  title?: string;
6
10
  /** Author for core properties. */
7
11
  creator?: string;
12
+ /**
13
+ * Resolve um `src` de imagem para bytes. Default: data URLs (decodificadas
14
+ * localmente) e http(s) via fetch. Retornar null pula a imagem (contada em
15
+ * `skippedImages`) sem abortar o export.
16
+ */
17
+ resolveImage?: (src: string) => Promise<ResolvedImage | null>;
8
18
  }
9
19
  /**
10
20
  * Build a docx `Document` from a `SerializedDocument`. Output is a Blob you can
@@ -18,10 +28,18 @@ interface DocumentToDocxOptions {
18
28
  * `columnSpan`; "covered" cells are skipped (docx infers cover from spans)
19
29
  * - inline marks → TextRun props (bold, italics, underline, strike, color,
20
30
  * font, size in half-points)
21
- * - image embeds → ImageRun with the base64 payload decoded to Uint8Array
31
+ * - image embeds → ImageRun with bytes resolved via `resolveImage` (default:
32
+ * data URLs decoded locally, http(s) fetched; unresolved images are
33
+ * skipped and counted in `skippedImages` instead of aborting the export)
22
34
  */
23
- declare function documentToDocxBlob(doc: SerializedDocument | LegacySerializedDocument, options?: DocumentToDocxOptions): Promise<Blob>;
35
+ declare function documentToDocxBlob(doc: SerializedDocument | LegacySerializedDocument, options?: DocumentToDocxOptions): Promise<{
36
+ blob: Blob;
37
+ skippedImages: number;
38
+ }>;
24
39
  /** Same as `documentToDocxBlob` but returns a Uint8Array (useful in Node/tests). */
25
- declare function documentToDocxBuffer(doc: SerializedDocument | LegacySerializedDocument, options?: DocumentToDocxOptions): Promise<Uint8Array>;
40
+ declare function documentToDocxBuffer(doc: SerializedDocument | LegacySerializedDocument, options?: DocumentToDocxOptions): Promise<{
41
+ buffer: Uint8Array;
42
+ skippedImages: number;
43
+ }>;
26
44
 
27
- export { type DocumentToDocxOptions, documentToDocxBlob, documentToDocxBuffer };
45
+ export { type DocumentToDocxOptions, type ResolvedImage, documentToDocxBlob, documentToDocxBuffer };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,20 @@
1
1
  import { SerializedDocument, LegacySerializedDocument } from '@sofereditor/core';
2
2
 
3
+ interface ResolvedImage {
4
+ data: Uint8Array;
5
+ type: "png" | "jpg" | "gif" | "bmp" | "svg";
6
+ }
3
7
  interface DocumentToDocxOptions {
4
8
  /** Document title; embedded in core properties. */
5
9
  title?: string;
6
10
  /** Author for core properties. */
7
11
  creator?: string;
12
+ /**
13
+ * Resolve um `src` de imagem para bytes. Default: data URLs (decodificadas
14
+ * localmente) e http(s) via fetch. Retornar null pula a imagem (contada em
15
+ * `skippedImages`) sem abortar o export.
16
+ */
17
+ resolveImage?: (src: string) => Promise<ResolvedImage | null>;
8
18
  }
9
19
  /**
10
20
  * Build a docx `Document` from a `SerializedDocument`. Output is a Blob you can
@@ -18,10 +28,18 @@ interface DocumentToDocxOptions {
18
28
  * `columnSpan`; "covered" cells are skipped (docx infers cover from spans)
19
29
  * - inline marks → TextRun props (bold, italics, underline, strike, color,
20
30
  * font, size in half-points)
21
- * - image embeds → ImageRun with the base64 payload decoded to Uint8Array
31
+ * - image embeds → ImageRun with bytes resolved via `resolveImage` (default:
32
+ * data URLs decoded locally, http(s) fetched; unresolved images are
33
+ * skipped and counted in `skippedImages` instead of aborting the export)
22
34
  */
23
- declare function documentToDocxBlob(doc: SerializedDocument | LegacySerializedDocument, options?: DocumentToDocxOptions): Promise<Blob>;
35
+ declare function documentToDocxBlob(doc: SerializedDocument | LegacySerializedDocument, options?: DocumentToDocxOptions): Promise<{
36
+ blob: Blob;
37
+ skippedImages: number;
38
+ }>;
24
39
  /** Same as `documentToDocxBlob` but returns a Uint8Array (useful in Node/tests). */
25
- declare function documentToDocxBuffer(doc: SerializedDocument | LegacySerializedDocument, options?: DocumentToDocxOptions): Promise<Uint8Array>;
40
+ declare function documentToDocxBuffer(doc: SerializedDocument | LegacySerializedDocument, options?: DocumentToDocxOptions): Promise<{
41
+ buffer: Uint8Array;
42
+ skippedImages: number;
43
+ }>;
26
44
 
27
- export { type DocumentToDocxOptions, documentToDocxBlob, documentToDocxBuffer };
45
+ export { type DocumentToDocxOptions, type ResolvedImage, documentToDocxBlob, documentToDocxBuffer };