@pixerate/editor 0.25.0 → 0.26.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.
Files changed (112) hide show
  1. package/README.md +30 -0
  2. package/THIRD_PARTY_NOTICES.md +11 -0
  3. package/dist/agent/index.cjs +174 -11
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +18 -4
  6. package/dist/agent/index.d.ts +18 -4
  7. package/dist/agent/index.js +167 -4
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/{arrangeShortcuts-B8O3f8-P.d.cts → arrange-BICutYPk.d.cts} +243 -29
  10. package/dist/{arrangeShortcuts-sLlD2rSX.d.ts → arrange-BICutYPk.d.ts} +243 -29
  11. package/dist/builtins-CdPcLwwF.d.cts +176 -0
  12. package/dist/builtins-Chj6zv_O.d.ts +176 -0
  13. package/dist/canvas/index.cjs +3917 -19
  14. package/dist/canvas/index.cjs.map +1 -1
  15. package/dist/canvas/index.d.cts +1232 -7
  16. package/dist/canvas/index.d.ts +1232 -7
  17. package/dist/canvas/index.js +3916 -18
  18. package/dist/canvas/index.js.map +1 -1
  19. package/dist/chunk-3CC7OPUQ.cjs +831 -0
  20. package/dist/chunk-3CC7OPUQ.cjs.map +1 -0
  21. package/dist/{chunk-AL6JMGWW.cjs → chunk-CS6NZWWA.cjs} +24 -24
  22. package/dist/{chunk-AL6JMGWW.cjs.map → chunk-CS6NZWWA.cjs.map} +1 -1
  23. package/dist/{chunk-C7P22VIP.cjs → chunk-EWCOFSCQ.cjs} +17 -6
  24. package/dist/chunk-EWCOFSCQ.cjs.map +1 -0
  25. package/dist/chunk-HDYMLS3X.cjs +409 -0
  26. package/dist/chunk-HDYMLS3X.cjs.map +1 -0
  27. package/dist/{chunk-X4N4WUKB.cjs → chunk-IFHKKVXH.cjs} +150 -38
  28. package/dist/chunk-IFHKKVXH.cjs.map +1 -0
  29. package/dist/{chunk-ECCCYKGL.js → chunk-JOIREKL7.js} +63 -39
  30. package/dist/chunk-JOIREKL7.js.map +1 -0
  31. package/dist/chunk-KRGXUQ2Y.js +1696 -0
  32. package/dist/chunk-KRGXUQ2Y.js.map +1 -0
  33. package/dist/{chunk-QGYDWPRM.js → chunk-MHEPLBFB.js} +2 -2
  34. package/dist/chunk-MR2764X2.js +831 -0
  35. package/dist/chunk-MR2764X2.js.map +1 -0
  36. package/dist/{chunk-7WISYZFO.cjs → chunk-NFVBT3UZ.cjs} +67 -43
  37. package/dist/chunk-NFVBT3UZ.cjs.map +1 -0
  38. package/dist/{chunk-WSQKHXW7.cjs → chunk-NQF32I6Z.cjs} +1209 -457
  39. package/dist/chunk-NQF32I6Z.cjs.map +1 -0
  40. package/dist/{chunk-IXHSYOVY.js → chunk-Q2OHWD2A.js} +638 -866
  41. package/dist/chunk-Q2OHWD2A.js.map +1 -0
  42. package/dist/{chunk-NAXVVS5I.js → chunk-QJSOZFCZ.js} +2 -2
  43. package/dist/{chunk-V3CHHWOE.cjs → chunk-RRREAOVM.cjs} +4 -4
  44. package/dist/{chunk-V3CHHWOE.cjs.map → chunk-RRREAOVM.cjs.map} +1 -1
  45. package/dist/{chunk-UAKDG62Z.js → chunk-SWE3NPOU.js} +116 -4
  46. package/dist/chunk-SWE3NPOU.js.map +1 -0
  47. package/dist/chunk-TXXNBDQX.cjs +1696 -0
  48. package/dist/chunk-TXXNBDQX.cjs.map +1 -0
  49. package/dist/chunk-UBCFIABU.js +409 -0
  50. package/dist/chunk-UBCFIABU.js.map +1 -0
  51. package/dist/{chunk-RR7F5GUM.js → chunk-X7HXL2SB.js} +941 -189
  52. package/dist/chunk-X7HXL2SB.js.map +1 -0
  53. package/dist/{chunk-PO6A6QQH.cjs → chunk-YOMYSFJU.cjs} +742 -970
  54. package/dist/chunk-YOMYSFJU.cjs.map +1 -0
  55. package/dist/{chunk-FH6F67DN.js → chunk-ZRNHGAQT.js} +17 -6
  56. package/dist/{chunk-FH6F67DN.js.map → chunk-ZRNHGAQT.js.map} +1 -1
  57. package/dist/commands/index.cjs +352 -4
  58. package/dist/commands/index.cjs.map +1 -1
  59. package/dist/commands/index.d.cts +37 -108
  60. package/dist/commands/index.d.ts +37 -108
  61. package/dist/commands/index.js +351 -3
  62. package/dist/commands/index.js.map +1 -1
  63. package/dist/compare/index.cjs +3 -3
  64. package/dist/compare/index.js +2 -2
  65. package/dist/{controller-BIJiNoVs.d.cts → controller-CxXGOEmy.d.cts} +209 -13
  66. package/dist/{controller-BIJiNoVs.d.ts → controller-CxXGOEmy.d.ts} +209 -13
  67. package/dist/export/index.cjs +36 -0
  68. package/dist/export/index.cjs.map +1 -0
  69. package/dist/export/index.d.cts +243 -0
  70. package/dist/export/index.d.ts +243 -0
  71. package/dist/export/index.js +36 -0
  72. package/dist/export/index.js.map +1 -0
  73. package/dist/extensions/index.cjs +54 -6
  74. package/dist/extensions/index.cjs.map +1 -1
  75. package/dist/extensions/index.d.cts +7 -2
  76. package/dist/extensions/index.d.ts +7 -2
  77. package/dist/extensions/index.js +59 -11
  78. package/dist/{index-7a6bHTLy.d.ts → index-B273Zllk.d.ts} +211 -207
  79. package/dist/{index-Csd5qI8u.d.cts → index-jcJwmBYt.d.cts} +211 -207
  80. package/dist/index.cjs +144 -32
  81. package/dist/index.cjs.map +1 -1
  82. package/dist/index.d.cts +49 -13
  83. package/dist/index.d.ts +49 -13
  84. package/dist/index.js +136 -24
  85. package/dist/index.js.map +1 -1
  86. package/dist/registry-CED5C0lA.d.cts +204 -0
  87. package/dist/registry-CED5C0lA.d.ts +204 -0
  88. package/dist/{registry-D32LpIc4.d.cts → registry-D0GADKc7.d.cts} +1 -1
  89. package/dist/{registry-evKTP4ls.d.ts → registry-DhVUZioj.d.ts} +1 -1
  90. package/dist/serializers/index.cjs +4 -4
  91. package/dist/serializers/index.d.cts +2 -1
  92. package/dist/serializers/index.d.ts +2 -1
  93. package/dist/serializers/index.js +3 -3
  94. package/dist/spreadsheet/index.cjs +33 -3
  95. package/dist/spreadsheet/index.cjs.map +1 -1
  96. package/dist/spreadsheet/index.d.cts +106 -3
  97. package/dist/spreadsheet/index.d.ts +106 -3
  98. package/dist/spreadsheet/index.js +32 -2
  99. package/package.json +10 -1
  100. package/dist/chunk-7WISYZFO.cjs.map +0 -1
  101. package/dist/chunk-C7P22VIP.cjs.map +0 -1
  102. package/dist/chunk-ECCCYKGL.js.map +0 -1
  103. package/dist/chunk-IXHSYOVY.js.map +0 -1
  104. package/dist/chunk-PO6A6QQH.cjs.map +0 -1
  105. package/dist/chunk-RR7F5GUM.js.map +0 -1
  106. package/dist/chunk-UAKDG62Z.js.map +0 -1
  107. package/dist/chunk-WSQKHXW7.cjs.map +0 -1
  108. package/dist/chunk-X4N4WUKB.cjs.map +0 -1
  109. package/dist/types-DX4Mh4SL.d.cts +0 -217
  110. package/dist/types-DX4Mh4SL.d.ts +0 -217
  111. /package/dist/{chunk-QGYDWPRM.js.map → chunk-MHEPLBFB.js.map} +0 -0
  112. /package/dist/{chunk-NAXVVS5I.js.map → chunk-QJSOZFCZ.js.map} +0 -0
@@ -0,0 +1,1696 @@
1
+ import {
2
+ DEFAULT_STYLES,
3
+ NORMAL_STYLE_ID,
4
+ isValidStyleId,
5
+ zipCrc32
6
+ } from "./chunk-MR2764X2.js";
7
+
8
+ // src/export/images.ts
9
+ var DOCX_IMAGE_TYPES = {
10
+ "image/png": "png",
11
+ "image/jpeg": "jpeg",
12
+ "image/jpg": "jpeg",
13
+ "image/gif": "gif",
14
+ "image/bmp": "bmp",
15
+ "image/webp": "webp",
16
+ "image/tiff": "tiff"
17
+ };
18
+ var DOCX_IMAGE_CONTENT_TYPES = {
19
+ png: "image/png",
20
+ jpeg: "image/jpeg",
21
+ gif: "image/gif",
22
+ bmp: "image/bmp",
23
+ webp: "image/webp",
24
+ tiff: "image/tiff"
25
+ };
26
+ var MAX_DATA_URL = 64 * 1024 * 1024;
27
+ function decodeDataUrl(src) {
28
+ if (src.length > MAX_DATA_URL) return null;
29
+ const comma = src.indexOf(",");
30
+ if (comma === -1) return null;
31
+ const header = /^data:([a-z0-9.+-]+\/[a-z0-9.+-]+)((?:;[a-z0-9-]+=[^;,]*)*);base64$/i.exec(src.slice(0, comma));
32
+ if (!header) return null;
33
+ const payload = src.slice(comma + 1).replace(/\s+/g, "");
34
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(payload)) return null;
35
+ try {
36
+ const binary = atob(payload);
37
+ const bytes = new Uint8Array(binary.length);
38
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
39
+ return { mime: header[1].toLowerCase(), bytes };
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ var u16be = (b, i) => b[i] << 8 | b[i + 1];
45
+ var u16le = (b, i) => b[i] | b[i + 1] << 8;
46
+ var u32be = (b, i) => (b[i] << 24 | b[i + 1] << 16 | b[i + 2] << 8 | b[i + 3]) >>> 0;
47
+ var i32le = (b, i) => b[i] | b[i + 1] << 8 | b[i + 2] << 16 | b[i + 3] << 24;
48
+ function imagePixelSize(b) {
49
+ const ok = (width, height) => width > 0 && height > 0 && width < 1e6 && height < 1e6 ? { width, height } : null;
50
+ if (b.length >= 24 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) {
51
+ return ok(u32be(b, 16), u32be(b, 20));
52
+ }
53
+ if (b.length >= 10 && b[0] === 71 && b[1] === 73 && b[2] === 70) return ok(u16le(b, 6), u16le(b, 8));
54
+ if (b.length >= 26 && b[0] === 66 && b[1] === 77) return ok(Math.abs(i32le(b, 18)), Math.abs(i32le(b, 22)));
55
+ if (b.length >= 30 && b[0] === 82 && b[1] === 73 && b[8] === 87 && b[9] === 69) {
56
+ const chunk = String.fromCharCode(b[12], b[13], b[14], b[15]);
57
+ if (chunk === "VP8 ") return ok(u16le(b, 26) & 16383, u16le(b, 28) & 16383);
58
+ if (chunk === "VP8L") {
59
+ const bits = b[21] | b[22] << 8 | b[23] << 16 | b[24] << 24;
60
+ return ok((bits & 16383) + 1, (bits >>> 14 & 16383) + 1);
61
+ }
62
+ if (chunk === "VP8X") return ok((b[24] | b[25] << 8 | b[26] << 16) + 1, (b[27] | b[28] << 8 | b[29] << 16) + 1);
63
+ return null;
64
+ }
65
+ if (b.length >= 4 && b[0] === 255 && b[1] === 216) {
66
+ let i = 2;
67
+ while (i + 9 < b.length) {
68
+ if (b[i] !== 255) return null;
69
+ const marker = b[i + 1];
70
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
71
+ i += 2;
72
+ continue;
73
+ }
74
+ const length = u16be(b, i + 2);
75
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
76
+ return ok(u16be(b, i + 7), u16be(b, i + 5));
77
+ }
78
+ if (length < 2) return null;
79
+ i += 2 + length;
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+ function pixelAttr(value) {
85
+ if (typeof value === "number") return Number.isFinite(value) && value > 0 ? value : void 0;
86
+ if (typeof value !== "string") return void 0;
87
+ const match = /^\s*(\d+(?:\.\d+)?)\s*(px)?\s*$/i.exec(value);
88
+ return match ? Number(match[1]) || void 0 : void 0;
89
+ }
90
+
91
+ // src/export/units.ts
92
+ var DOCX_BASE_FONT_PT = 11;
93
+ var MAX_LEN_PT = 1584;
94
+ var UNIT_PT = {
95
+ pt: 1,
96
+ px: 0.75,
97
+ in: 72,
98
+ cm: 72 / 2.54,
99
+ mm: 72 / 25.4,
100
+ pc: 12
101
+ };
102
+ function cssLengthToPt(value, basePt = DOCX_BASE_FONT_PT) {
103
+ if (typeof value === "number") return Number.isFinite(value) ? clampPt(value * UNIT_PT.px) : void 0;
104
+ if (typeof value !== "string") return void 0;
105
+ const match = /^\s*(-?\d+(?:\.\d+)?|-?\.\d+)\s*([a-z%]*)\s*$/i.exec(value);
106
+ if (!match) return void 0;
107
+ const n = Number(match[1]);
108
+ const unit = match[2].toLowerCase() || "px";
109
+ let pt;
110
+ if (unit === "em" || unit === "rem") pt = n * basePt;
111
+ else if (unit === "%") pt = n / 100 * basePt;
112
+ else if (unit in UNIT_PT) pt = n * UNIT_PT[unit];
113
+ return pt === void 0 || !Number.isFinite(pt) ? void 0 : clampPt(pt);
114
+ }
115
+ function clampPt(pt) {
116
+ return Math.max(-MAX_LEN_PT, Math.min(MAX_LEN_PT, pt));
117
+ }
118
+ var ptToTwips = (pt) => Math.round(pt * 20);
119
+ var ptToHalfPoints = (pt) => Math.max(1, Math.min(3276, Math.round(pt * 2)));
120
+ var pxToEmu = (px) => Math.round(px * 9525);
121
+ var pxToTwips = (px) => Math.round(px * 15);
122
+ var NAMED_COLORS = {
123
+ black: "000000",
124
+ white: "FFFFFF",
125
+ red: "FF0000",
126
+ green: "008000",
127
+ lime: "00FF00",
128
+ blue: "0000FF",
129
+ yellow: "FFFF00",
130
+ cyan: "00FFFF",
131
+ aqua: "00FFFF",
132
+ magenta: "FF00FF",
133
+ fuchsia: "FF00FF",
134
+ gray: "808080",
135
+ grey: "808080",
136
+ silver: "C0C0C0",
137
+ maroon: "800000",
138
+ navy: "000080",
139
+ olive: "808000",
140
+ purple: "800080",
141
+ teal: "008080",
142
+ orange: "FFA500",
143
+ pink: "FFC0CB"
144
+ };
145
+ function cssColorToHex(value) {
146
+ if (typeof value !== "string") return void 0;
147
+ const color = value.trim().toLowerCase();
148
+ if (color in NAMED_COLORS) return NAMED_COLORS[color];
149
+ let match = /^#([0-9a-f]{3,4})$/.exec(color);
150
+ if (match) return match[1].slice(0, 3).split("").map((c) => c + c).join("").toUpperCase();
151
+ match = /^#([0-9a-f]{6})(?:[0-9a-f]{2})?$/.exec(color);
152
+ if (match) return match[1].toUpperCase();
153
+ match = /^rgba?\(\s*(\d{1,3})\s*[, ]\s*(\d{1,3})\s*[, ]\s*(\d{1,3})\s*(?:[,/]\s*[\d.]+%?\s*)?\)$/.exec(color);
154
+ if (match) {
155
+ return match.slice(1, 4).map((n) => Math.min(255, Number(n)).toString(16).padStart(2, "0")).join("").toUpperCase();
156
+ }
157
+ return void 0;
158
+ }
159
+ var WORD_HIGHLIGHTS = {
160
+ FFFF00: "yellow",
161
+ "00FF00": "green",
162
+ "00FFFF": "cyan",
163
+ FF00FF: "magenta",
164
+ "0000FF": "blue",
165
+ FF0000: "red",
166
+ "000080": "darkBlue",
167
+ "008080": "darkCyan",
168
+ "008000": "darkGreen",
169
+ "800080": "darkMagenta",
170
+ "800000": "darkRed",
171
+ "808000": "darkYellow",
172
+ "808080": "darkGray",
173
+ C0C0C0: "lightGray",
174
+ "000000": "black",
175
+ FFFFFF: "white"
176
+ };
177
+
178
+ // src/export/xml.ts
179
+ var INVALID_XML_CHARS = /[^\t\n\r -퟿-�\u{10000}-\u{10FFFF}]/gu;
180
+ function stripInvalidXmlChars(value) {
181
+ return value.replace(INVALID_XML_CHARS, "");
182
+ }
183
+ function escapeXmlText(value) {
184
+ return stripInvalidXmlChars(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
185
+ }
186
+ function escapeXmlAttr(value) {
187
+ return escapeXmlText(value).replace(/"/g, "&quot;").replace(/'/g, "&apos;").replace(/\t/g, "&#9;").replace(/\n/g, "&#10;").replace(/\r/g, "&#13;");
188
+ }
189
+ function attrs(values) {
190
+ if (!values) return "";
191
+ let out = "";
192
+ for (const [name, value] of Object.entries(values)) {
193
+ if (value === void 0 || value === null || value === false) continue;
194
+ out += ` ${name}="${escapeXmlAttr(String(value))}"`;
195
+ }
196
+ return out;
197
+ }
198
+ var XmlWriter = class {
199
+ parts;
200
+ /** `declaration: false` writes a fragment (e.g. one paragraph). */
201
+ constructor(declaration = true) {
202
+ this.parts = declaration ? ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'] : [];
203
+ }
204
+ open(name, values) {
205
+ this.parts.push(`<${name}${attrs(values)}>`);
206
+ return this;
207
+ }
208
+ empty(name, values) {
209
+ this.parts.push(`<${name}${attrs(values)}/>`);
210
+ return this;
211
+ }
212
+ close(name) {
213
+ this.parts.push(`</${name}>`);
214
+ return this;
215
+ }
216
+ text(value) {
217
+ this.parts.push(escapeXmlText(value));
218
+ return this;
219
+ }
220
+ /** `<name attrs>text</name>`. */
221
+ leaf(name, values, text) {
222
+ return this.open(name, values).text(text).close(name);
223
+ }
224
+ /** `<name w:val="value"/>`. */
225
+ val(name, value) {
226
+ return this.empty(name, { "w:val": value });
227
+ }
228
+ /** Appends markup produced by another writer. */
229
+ raw(markup) {
230
+ this.parts.push(markup);
231
+ return this;
232
+ }
233
+ toString() {
234
+ return this.parts.join("");
235
+ }
236
+ };
237
+
238
+ // src/export/zip-writer.ts
239
+ var encoder = new TextEncoder();
240
+ var MAX_SIZE = 4294967295;
241
+ async function readAll(readable) {
242
+ const reader = readable.getReader();
243
+ const chunks = [];
244
+ let length = 0;
245
+ for (; ; ) {
246
+ const { done, value } = await reader.read();
247
+ if (done) break;
248
+ chunks.push(value);
249
+ length += value.length;
250
+ }
251
+ const out = new Uint8Array(length);
252
+ let pos = 0;
253
+ for (const chunk of chunks) {
254
+ out.set(chunk, pos);
255
+ pos += chunk.length;
256
+ }
257
+ return out;
258
+ }
259
+ async function deflateRaw(data) {
260
+ const Stream = globalThis.CompressionStream;
261
+ if (typeof Stream !== "function") return null;
262
+ let stream;
263
+ try {
264
+ stream = new Stream("deflate-raw");
265
+ } catch {
266
+ return null;
267
+ }
268
+ const writer = stream.writable.getWriter();
269
+ const written = writer.write(data).then(() => writer.close()).catch(() => {
270
+ });
271
+ const out = await readAll(stream.readable);
272
+ await written;
273
+ return out;
274
+ }
275
+ function dosDateTime(date) {
276
+ const year = Math.min(Math.max(date.getUTCFullYear(), 1980), 2107);
277
+ return {
278
+ time: date.getUTCHours() << 11 | date.getUTCMinutes() << 5 | Math.floor(date.getUTCSeconds() / 2),
279
+ date: year - 1980 << 9 | date.getUTCMonth() + 1 << 5 | date.getUTCDate()
280
+ };
281
+ }
282
+ var ByteSink = class {
283
+ chunks = [];
284
+ length = 0;
285
+ bytes(data) {
286
+ this.chunks.push(data);
287
+ this.length += data.length;
288
+ }
289
+ header(fields) {
290
+ const size = fields.reduce((n, [, width]) => n + width, 0);
291
+ const buffer = new Uint8Array(size);
292
+ const view = new DataView(buffer.buffer);
293
+ let pos = 0;
294
+ for (const [value, width] of fields) {
295
+ if (width === 2) view.setUint16(pos, value, true);
296
+ else view.setUint32(pos, value >>> 0, true);
297
+ pos += width;
298
+ }
299
+ this.bytes(buffer);
300
+ }
301
+ toUint8Array() {
302
+ const out = new Uint8Array(this.length);
303
+ let pos = 0;
304
+ for (const chunk of this.chunks) {
305
+ out.set(chunk, pos);
306
+ pos += chunk.length;
307
+ }
308
+ return out;
309
+ }
310
+ };
311
+ async function writeZip(entries, options = {}) {
312
+ if (entries.length > 65535) throw new RangeError("Too many ZIP entries (no ZIP64 support).");
313
+ const { time, date } = dosDateTime(options.modified ?? new Date(Date.UTC(1980, 0, 1)));
314
+ const out = new ByteSink();
315
+ const central = new ByteSink();
316
+ const seen = /* @__PURE__ */ new Set();
317
+ for (const entry of entries) {
318
+ const name = entry.name;
319
+ if (!name || name.startsWith("/") || name.includes("\\") || name.split("/").includes("..") || name.includes("\0")) {
320
+ throw new Error(`Unsafe ZIP entry name: ${JSON.stringify(name)}`);
321
+ }
322
+ if (seen.has(name)) throw new Error(`Duplicate ZIP entry name: ${JSON.stringify(name)}`);
323
+ seen.add(name);
324
+ const raw = typeof entry.data === "string" ? encoder.encode(entry.data) : entry.data;
325
+ const deflated = options.compression === "store" || raw.length === 0 ? null : await deflateRaw(raw);
326
+ const useDeflate = !!deflated && deflated.length < raw.length;
327
+ const body = useDeflate ? deflated : raw;
328
+ const method = useDeflate ? 8 : 0;
329
+ const nameBytes = encoder.encode(name);
330
+ const crc = zipCrc32(raw);
331
+ const offset = out.length;
332
+ if (body.length > MAX_SIZE || raw.length > MAX_SIZE || offset > MAX_SIZE) {
333
+ throw new RangeError("ZIP archive too large (no ZIP64 support).");
334
+ }
335
+ const flags = 2048;
336
+ out.header([
337
+ [67324752, 4],
338
+ [20, 2],
339
+ [flags, 2],
340
+ [method, 2],
341
+ [time, 2],
342
+ [date, 2],
343
+ [crc, 4],
344
+ [body.length, 4],
345
+ [raw.length, 4],
346
+ [nameBytes.length, 2],
347
+ [0, 2]
348
+ ]);
349
+ out.bytes(nameBytes);
350
+ out.bytes(body);
351
+ central.header([
352
+ [33639248, 4],
353
+ [20, 2],
354
+ [20, 2],
355
+ [flags, 2],
356
+ [method, 2],
357
+ [time, 2],
358
+ [date, 2],
359
+ [crc, 4],
360
+ [body.length, 4],
361
+ [raw.length, 4],
362
+ [nameBytes.length, 2],
363
+ [0, 2],
364
+ [0, 2],
365
+ [0, 2],
366
+ [0, 2],
367
+ [0, 4],
368
+ [offset, 4]
369
+ ]);
370
+ central.bytes(nameBytes);
371
+ }
372
+ const cdOffset = out.length;
373
+ const cd = central.toUint8Array();
374
+ out.bytes(cd);
375
+ out.header([
376
+ [101010256, 4],
377
+ [0, 2],
378
+ [0, 2],
379
+ [entries.length, 2],
380
+ [entries.length, 2],
381
+ [cd.length, 4],
382
+ [cdOffset, 4],
383
+ [0, 2]
384
+ ]);
385
+ return out.toUint8Array();
386
+ }
387
+
388
+ // src/export/docx.ts
389
+ var DOCX_EXPORT_MIME_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
390
+ var NS = {
391
+ w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
392
+ r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
393
+ wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
394
+ a: "http://schemas.openxmlformats.org/drawingml/2006/main",
395
+ pic: "http://schemas.openxmlformats.org/drawingml/2006/picture",
396
+ mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
397
+ w14: "http://schemas.microsoft.com/office/word/2010/wordml",
398
+ w15: "http://schemas.microsoft.com/office/word/2012/wordml"
399
+ };
400
+ var BODY_NS = {
401
+ "xmlns:w": NS.w,
402
+ "xmlns:r": NS.r,
403
+ "xmlns:wp": NS.wp,
404
+ "xmlns:a": NS.a,
405
+ "xmlns:pic": NS.pic,
406
+ "xmlns:mc": NS.mc,
407
+ "xmlns:w14": NS.w14,
408
+ "xmlns:w15": NS.w15,
409
+ "mc:Ignorable": "w14 w15"
410
+ };
411
+ var REL = {
412
+ officeDocument: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
413
+ core: "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
414
+ app: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
415
+ styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
416
+ numbering: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
417
+ settings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings",
418
+ comments: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments",
419
+ commentsExtended: "http://schemas.microsoft.com/office/2011/relationships/commentsExtended",
420
+ people: "http://schemas.microsoft.com/office/2011/relationships/people",
421
+ image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
422
+ hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
423
+ };
424
+ var CT_WML = "application/vnd.openxmlformats-officedocument.wordprocessingml.";
425
+ var CONTENT_WIDTH_TWIPS = 9360;
426
+ var MAX_IMAGE_WIDTH_PX = 624;
427
+ var INDENT_STEP = 720;
428
+ var DOCX_ANONYMOUS_AUTHOR = "Author";
429
+ var ANONYMOUS_INITIALS = "A";
430
+ var MAX_DEPTH = 64;
431
+ var BULLETS = ["\u2022", "\u25E6", "\u25AA"];
432
+ var BULLET_FONTS = ["Symbol", "Courier New", "Wingdings"];
433
+ var TASK_UNCHECKED = "\u2610";
434
+ var TASK_CHECKED = "\u2612";
435
+ var FORMAT_MARKS = /* @__PURE__ */ new Set([
436
+ "bold",
437
+ "italic",
438
+ "underline",
439
+ "strike",
440
+ "subscript",
441
+ "superscript",
442
+ "code",
443
+ "textStyle",
444
+ "highlight",
445
+ "characterStyle"
446
+ ]);
447
+ var STRUCTURAL_MARKS = /* @__PURE__ */ new Set(["link", "insertion", "deletion", "formatChange", "comment", "bookmark"]);
448
+ var isEditorLike = (value) => !!value && typeof value.getJSON === "function";
449
+ function threadsOf(source) {
450
+ if (!source) return [];
451
+ if (Array.isArray(source)) return [...source];
452
+ const value = source;
453
+ if (typeof value.toJSON === "function") return [...value.toJSON()?.threads ?? []];
454
+ if (typeof value.getSnapshot === "function") return [...value.getSnapshot()?.threads ?? []];
455
+ return Array.isArray(value.threads) ? [...value.threads] : [];
456
+ }
457
+ function stylesOf(source) {
458
+ if (!source) return { styles: DEFAULT_STYLES.map((s) => ({ ...s })), defaults: {} };
459
+ if (Array.isArray(source)) return { styles: [...source], defaults: {} };
460
+ const value = source;
461
+ const json = typeof value.toJSON === "function" ? value.toJSON() : value;
462
+ return { styles: [...json.styles ?? []], defaults: json.defaults ?? {} };
463
+ }
464
+ function authorOf(author) {
465
+ if (!author) return void 0;
466
+ return typeof author === "string" ? { id: author, name: author } : author;
467
+ }
468
+ function isoDate(ms) {
469
+ const n = typeof ms === "number" ? ms : Number(ms);
470
+ if (!Number.isFinite(n) || n <= 0) return void 0;
471
+ const date = new Date(n);
472
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString().replace(/\.\d{3}Z$/, "Z");
473
+ }
474
+ function initialsOf(name) {
475
+ const letters = name.split(/\s+/).filter(Boolean).map((word) => Array.from(word)[0] ?? "").join("").slice(0, 3).toUpperCase();
476
+ return letters || ANONYMOUS_INITIALS;
477
+ }
478
+ function fileStyleName(name) {
479
+ const heading = /^Heading ([1-9])$/.exec(name);
480
+ return heading ? `heading ${heading[1]}` : name;
481
+ }
482
+ var truncate = (value, max = 80) => value.length > max ? `${value.slice(0, max - 1)}\u2026` : value;
483
+ function runFormatOf(marks) {
484
+ const format = {};
485
+ for (const mark of marks ?? []) {
486
+ const attrs2 = mark.attrs ?? {};
487
+ switch (mark.type) {
488
+ case "bold":
489
+ format.bold = true;
490
+ break;
491
+ case "italic":
492
+ format.italic = true;
493
+ break;
494
+ case "underline":
495
+ format.underline = true;
496
+ break;
497
+ case "strike":
498
+ format.strike = true;
499
+ break;
500
+ case "subscript":
501
+ format.vertAlign = "subscript";
502
+ break;
503
+ case "superscript":
504
+ format.vertAlign = "superscript";
505
+ break;
506
+ case "code":
507
+ format.font = "Consolas";
508
+ break;
509
+ case "textStyle": {
510
+ const color = cssColorToHex(attrs2.color);
511
+ if (color) format.color = color;
512
+ const pt = cssLengthToPt(attrs2.fontSize);
513
+ if (pt !== void 0 && pt > 0) format.halfPoints = ptToHalfPoints(pt);
514
+ if (typeof attrs2.fontFamily === "string" && attrs2.fontFamily.trim()) {
515
+ format.font = attrs2.fontFamily.split(",")[0].trim().replace(/^["']|["']$/g, "");
516
+ }
517
+ break;
518
+ }
519
+ case "highlight": {
520
+ const color = attrs2.color ? cssColorToHex(attrs2.color) : "FFFF00";
521
+ if (color && WORD_HIGHLIGHTS[color]) format.highlight = WORD_HIGHLIGHTS[color];
522
+ else if (color) format.shading = color;
523
+ break;
524
+ }
525
+ case "characterStyle":
526
+ if (isValidStyleId(attrs2.styleId)) format.charStyle = attrs2.styleId;
527
+ break;
528
+ case "link":
529
+ format.charStyle ??= "Hyperlink";
530
+ break;
531
+ default:
532
+ break;
533
+ }
534
+ }
535
+ return format;
536
+ }
537
+ function writeRunProps(w, f) {
538
+ if (f.charStyle) w.val("w:rStyle", f.charStyle);
539
+ if (f.font) w.empty("w:rFonts", { "w:ascii": f.font, "w:hAnsi": f.font, "w:cs": f.font, "w:eastAsia": f.font });
540
+ if (f.bold) w.empty("w:b");
541
+ if (f.italic) w.empty("w:i");
542
+ if (f.strike) w.empty("w:strike");
543
+ if (f.color) w.val("w:color", f.color);
544
+ if (f.halfPoints) {
545
+ w.val("w:sz", f.halfPoints);
546
+ w.val("w:szCs", f.halfPoints);
547
+ }
548
+ if (f.highlight) w.val("w:highlight", f.highlight);
549
+ if (f.underline) w.val("w:u", "single");
550
+ if (f.shading) w.empty("w:shd", { "w:val": "clear", "w:color": "auto", "w:fill": f.shading });
551
+ if (f.vertAlign) w.val("w:vertAlign", f.vertAlign);
552
+ }
553
+ var hasRunProps = (f) => Object.values(f).some((v) => v !== void 0 && v !== false);
554
+ function characterFormatToRun(c) {
555
+ const f = {};
556
+ if (!c) return f;
557
+ if (c.fontFamily) f.font = c.fontFamily.split(",")[0].trim().replace(/^["']|["']$/g, "");
558
+ if (c.bold) f.bold = true;
559
+ if (c.italic) f.italic = true;
560
+ if (c.underline) f.underline = true;
561
+ if (c.strike) f.strike = true;
562
+ const color = cssColorToHex(c.color);
563
+ if (color) f.color = color;
564
+ const pt = cssLengthToPt(c.fontSize);
565
+ if (pt !== void 0 && pt > 0) f.halfPoints = ptToHalfPoints(pt);
566
+ const background = cssColorToHex(c.backgroundColor);
567
+ if (background) f.shading = background;
568
+ return f;
569
+ }
570
+ function writeParagraphFormat(w, p, outline) {
571
+ if (!p && outline === void 0) return;
572
+ const before = cssLengthToPt(p?.spaceBefore);
573
+ const after = cssLengthToPt(p?.spaceAfter);
574
+ const lineValue = p?.lineHeight;
575
+ let line;
576
+ let lineRule;
577
+ if (typeof lineValue === "string" && /^\s*\d+(\.\d+)?\s*$/.test(lineValue)) {
578
+ line = Math.round(Number(lineValue) * 240);
579
+ lineRule = "auto";
580
+ } else if (lineValue) {
581
+ const pt = cssLengthToPt(lineValue);
582
+ if (pt !== void 0 && pt > 0) {
583
+ line = ptToTwips(pt);
584
+ lineRule = "atLeast";
585
+ }
586
+ }
587
+ if (before !== void 0 || after !== void 0 || line !== void 0) {
588
+ w.empty("w:spacing", {
589
+ "w:before": before !== void 0 ? Math.max(0, ptToTwips(before)) : void 0,
590
+ "w:after": after !== void 0 ? Math.max(0, ptToTwips(after)) : void 0,
591
+ "w:line": line,
592
+ "w:lineRule": lineRule
593
+ });
594
+ }
595
+ const left = cssLengthToPt(p?.indentLeft);
596
+ const right = cssLengthToPt(p?.indentRight);
597
+ const first = cssLengthToPt(p?.indentFirstLine);
598
+ if (left !== void 0 || right !== void 0 || first !== void 0) {
599
+ w.empty("w:ind", {
600
+ "w:left": left !== void 0 ? ptToTwips(left) : void 0,
601
+ "w:right": right !== void 0 ? ptToTwips(right) : void 0,
602
+ "w:firstLine": first !== void 0 && first >= 0 ? ptToTwips(first) : void 0,
603
+ "w:hanging": first !== void 0 && first < 0 ? ptToTwips(-first) : void 0
604
+ });
605
+ }
606
+ const jc = alignment(p?.textAlign);
607
+ if (jc) w.val("w:jc", jc);
608
+ if (outline !== void 0) w.val("w:outlineLvl", outline);
609
+ }
610
+ function alignment(value) {
611
+ switch (value) {
612
+ case "left":
613
+ return "left";
614
+ case "center":
615
+ return "center";
616
+ case "right":
617
+ return "right";
618
+ case "justify":
619
+ return "both";
620
+ default:
621
+ return void 0;
622
+ }
623
+ }
624
+ var Relationships = class {
625
+ list = [];
626
+ index = /* @__PURE__ */ new Map();
627
+ add(type, target, external = false) {
628
+ const key = `${type}
629
+ ${target}
630
+ ${external}`;
631
+ const existing = this.index.get(key);
632
+ if (existing) return existing;
633
+ const id = `rId${this.list.length + 1}`;
634
+ this.list.push({ id, type, target, external });
635
+ this.index.set(key, id);
636
+ return id;
637
+ }
638
+ get size() {
639
+ return this.list.length;
640
+ }
641
+ xml() {
642
+ const w = new XmlWriter();
643
+ w.open("Relationships", { xmlns: "http://schemas.openxmlformats.org/package/2006/relationships" });
644
+ for (const rel of this.list) {
645
+ w.empty("Relationship", {
646
+ Id: rel.id,
647
+ Type: rel.type,
648
+ Target: rel.target,
649
+ TargetMode: rel.external ? "External" : void 0
650
+ });
651
+ }
652
+ w.close("Relationships");
653
+ return w.toString();
654
+ }
655
+ };
656
+ var DocxBuilder = class {
657
+ constructor(doc, options, threads, styles) {
658
+ this.doc = doc;
659
+ this.options = options;
660
+ this.threads = threads;
661
+ this.styles = styles;
662
+ this.anonymous = options.privacy === "strip-authors";
663
+ this.defaultAuthor = authorOf(options.author);
664
+ this.styleIds = new Set(styles.styles.filter((s) => isValidStyleId(s.id)).map((s) => s.id));
665
+ }
666
+ doc;
667
+ options;
668
+ threads;
669
+ styles;
670
+ report = {
671
+ dropped: [],
672
+ written: { paragraphs: 0, tables: 0, images: 0, comments: 0, replies: 0, trackedChanges: 0 }
673
+ };
674
+ rels = new Relationships();
675
+ media = [];
676
+ mediaTypes = /* @__PURE__ */ new Set();
677
+ nums = [];
678
+ revisionId = 0;
679
+ docPrId = 0;
680
+ paraIdCounter = 268435456;
681
+ comments = /* @__PURE__ */ new Map();
682
+ /** Thread id → JSON node carrying its last comment mark. */
683
+ commentEnds = /* @__PURE__ */ new Map();
684
+ /** JSON node → threads whose range ends after it (so ends are found in O(1) per node). */
685
+ endsByNode = /* @__PURE__ */ new Map();
686
+ people = /* @__PURE__ */ new Map();
687
+ styleIds;
688
+ extraStyles = /* @__PURE__ */ new Set();
689
+ anonymous;
690
+ defaultAuthor;
691
+ drop(kind, name, reason) {
692
+ const issue = this.report.dropped.find((i) => i.kind === kind && i.name === name && i.reason === reason);
693
+ if (issue) issue.count++;
694
+ else this.report.dropped.push({ kind, name: truncate(name), reason, count: 1 });
695
+ }
696
+ // -- Authors and revisions ------------------------------------------------
697
+ authorName(name, id) {
698
+ if (this.anonymous) return DOCX_ANONYMOUS_AUTHOR;
699
+ const author = typeof name === "string" && name.trim() || typeof id === "string" && id.trim() || this.defaultAuthor?.name || "Unknown";
700
+ const authorId = typeof id === "string" && id.trim() || this.defaultAuthor?.id;
701
+ if (authorId && authorId !== author && !this.people.has(author)) this.people.set(author, authorId);
702
+ return author;
703
+ }
704
+ changeAttrs(attrs2) {
705
+ return {
706
+ "w:id": ++this.revisionId,
707
+ "w:author": this.authorName(attrs2.authorName, attrs2.authorId),
708
+ "w:date": isoDate(attrs2.createdAt)
709
+ };
710
+ }
711
+ // -- Comments ---------------------------------------------------------------
712
+ prepareComments() {
713
+ const marked = /* @__PURE__ */ new Set();
714
+ const visit = (node, depth) => {
715
+ if (depth > MAX_DEPTH) return;
716
+ for (const mark of node.marks ?? []) {
717
+ if (mark.type === "comment" && typeof mark.attrs?.commentId === "string") {
718
+ marked.add(mark.attrs.commentId);
719
+ this.commentEnds.set(mark.attrs.commentId, node);
720
+ }
721
+ }
722
+ node.content?.forEach((child) => visit(child, depth + 1));
723
+ };
724
+ visit(this.doc, 0);
725
+ let next = 0;
726
+ for (const thread of this.threads) {
727
+ if (!thread || typeof thread.id !== "string") continue;
728
+ if (!marked.has(thread.id)) {
729
+ this.drop("comment", thread.id, "comment text is not in the document");
730
+ continue;
731
+ }
732
+ if (!thread.body?.trim()) {
733
+ this.drop("comment", thread.id, "draft comment without text");
734
+ this.commentEnds.delete(thread.id);
735
+ continue;
736
+ }
737
+ const replies = (thread.replies ?? []).filter((reply) => reply && reply.body?.trim());
738
+ const ids = [next++, ...replies.map(() => next++)];
739
+ this.comments.set(thread.id, { thread: { ...thread, replies }, ids, started: false, ended: false });
740
+ }
741
+ for (const [threadId, node] of this.commentEnds) {
742
+ if (!this.comments.has(threadId)) continue;
743
+ const list = this.endsByNode.get(node) ?? [];
744
+ list.push(threadId);
745
+ this.endsByNode.set(node, list);
746
+ }
747
+ }
748
+ // -- Document ---------------------------------------------------------------
749
+ documentXml() {
750
+ this.prepareComments();
751
+ const body = new XmlWriter(false);
752
+ const blocks = this.doc.type === "doc" || !this.doc.type ? this.doc.content ?? [] : [this.doc];
753
+ this.blocks(body, blocks, { depth: 0, quoteLevel: 0 });
754
+ if (!blocks.length || blocks[blocks.length - 1].type === "table") body.empty("w:p");
755
+ for (const [id, entry] of this.comments) if (entry.started && !entry.ended) this.endComment(body, id);
756
+ const w = new XmlWriter();
757
+ w.open("w:document", BODY_NS).open("w:body").raw(body.toString());
758
+ w.open("w:sectPr").empty("w:pgSz", { "w:w": 12240, "w:h": 15840 }).empty("w:pgMar", {
759
+ "w:top": 1440,
760
+ "w:right": 1440,
761
+ "w:bottom": 1440,
762
+ "w:left": 1440,
763
+ "w:header": 720,
764
+ "w:footer": 720,
765
+ "w:gutter": 0
766
+ }).close("w:sectPr");
767
+ w.close("w:body").close("w:document");
768
+ return w.toString();
769
+ }
770
+ blocks(w, nodes, ctx) {
771
+ if (ctx.depth > MAX_DEPTH) {
772
+ this.drop("node", "nesting", `content nested deeper than ${MAX_DEPTH} levels`);
773
+ return;
774
+ }
775
+ let first = true;
776
+ nodes.forEach((node, i) => {
777
+ const itemCtx = { ...ctx, depth: ctx.depth + 1, listItemFirst: ctx.list ? first : void 0 };
778
+ this.block(w, node, itemCtx);
779
+ if (node.type !== "bulletList" && node.type !== "orderedList" && node.type !== "taskList") first = false;
780
+ if (node.type === "table" && nodes[i + 1]?.type === "table") w.empty("w:p");
781
+ });
782
+ }
783
+ block(w, node, ctx) {
784
+ const type = node.type ?? "";
785
+ switch (type) {
786
+ case "paragraph":
787
+ this.paragraph(w, node, ctx);
788
+ return;
789
+ case "heading":
790
+ this.paragraph(w, node, ctx, { heading: Math.max(1, Math.min(6, Number(node.attrs?.level) || 1)) });
791
+ return;
792
+ case "codeBlock":
793
+ this.paragraph(w, node, ctx, { code: true });
794
+ return;
795
+ case "blockquote":
796
+ this.blocks(w, node.content ?? [], { ...ctx, quoteLevel: ctx.quoteLevel + 1 });
797
+ return;
798
+ case "bulletList":
799
+ case "orderedList":
800
+ case "taskList":
801
+ this.list(w, node, ctx);
802
+ return;
803
+ case "table":
804
+ this.table(w, node, ctx);
805
+ return;
806
+ case "horizontalRule":
807
+ w.open("w:p").open("w:pPr").open("w:pBdr");
808
+ w.empty("w:bottom", { "w:val": "single", "w:sz": 6, "w:space": 1, "w:color": "auto" });
809
+ w.close("w:pBdr").close("w:pPr").close("w:p");
810
+ this.report.written.paragraphs++;
811
+ return;
812
+ case "pageBreak":
813
+ w.open("w:p").open("w:r").empty("w:br", { "w:type": "page" }).close("w:r").close("w:p");
814
+ return;
815
+ case "image":
816
+ this.paragraph(w, { type: "paragraph", content: [node] }, ctx);
817
+ return;
818
+ default:
819
+ break;
820
+ }
821
+ const content = node.content ?? [];
822
+ if (content.some((child) => child.type === "text" || isInlineType(child.type))) {
823
+ this.drop("node", type || "unknown", "written as a plain paragraph");
824
+ this.paragraph(w, { type: "paragraph", content }, ctx);
825
+ } else if (content.length) {
826
+ this.drop("node", type || "unknown", "container flattened");
827
+ this.blocks(w, content, ctx);
828
+ } else {
829
+ this.drop("node", type || "unknown", "unsupported node");
830
+ }
831
+ }
832
+ // -- Lists --------------------------------------------------------------------
833
+ list(w, node, ctx) {
834
+ const level = Math.min(8, ctx.list ? ctx.list.level + 1 : 0);
835
+ const abstractId = node.type === "orderedList" ? 2 : node.type === "taskList" ? 3 : 1;
836
+ const numId = this.nums.length + 1;
837
+ const start = node.type === "orderedList" ? Number(node.attrs?.start ?? 1) : void 0;
838
+ this.nums.push({ numId, abstractId, level, start: start !== void 0 && start !== 1 && Number.isFinite(start) ? start : void 0 });
839
+ for (const item of node.content ?? []) {
840
+ const task = item.type === "taskItem";
841
+ this.blocks(w, item.content ?? [], {
842
+ ...ctx,
843
+ list: { numId, level, task, checked: task ? !!item.attrs?.checked : void 0 }
844
+ });
845
+ }
846
+ }
847
+ // -- Paragraphs -----------------------------------------------------------------
848
+ styleFor(node, ctx, opts) {
849
+ const own = node.attrs?.styleId;
850
+ if (own) {
851
+ if (isValidStyleId(own) && this.styleIds.has(own)) return own;
852
+ this.drop("style", String(own), "style is not in the registry");
853
+ }
854
+ if (opts.heading) return this.needStyle(`Heading${opts.heading}`);
855
+ if (opts.code) return this.needStyle("Code");
856
+ if (ctx.quoteLevel > 0) return this.needStyle("BlockText");
857
+ if (ctx.list) return this.needStyle("ListParagraph");
858
+ return void 0;
859
+ }
860
+ /** Uses a style the registry may not define (it is then written with defaults). */
861
+ needStyle(id) {
862
+ if (!this.styleIds.has(id)) this.extraStyles.add(id);
863
+ return id;
864
+ }
865
+ previousStyle(previous) {
866
+ if (!previous) return void 0;
867
+ const id = previous.attrs?.styleId;
868
+ if (isValidStyleId(id) && this.styleIds.has(id)) return id;
869
+ if (previous.type === "heading") return this.needStyle(`Heading${Math.max(1, Math.min(6, Number(previous.attrs?.level) || 1))}`);
870
+ if (previous.type === "codeBlock") return this.needStyle("Code");
871
+ return void 0;
872
+ }
873
+ paragraph(w, node, ctx, opts = {}) {
874
+ this.report.written.paragraphs++;
875
+ const style = this.styleFor(node, ctx, opts);
876
+ const change = node.attrs?.blockChange ?? null;
877
+ const operation = change?.operation;
878
+ const list = ctx.list;
879
+ const numbered = list && ctx.listItemFirst;
880
+ const indent = ctx.quoteLevel * INDENT_STEP + (list && !numbered ? (list.level + 1) * INDENT_STEP : 0);
881
+ const jc = alignment(node.attrs?.textAlign);
882
+ w.open("w:p");
883
+ const ppr = new XmlWriter(false);
884
+ if (style) ppr.val("w:pStyle", style);
885
+ if (numbered) {
886
+ ppr.open("w:numPr").val("w:ilvl", list.level).val("w:numId", list.numId).close("w:numPr");
887
+ }
888
+ if (indent && !(ctx.quoteLevel > 0 && !list && ctx.quoteLevel === 1)) ppr.empty("w:ind", { "w:left": indent });
889
+ if (jc) ppr.val("w:jc", jc);
890
+ if (change && operation) {
891
+ ppr.open("w:rPr").empty(operation === "insert" ? "w:ins" : "w:del", this.changeAttrs(change)).close("w:rPr");
892
+ this.report.written.trackedChanges++;
893
+ } else if (change?.previous) {
894
+ ppr.open("w:pPrChange", this.changeAttrs(change)).open("w:pPr");
895
+ const old = this.previousStyle(change.previous);
896
+ if (old) ppr.val("w:pStyle", old);
897
+ const oldJc = alignment(change.previous.attrs?.textAlign);
898
+ if (oldJc) ppr.val("w:jc", oldJc);
899
+ ppr.close("w:pPr").close("w:pPrChange");
900
+ this.report.written.trackedChanges++;
901
+ }
902
+ const pprXml = ppr.toString();
903
+ if (pprXml) w.open("w:pPr").raw(pprXml).close("w:pPr");
904
+ if (list?.task && numbered) {
905
+ w.open("w:r").leaf("w:t", { "xml:space": "preserve" }, `${list.checked ? TASK_CHECKED : TASK_UNCHECKED} `).close("w:r");
906
+ }
907
+ const synthetic = change && operation ? { kind: operation === "insert" ? "ins" : "del", attrs: change } : void 0;
908
+ this.inline(w, this.pieces(node.content ?? [], opts.code === true), synthetic);
909
+ w.close("w:p");
910
+ }
911
+ /** Flattens inline content into runs of text, tabs, breaks and images. */
912
+ pieces(nodes, code, out = [], depth = 0) {
913
+ if (depth > MAX_DEPTH) return out;
914
+ for (const node of nodes) {
915
+ const marks = node.marks ?? [];
916
+ switch (node.type) {
917
+ case "text": {
918
+ const text = node.text ?? "";
919
+ const parts = text.split(/(\t|\r\n|\n|\r)/);
920
+ const pieces = [];
921
+ for (const part of parts) {
922
+ if (!part) continue;
923
+ if (part === " ") pieces.push({ kind: "tab", node, last: false, marks });
924
+ else if (part === "\n" || part === "\r\n" || part === "\r") pieces.push({ kind: "br", node, last: false, marks });
925
+ else pieces.push({ kind: "text", text: part, node, last: false, marks });
926
+ }
927
+ if (!pieces.length) pieces.push({ kind: "text", text: "", node, last: false, marks });
928
+ pieces[pieces.length - 1].last = true;
929
+ out.push(...pieces);
930
+ break;
931
+ }
932
+ case "hardBreak":
933
+ out.push({ kind: "br", node, last: true, marks });
934
+ break;
935
+ case "image":
936
+ out.push({ kind: "image", node, last: true, marks });
937
+ break;
938
+ case "mention": {
939
+ const label = node.attrs?.label ?? node.attrs?.id ?? "";
940
+ out.push({ kind: "text", text: label ? `@${label}` : "", node, last: true, marks });
941
+ break;
942
+ }
943
+ default:
944
+ if (node.content?.length) {
945
+ this.pieces(node.content, code, out, depth + 1);
946
+ } else if (typeof node.attrs?.label === "string" || typeof node.attrs?.text === "string") {
947
+ this.drop("node", node.type ?? "unknown", "written as text");
948
+ out.push({ kind: "text", text: String(node.attrs.label ?? node.attrs.text), node, last: true, marks });
949
+ } else {
950
+ this.drop("node", node.type ?? "unknown", "unsupported inline node");
951
+ }
952
+ }
953
+ }
954
+ return out;
955
+ }
956
+ revisionsOf(marks, synthetic) {
957
+ const out = [];
958
+ const ins = marks.find((m) => m.type === "insertion");
959
+ const del = marks.find((m) => m.type === "deletion");
960
+ if (ins) out.push({ kind: "ins", attrs: ins.attrs ?? {}, moveId: ins.attrs?.moveId || void 0 });
961
+ if (del) out.push({ kind: "del", attrs: del.attrs ?? {}, moveId: del.attrs?.moveId || void 0 });
962
+ if (!out.length && synthetic) out.push(synthetic);
963
+ return out;
964
+ }
965
+ linkOf(marks) {
966
+ const link = marks.find((m) => m.type === "link");
967
+ const href = typeof link?.attrs?.href === "string" ? link.attrs.href.trim() : "";
968
+ if (!href) return void 0;
969
+ if (href.startsWith("#") || /^(https?|mailto|tel|ftp):/i.test(href) || !/^[a-z][a-z0-9+.-]*:/i.test(href)) return href;
970
+ this.drop("link", href, "unsafe URL scheme");
971
+ return void 0;
972
+ }
973
+ /** Writes pieces grouped into hyperlinks (outer) and tracked-change wrappers (inner). */
974
+ inline(w, pieces, synthetic) {
975
+ let openLink;
976
+ let openRevs = null;
977
+ const closeRevs = () => {
978
+ const open = openRevs;
979
+ if (!open) return;
980
+ for (const tag of [...open.tags].reverse()) w.close(tag);
981
+ for (const range of open.moveRanges) w.empty(range.tag, { "w:id": range.id });
982
+ openRevs = null;
983
+ };
984
+ const closeLink = () => {
985
+ closeRevs();
986
+ if (openLink !== void 0) w.close("w:hyperlink");
987
+ openLink = void 0;
988
+ };
989
+ for (const piece of pieces) {
990
+ const href = this.linkOf(piece.marks);
991
+ if (href !== openLink) {
992
+ closeLink();
993
+ if (href !== void 0) {
994
+ if (href.startsWith("#")) w.open("w:hyperlink", { "w:anchor": href.slice(1), "w:history": 1 });
995
+ else w.open("w:hyperlink", { "r:id": this.rels.add(REL.hyperlink, href, true), "w:history": 1 });
996
+ openLink = href;
997
+ }
998
+ }
999
+ const revs = this.revisionsOf(piece.marks, synthetic);
1000
+ const key = revs.map((r) => `${r.kind}:${r.attrs.changeId ?? ""}:${r.moveId ?? ""}:${r.attrs.authorName ?? ""}`).join("|");
1001
+ if (key !== (openRevs?.key ?? "")) {
1002
+ closeRevs();
1003
+ if (revs.length) {
1004
+ const state = { key, tags: [], moveRanges: [] };
1005
+ for (const rev of revs) {
1006
+ const attrs2 = this.changeAttrs(rev.attrs);
1007
+ this.report.written.trackedChanges++;
1008
+ if (rev.moveId) {
1009
+ const rangeTag = rev.kind === "ins" ? "w:moveToRange" : "w:moveFromRange";
1010
+ const rangeId = ++this.revisionId;
1011
+ w.empty(`${rangeTag}Start`, { "w:id": rangeId, "w:name": rev.moveId, "w:author": attrs2["w:author"], "w:date": attrs2["w:date"] });
1012
+ state.moveRanges.push({ tag: `${rangeTag}End`, id: rangeId });
1013
+ }
1014
+ const tag = rev.moveId ? rev.kind === "ins" ? "w:moveTo" : "w:moveFrom" : rev.kind === "ins" ? "w:ins" : "w:del";
1015
+ w.open(tag, attrs2);
1016
+ state.tags.push(tag);
1017
+ }
1018
+ openRevs = state;
1019
+ }
1020
+ }
1021
+ this.commentStarts(w, piece.marks);
1022
+ this.run(w, piece, revs);
1023
+ if (piece.last) this.commentEndsAfter(w, piece.node);
1024
+ }
1025
+ closeLink();
1026
+ }
1027
+ commentStarts(w, marks) {
1028
+ for (const mark of marks) {
1029
+ if (mark.type !== "comment") continue;
1030
+ const entry = this.comments.get(mark.attrs?.commentId);
1031
+ if (!entry || entry.started) continue;
1032
+ entry.started = true;
1033
+ for (const id of entry.ids) w.empty("w:commentRangeStart", { "w:id": id });
1034
+ }
1035
+ }
1036
+ commentEndsAfter(w, node) {
1037
+ for (const threadId of this.endsByNode.get(node) ?? []) {
1038
+ const entry = this.comments.get(threadId);
1039
+ if (entry?.started && !entry.ended) this.endComment(w, threadId);
1040
+ }
1041
+ }
1042
+ endComment(w, threadId) {
1043
+ const entry = this.comments.get(threadId);
1044
+ entry.ended = true;
1045
+ for (const id of entry.ids) {
1046
+ w.empty("w:commentRangeEnd", { "w:id": id });
1047
+ w.open("w:r").open("w:rPr").val("w:rStyle", "CommentReference").close("w:rPr");
1048
+ w.empty("w:commentReference", { "w:id": id }).close("w:r");
1049
+ }
1050
+ this.needStyle("CommentReference");
1051
+ this.needStyle("CommentText");
1052
+ }
1053
+ run(w, piece, revs) {
1054
+ for (const mark of piece.marks) {
1055
+ if (!FORMAT_MARKS.has(mark.type) && !STRUCTURAL_MARKS.has(mark.type)) this.drop("mark", mark.type, "formatting not supported");
1056
+ if (mark.type === "characterStyle" && mark.attrs?.styleId && !this.styleIds.has(mark.attrs.styleId)) {
1057
+ this.drop("style", String(mark.attrs.styleId), "character style is not in the registry");
1058
+ }
1059
+ }
1060
+ if (piece.kind === "image") {
1061
+ const drawing = this.image(piece.node);
1062
+ if (!drawing) return;
1063
+ w.open("w:r");
1064
+ const format2 = runFormatOf(piece.marks);
1065
+ delete format2.charStyle;
1066
+ if (hasRunProps(format2)) {
1067
+ w.open("w:rPr");
1068
+ writeRunProps(w, format2);
1069
+ w.close("w:rPr");
1070
+ }
1071
+ w.raw(drawing).close("w:r");
1072
+ return;
1073
+ }
1074
+ const format = runFormatOf(piece.marks.filter((m) => m.type !== "characterStyle" || this.styleIds.has(m.attrs?.styleId)));
1075
+ if (format.charStyle === "Hyperlink") this.needStyle("Hyperlink");
1076
+ const formatChange = piece.marks.find((m) => m.type === "formatChange");
1077
+ w.open("w:r");
1078
+ if (hasRunProps(format) || formatChange) {
1079
+ w.open("w:rPr");
1080
+ writeRunProps(w, format);
1081
+ if (formatChange) {
1082
+ const attrs2 = formatChange.attrs ?? {};
1083
+ w.open("w:rPrChange", this.changeAttrs(attrs2)).open("w:rPr");
1084
+ writeRunProps(w, runFormatOf(Array.isArray(attrs2.before) ? attrs2.before : []));
1085
+ w.close("w:rPr").close("w:rPrChange");
1086
+ this.report.written.trackedChanges++;
1087
+ }
1088
+ w.close("w:rPr");
1089
+ }
1090
+ const deleted = revs.some((r) => r.kind === "del" && !r.moveId);
1091
+ if (piece.kind === "tab") w.empty("w:tab");
1092
+ else if (piece.kind === "br") w.empty("w:br");
1093
+ else if (piece.text) w.leaf(deleted ? "w:delText" : "w:t", { "xml:space": "preserve" }, piece.text);
1094
+ w.close("w:r");
1095
+ }
1096
+ // -- Images -----------------------------------------------------------------
1097
+ image(node) {
1098
+ const src = typeof node.attrs?.src === "string" ? node.attrs.src.trim() : "";
1099
+ const alt = typeof node.attrs?.alt === "string" ? node.attrs.alt : "";
1100
+ const title = typeof node.attrs?.title === "string" ? node.attrs.title : "";
1101
+ let relId;
1102
+ let natural = null;
1103
+ let linked = false;
1104
+ if (src.startsWith("data:")) {
1105
+ const decoded = decodeDataUrl(src);
1106
+ const ext = decoded ? DOCX_IMAGE_TYPES[decoded.mime] : void 0;
1107
+ if (!decoded || !ext) {
1108
+ this.drop("image", decoded?.mime ?? "data URL", "unsupported image type");
1109
+ return null;
1110
+ }
1111
+ const name = `image${this.media.length + 1}.${ext}`;
1112
+ this.media.push({ name, bytes: decoded.bytes });
1113
+ this.mediaTypes.add(ext);
1114
+ relId = this.rels.add(REL.image, `media/${name}`);
1115
+ natural = imagePixelSize(decoded.bytes);
1116
+ } else if (/^https?:\/\//i.test(src)) {
1117
+ if (this.options.remoteImages !== "link") {
1118
+ this.drop("image", src, 'remote image skipped (use remoteImages: "link")');
1119
+ return null;
1120
+ }
1121
+ relId = this.rels.add(REL.image, src, true);
1122
+ linked = true;
1123
+ } else {
1124
+ this.drop("image", src || "image", "image source is neither a data URL nor http(s)");
1125
+ return null;
1126
+ }
1127
+ let width = pixelAttr(node.attrs?.width) ?? natural?.width ?? 400;
1128
+ let height = pixelAttr(node.attrs?.height) ?? (natural ? natural.height * width / natural.width : 300);
1129
+ if (width > MAX_IMAGE_WIDTH_PX) {
1130
+ height = height * MAX_IMAGE_WIDTH_PX / width;
1131
+ width = MAX_IMAGE_WIDTH_PX;
1132
+ }
1133
+ const cx = pxToEmu(width);
1134
+ const cy = pxToEmu(height);
1135
+ const id = ++this.docPrId;
1136
+ this.report.written.images++;
1137
+ const w = new XmlWriter(false);
1138
+ w.open("w:drawing").open("wp:inline", { distT: 0, distB: 0, distL: 0, distR: 0 });
1139
+ w.empty("wp:extent", { cx, cy }).empty("wp:effectExtent", { l: 0, t: 0, r: 0, b: 0 });
1140
+ w.empty("wp:docPr", { id, name: `Picture ${id}`, descr: alt || void 0, title: title || void 0 });
1141
+ w.open("wp:cNvGraphicFramePr").empty("a:graphicFrameLocks", { noChangeAspect: 1 }).close("wp:cNvGraphicFramePr");
1142
+ w.open("a:graphic").open("a:graphicData", { uri: "http://schemas.openxmlformats.org/drawingml/2006/picture" });
1143
+ w.open("pic:pic").open("pic:nvPicPr").empty("pic:cNvPr", { id: 0, name: `Picture ${id}`, descr: alt || void 0 });
1144
+ w.empty("pic:cNvPicPr").close("pic:nvPicPr");
1145
+ w.open("pic:blipFill").empty("a:blip", linked ? { "r:link": relId } : { "r:embed": relId });
1146
+ w.open("a:stretch").empty("a:fillRect").close("a:stretch").close("pic:blipFill");
1147
+ w.open("pic:spPr").open("a:xfrm").empty("a:off", { x: 0, y: 0 }).empty("a:ext", { cx, cy }).close("a:xfrm");
1148
+ w.open("a:prstGeom", { prst: "rect" }).empty("a:avLst").close("a:prstGeom").close("pic:spPr");
1149
+ w.close("pic:pic").close("a:graphicData").close("a:graphic").close("wp:inline").close("w:drawing");
1150
+ return w.toString();
1151
+ }
1152
+ // -- Tables -------------------------------------------------------------------
1153
+ table(w, node, ctx) {
1154
+ const rows = (node.content ?? []).filter((row) => row.type === "tableRow" || row.content?.length);
1155
+ if (!rows.length) {
1156
+ this.drop("node", "table", "empty table");
1157
+ return;
1158
+ }
1159
+ const grid = rows.map(() => []);
1160
+ rows.forEach((row, r) => {
1161
+ let col = 0;
1162
+ for (const cell of row.content ?? []) {
1163
+ while (grid[r][col]) col++;
1164
+ const colspan = Math.max(1, Math.min(63, Number(cell.attrs?.colspan) || 1));
1165
+ const rowspan = Math.max(1, Math.min(rows.length - r, Number(cell.attrs?.rowspan) || 1));
1166
+ for (let i = 0; i < rowspan; i++) {
1167
+ for (let j = 0; j < colspan; j++) {
1168
+ grid[r + i][col + j] = { cell, rowOffset: i, colOffset: j, colspan, rowspan };
1169
+ }
1170
+ }
1171
+ col += colspan;
1172
+ }
1173
+ });
1174
+ const cols = Math.max(1, Math.min(63, ...grid.map((row) => row.length)));
1175
+ const widths = [];
1176
+ for (let c = 0; c < cols; c++) {
1177
+ const slot = grid[0][c];
1178
+ const colwidth = slot?.cell.attrs?.colwidth;
1179
+ const px = Array.isArray(colwidth) ? Number(colwidth[slot.colOffset]) : NaN;
1180
+ widths.push(Number.isFinite(px) && px > 0 ? pxToTwips(px) : 0);
1181
+ }
1182
+ const known = widths.reduce((n, v) => n + v, 0);
1183
+ const unknown = widths.filter((v) => !v).length;
1184
+ const fill = unknown ? Math.max(360, Math.floor(Math.max(0, CONTENT_WIDTH_TWIPS - known) / unknown)) : 0;
1185
+ for (let c = 0; c < cols; c++) if (!widths[c]) widths[c] = fill;
1186
+ this.report.written.tables++;
1187
+ w.open("w:tbl").open("w:tblPr");
1188
+ w.empty("w:tblW", { "w:w": 0, "w:type": "auto" });
1189
+ w.open("w:tblBorders");
1190
+ for (const side of ["top", "left", "bottom", "right", "insideH", "insideV"]) {
1191
+ w.empty(`w:${side}`, { "w:val": "single", "w:sz": 4, "w:space": 0, "w:color": "auto" });
1192
+ }
1193
+ w.close("w:tblBorders");
1194
+ w.empty("w:tblLook", { "w:val": "04A0", "w:firstRow": 1, "w:lastRow": 0, "w:firstColumn": 1, "w:lastColumn": 0, "w:noHBand": 0, "w:noVBand": 1 });
1195
+ w.close("w:tblPr");
1196
+ w.open("w:tblGrid");
1197
+ widths.forEach((width) => w.empty("w:gridCol", { "w:w": width }));
1198
+ w.close("w:tblGrid");
1199
+ const cellCtx = { depth: ctx.depth + 1, quoteLevel: 0 };
1200
+ rows.forEach((row, r) => {
1201
+ w.open("w:tr");
1202
+ const cells = row.content ?? [];
1203
+ if (cells.length && cells.every((cell) => cell.type === "tableHeader")) {
1204
+ w.open("w:trPr").empty("w:tblHeader").close("w:trPr");
1205
+ }
1206
+ for (let c = 0; c < cols; c++) {
1207
+ const slot = grid[r][c];
1208
+ if (slot && slot.colOffset > 0) continue;
1209
+ const span = slot?.colspan ?? 1;
1210
+ const width = widths.slice(c, c + span).reduce((n, v) => n + v, 0);
1211
+ w.open("w:tc").open("w:tcPr").empty("w:tcW", { "w:w": width, "w:type": "dxa" });
1212
+ if (span > 1) w.val("w:gridSpan", span);
1213
+ if (slot && slot.rowspan > 1) {
1214
+ if (slot.rowOffset === 0) w.val("w:vMerge", "restart");
1215
+ else w.empty("w:vMerge");
1216
+ }
1217
+ w.close("w:tcPr");
1218
+ const content = slot && slot.rowOffset === 0 ? slot.cell.content ?? [] : [];
1219
+ if (content.length) this.blocks(w, content, cellCtx);
1220
+ const lastType = content[content.length - 1]?.type;
1221
+ if (!content.length || lastType === "table") w.empty("w:p");
1222
+ w.close("w:tc");
1223
+ }
1224
+ w.close("w:tr");
1225
+ });
1226
+ w.close("w:tbl");
1227
+ }
1228
+ // -- Other parts --------------------------------------------------------------
1229
+ numberingXml() {
1230
+ if (!this.nums.length) return null;
1231
+ const w = new XmlWriter();
1232
+ w.open("w:numbering", BODY_NS);
1233
+ const abstract = (id, kind) => {
1234
+ w.open("w:abstractNum", { "w:abstractNumId": id }).val("w:multiLevelType", "hybridMultilevel");
1235
+ for (let level = 0; level < 9; level++) {
1236
+ w.open("w:lvl", { "w:ilvl": level }).val("w:start", 1);
1237
+ if (kind === "decimal") {
1238
+ const formats = ["decimal", "lowerLetter", "lowerRoman"];
1239
+ w.val("w:numFmt", formats[level % 3]).val("w:lvlText", `%${level + 1}.`);
1240
+ } else if (kind === "bullet") {
1241
+ w.val("w:numFmt", "bullet").val("w:lvlText", BULLETS[level % 3]);
1242
+ } else {
1243
+ w.val("w:numFmt", "none").val("w:lvlText", "");
1244
+ }
1245
+ w.val("w:lvlJc", "left");
1246
+ w.open("w:pPr").empty("w:ind", { "w:left": (level + 1) * INDENT_STEP, "w:hanging": kind === "task" ? 0 : 360 }).close("w:pPr");
1247
+ if (kind === "bullet") {
1248
+ const font = BULLET_FONTS[level % 3] === "Symbol" ? "Arial" : BULLET_FONTS[level % 3];
1249
+ w.open("w:rPr").empty("w:rFonts", { "w:ascii": font, "w:hAnsi": font, "w:hint": "default" }).close("w:rPr");
1250
+ }
1251
+ w.close("w:lvl");
1252
+ }
1253
+ w.close("w:abstractNum");
1254
+ };
1255
+ abstract(1, "bullet");
1256
+ abstract(2, "decimal");
1257
+ abstract(3, "task");
1258
+ for (const num of this.nums) {
1259
+ w.open("w:num", { "w:numId": num.numId }).val("w:abstractNumId", num.abstractId);
1260
+ if (num.start !== void 0) {
1261
+ w.open("w:lvlOverride", { "w:ilvl": num.level }).val("w:startOverride", num.start).close("w:lvlOverride");
1262
+ }
1263
+ w.close("w:num");
1264
+ }
1265
+ w.close("w:numbering");
1266
+ return w.toString();
1267
+ }
1268
+ stylesXml() {
1269
+ const w = new XmlWriter();
1270
+ w.open("w:styles", BODY_NS);
1271
+ const defaults = this.styles.defaults;
1272
+ const defaultRun = characterFormatToRun(defaults.character);
1273
+ w.open("w:docDefaults").open("w:rPrDefault").open("w:rPr");
1274
+ const font = defaultRun.font ?? "Calibri";
1275
+ w.empty("w:rFonts", { "w:ascii": font, "w:hAnsi": font, "w:eastAsia": font, "w:cs": font });
1276
+ const size = defaultRun.halfPoints ?? 22;
1277
+ w.val("w:sz", size).val("w:szCs", size);
1278
+ w.val("w:lang", "en-US");
1279
+ w.close("w:rPr").close("w:rPrDefault");
1280
+ w.open("w:pPrDefault").open("w:pPr");
1281
+ if (defaults.paragraph && Object.keys(defaults.paragraph).length) writeParagraphFormat(w, defaults.paragraph);
1282
+ else w.empty("w:spacing", { "w:after": 160, "w:line": 259, "w:lineRule": "auto" });
1283
+ w.close("w:pPr").close("w:pPrDefault").close("w:docDefaults");
1284
+ const byId = new Map(this.styles.styles.filter((s) => isValidStyleId(s.id)).map((s) => [s.id, s]));
1285
+ const written = /* @__PURE__ */ new Set();
1286
+ const blockOf = (style) => {
1287
+ let current = style;
1288
+ for (let i = 0; current && i < 32; i++) {
1289
+ if (current.block) return current.block;
1290
+ current = current.basedOn ? byId.get(current.basedOn) : void 0;
1291
+ }
1292
+ return null;
1293
+ };
1294
+ const writeStyle = (style) => {
1295
+ if (written.has(style.id)) return;
1296
+ written.add(style.id);
1297
+ const isNormal = style.kind === "paragraph" && style.id === NORMAL_STYLE_ID;
1298
+ w.open("w:style", {
1299
+ "w:type": style.kind === "character" ? "character" : "paragraph",
1300
+ "w:default": isNormal ? 1 : void 0,
1301
+ "w:customStyle": style.builtin ? void 0 : 1,
1302
+ "w:styleId": style.id
1303
+ });
1304
+ w.val("w:name", fileStyleName(style.name || style.id));
1305
+ const base = style.basedOn && byId.has(style.basedOn) && byId.get(style.basedOn).kind === style.kind ? style.basedOn : void 0;
1306
+ if (base) w.val("w:basedOn", base);
1307
+ if (style.kind === "paragraph" && style.next && byId.get(style.next)?.kind === "paragraph") w.val("w:next", style.next);
1308
+ if (style.linked && byId.has(style.linked)) w.val("w:link", style.linked);
1309
+ if (style.priority !== void 0 && Number.isFinite(style.priority)) w.val("w:uiPriority", Math.max(0, Math.round(style.priority)));
1310
+ if (style.hidden) w.empty("w:semiHidden").empty("w:unhideWhenUsed");
1311
+ if (style.quick) w.empty("w:qFormat");
1312
+ const block = style.kind === "paragraph" ? blockOf(style) : null;
1313
+ const ownBlock = style.kind === "paragraph" ? style.block : null;
1314
+ const outline = ownBlock?.type === "heading" ? Math.max(0, Math.min(8, (Number(ownBlock.attrs?.level) || 1) - 1)) : void 0;
1315
+ if (style.kind === "paragraph" && (style.paragraph || outline !== void 0)) {
1316
+ const p = new XmlWriter(false);
1317
+ writeParagraphFormat(p, style.paragraph, outline);
1318
+ const xml = p.toString();
1319
+ if (xml) w.open("w:pPr").raw(xml).close("w:pPr");
1320
+ }
1321
+ const run = characterFormatToRun(style.character);
1322
+ if (block?.type === "codeBlock" && !run.font) run.font = "Consolas";
1323
+ if (hasRunProps(run)) {
1324
+ w.open("w:rPr");
1325
+ writeRunProps(w, run);
1326
+ w.close("w:rPr");
1327
+ }
1328
+ w.close("w:style");
1329
+ };
1330
+ const visit = (style, depth = 0) => {
1331
+ if (depth > 32 || written.has(style.id)) return;
1332
+ const base = style.basedOn ? byId.get(style.basedOn) : void 0;
1333
+ if (base) visit(base, depth + 1);
1334
+ writeStyle(style);
1335
+ };
1336
+ byId.forEach((style) => visit(style));
1337
+ for (const id of this.extraStyles) {
1338
+ if (written.has(id)) continue;
1339
+ const extra = EXTRA_STYLES[id] ?? { id, name: id, kind: "paragraph", basedOn: NORMAL_STYLE_ID };
1340
+ byId.set(id, extra);
1341
+ writeStyle(extra);
1342
+ }
1343
+ w.close("w:styles");
1344
+ return w.toString();
1345
+ }
1346
+ settingsXml() {
1347
+ const w = new XmlWriter();
1348
+ w.open("w:settings", BODY_NS).empty("w:zoom", { "w:percent": 100 });
1349
+ if (this.anonymous) w.empty("w:removePersonalInformation");
1350
+ w.val("w:defaultTabStop", 720);
1351
+ w.val("w:characterSpacingControl", "doNotCompress");
1352
+ w.open("w:compat").empty("w:compatSetting", {
1353
+ "w:name": "compatibilityMode",
1354
+ "w:uri": "http://schemas.microsoft.com/office/word",
1355
+ "w:val": 15
1356
+ });
1357
+ w.close("w:compat").close("w:settings");
1358
+ return w.toString();
1359
+ }
1360
+ commentParts() {
1361
+ if (!this.comments.size) return null;
1362
+ const w = new XmlWriter();
1363
+ const ext = new XmlWriter();
1364
+ w.open("w:comments", BODY_NS);
1365
+ ext.open("w15:commentsEx", { "xmlns:w15": NS.w15, "xmlns:mc": NS.mc, "mc:Ignorable": "w15" });
1366
+ for (const entry of this.comments.values()) {
1367
+ if (!entry.started) continue;
1368
+ const { thread } = entry;
1369
+ const parentPara = this.comment(w, entry.ids[0], thread.author, thread.createdAt, thread.body);
1370
+ ext.empty("w15:commentEx", { "w15:paraId": parentPara, "w15:done": thread.resolved ? 1 : 0 });
1371
+ this.report.written.comments++;
1372
+ thread.replies.forEach((reply, i) => {
1373
+ const para = this.comment(w, entry.ids[i + 1], reply.author, reply.createdAt, reply.body);
1374
+ ext.empty("w15:commentEx", { "w15:paraId": para, "w15:paraIdParent": parentPara, "w15:done": thread.resolved ? 1 : 0 });
1375
+ this.report.written.replies++;
1376
+ });
1377
+ }
1378
+ w.close("w:comments");
1379
+ ext.close("w15:commentsEx");
1380
+ return { comments: w.toString(), extended: ext.toString() };
1381
+ }
1382
+ comment(w, id, author, createdAt, body) {
1383
+ const name = this.authorName(author?.name, author?.id);
1384
+ w.open("w:comment", {
1385
+ "w:id": id,
1386
+ "w:author": name,
1387
+ "w:date": isoDate(createdAt),
1388
+ "w:initials": this.anonymous ? ANONYMOUS_INITIALS : initialsOf(name)
1389
+ });
1390
+ const lines = String(body ?? "").replace(/\r\n?/g, "\n").split("\n");
1391
+ let paraId = "";
1392
+ lines.forEach((line, i) => {
1393
+ const last = i === lines.length - 1;
1394
+ if (last) paraId = (this.paraIdCounter++).toString(16).toUpperCase().padStart(8, "0");
1395
+ w.open("w:p", last ? { "w14:paraId": paraId, "w14:textId": "77777777" } : void 0);
1396
+ w.open("w:pPr").val("w:pStyle", "CommentText").close("w:pPr");
1397
+ if (i === 0) {
1398
+ w.open("w:r").open("w:rPr").val("w:rStyle", "CommentReference").close("w:rPr").empty("w:annotationRef").close("w:r");
1399
+ }
1400
+ if (line) w.open("w:r").leaf("w:t", { "xml:space": "preserve" }, line).close("w:r");
1401
+ w.close("w:p");
1402
+ });
1403
+ w.close("w:comment");
1404
+ return paraId;
1405
+ }
1406
+ peopleXml() {
1407
+ if (this.anonymous || !this.people.size) return null;
1408
+ const w = new XmlWriter();
1409
+ w.open("w15:people", { "xmlns:w15": NS.w15, "xmlns:mc": NS.mc, "mc:Ignorable": "w15" });
1410
+ for (const [name, id] of this.people) {
1411
+ w.open("w15:person", { "w15:author": name });
1412
+ w.empty("w15:presenceInfo", { "w15:providerId": "None", "w15:userId": id });
1413
+ w.close("w15:person");
1414
+ }
1415
+ w.close("w15:people");
1416
+ return w.toString();
1417
+ }
1418
+ coreXml(now) {
1419
+ const w = new XmlWriter();
1420
+ w.open("cp:coreProperties", {
1421
+ "xmlns:cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
1422
+ "xmlns:dc": "http://purl.org/dc/elements/1.1/",
1423
+ "xmlns:dcterms": "http://purl.org/dc/terms/",
1424
+ "xmlns:dcmitype": "http://purl.org/dc/dcmitype/",
1425
+ "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance"
1426
+ });
1427
+ if (this.options.title) w.leaf("dc:title", void 0, this.options.title);
1428
+ const creator = this.anonymous ? void 0 : this.defaultAuthor?.name;
1429
+ if (creator) w.leaf("dc:creator", void 0, creator).leaf("cp:lastModifiedBy", void 0, creator);
1430
+ const date = isoDate(now.getTime()) ?? "2026-01-01T00:00:00Z";
1431
+ w.leaf("dcterms:created", { "xsi:type": "dcterms:W3CDTF" }, date);
1432
+ w.leaf("dcterms:modified", { "xsi:type": "dcterms:W3CDTF" }, date);
1433
+ w.close("cp:coreProperties");
1434
+ return w.toString();
1435
+ }
1436
+ /** Assembles every part; `[Content_Types].xml` first. */
1437
+ parts(now) {
1438
+ const document2 = this.documentXml();
1439
+ const parts = [];
1440
+ const overrides = [];
1441
+ const add = (name, data, contentType) => {
1442
+ parts.push({ name, data });
1443
+ overrides.push([`/${name}`, contentType]);
1444
+ };
1445
+ this.rels.add(REL.styles, "styles.xml");
1446
+ this.rels.add(REL.settings, "settings.xml");
1447
+ const numbering = this.numberingXml();
1448
+ if (numbering) this.rels.add(REL.numbering, "numbering.xml");
1449
+ const comments = this.commentParts();
1450
+ if (comments) {
1451
+ this.rels.add(REL.comments, "comments.xml");
1452
+ this.rels.add(REL.commentsExtended, "commentsExtended.xml");
1453
+ }
1454
+ const people = this.peopleXml();
1455
+ if (people) this.rels.add(REL.people, "people.xml");
1456
+ const styles = this.stylesXml();
1457
+ add("word/document.xml", document2, `${CT_WML}document.main+xml`);
1458
+ add("word/styles.xml", styles, `${CT_WML}styles+xml`);
1459
+ add("word/settings.xml", this.settingsXml(), `${CT_WML}settings+xml`);
1460
+ if (numbering) add("word/numbering.xml", numbering, `${CT_WML}numbering+xml`);
1461
+ if (comments) {
1462
+ add("word/comments.xml", comments.comments, `${CT_WML}comments+xml`);
1463
+ add("word/commentsExtended.xml", comments.extended, `${CT_WML}commentsExtended+xml`);
1464
+ }
1465
+ if (people) add("word/people.xml", people, `${CT_WML}people+xml`);
1466
+ parts.push({ name: "word/_rels/document.xml.rels", data: this.rels.xml() });
1467
+ for (const file of this.media) parts.push({ name: `word/media/${file.name}`, data: file.bytes });
1468
+ add("docProps/core.xml", this.coreXml(now), "application/vnd.openxmlformats-package.core-properties+xml");
1469
+ const app = new XmlWriter();
1470
+ app.open("Properties", { xmlns: "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" });
1471
+ app.leaf("Application", void 0, "@pixerate/editor").close("Properties");
1472
+ add("docProps/app.xml", app.toString(), "application/vnd.openxmlformats-officedocument.extended-properties+xml");
1473
+ const root = new Relationships();
1474
+ root.add(REL.officeDocument, "word/document.xml");
1475
+ root.add(REL.core, "docProps/core.xml");
1476
+ root.add(REL.app, "docProps/app.xml");
1477
+ const types = new XmlWriter();
1478
+ types.open("Types", { xmlns: "http://schemas.openxmlformats.org/package/2006/content-types" });
1479
+ types.empty("Default", { Extension: "rels", ContentType: "application/vnd.openxmlformats-package.relationships+xml" });
1480
+ types.empty("Default", { Extension: "xml", ContentType: "application/xml" });
1481
+ for (const ext of this.mediaTypes) types.empty("Default", { Extension: ext, ContentType: DOCX_IMAGE_CONTENT_TYPES[ext] });
1482
+ for (const [name, contentType] of overrides) types.empty("Override", { PartName: name, ContentType: contentType });
1483
+ types.close("Types");
1484
+ return [
1485
+ { name: "[Content_Types].xml", data: types.toString() },
1486
+ { name: "_rels/.rels", data: root.xml() },
1487
+ ...parts
1488
+ ];
1489
+ }
1490
+ };
1491
+ var isInlineType = (type) => type === "hardBreak" || type === "image" || type === "mention" || type === "text";
1492
+ var EXTRA_STYLES = {
1493
+ BlockText: {
1494
+ id: "BlockText",
1495
+ name: "Block Text",
1496
+ kind: "paragraph",
1497
+ basedOn: NORMAL_STYLE_ID,
1498
+ paragraph: { indentLeft: "0.5in", indentRight: "0.5in" },
1499
+ character: { italic: true },
1500
+ priority: 99,
1501
+ builtin: true
1502
+ },
1503
+ ListParagraph: {
1504
+ id: "ListParagraph",
1505
+ name: "List Paragraph",
1506
+ kind: "paragraph",
1507
+ basedOn: NORMAL_STYLE_ID,
1508
+ paragraph: { indentLeft: "0.5in" },
1509
+ priority: 34,
1510
+ quick: true,
1511
+ builtin: true
1512
+ },
1513
+ Code: {
1514
+ id: "Code",
1515
+ name: "Code",
1516
+ kind: "paragraph",
1517
+ basedOn: NORMAL_STYLE_ID,
1518
+ block: { type: "codeBlock" },
1519
+ character: { fontFamily: "Consolas", fontSize: "10pt" },
1520
+ paragraph: { spaceAfter: "0pt", lineHeight: "1" },
1521
+ priority: 30
1522
+ },
1523
+ Hyperlink: {
1524
+ id: "Hyperlink",
1525
+ name: "Hyperlink",
1526
+ kind: "character",
1527
+ character: { color: "#0563C1", underline: true },
1528
+ priority: 99,
1529
+ hidden: true,
1530
+ builtin: true
1531
+ },
1532
+ CommentReference: {
1533
+ id: "CommentReference",
1534
+ name: "annotation reference",
1535
+ kind: "character",
1536
+ character: { fontSize: "8pt" },
1537
+ priority: 99,
1538
+ hidden: true,
1539
+ builtin: true
1540
+ },
1541
+ CommentText: {
1542
+ id: "CommentText",
1543
+ name: "annotation text",
1544
+ kind: "paragraph",
1545
+ basedOn: NORMAL_STYLE_ID,
1546
+ character: { fontSize: "10pt" },
1547
+ priority: 99,
1548
+ hidden: true,
1549
+ builtin: true
1550
+ }
1551
+ };
1552
+ for (let level = 1; level <= 6; level++) {
1553
+ EXTRA_STYLES[`Heading${level}`] = {
1554
+ id: `Heading${level}`,
1555
+ name: `Heading ${level}`,
1556
+ kind: "paragraph",
1557
+ basedOn: NORMAL_STYLE_ID,
1558
+ next: NORMAL_STYLE_ID,
1559
+ block: { type: "heading", attrs: { level } },
1560
+ character: { bold: true },
1561
+ priority: 9,
1562
+ quick: true,
1563
+ builtin: true
1564
+ };
1565
+ }
1566
+ function resolveInput(input, options) {
1567
+ if (!isEditorLike(input)) return { doc: input, options };
1568
+ const storage = input.storage ?? {};
1569
+ return {
1570
+ doc: input.getJSON(),
1571
+ options: {
1572
+ ...options,
1573
+ comments: options.comments !== void 0 ? options.comments : storage.comment?.store ?? null,
1574
+ styles: options.styles ?? storage.paragraphStyles?.registry ?? void 0
1575
+ }
1576
+ };
1577
+ }
1578
+ function createDocxParts(input, options = {}) {
1579
+ const resolved = resolveInput(input, options);
1580
+ const builder = new DocxBuilder(
1581
+ resolved.doc ?? { type: "doc", content: [] },
1582
+ resolved.options,
1583
+ threadsOf(resolved.options.comments),
1584
+ stylesOf(resolved.options.styles)
1585
+ );
1586
+ const parts = builder.parts(resolved.options.now?.() ?? /* @__PURE__ */ new Date());
1587
+ return { parts, report: builder.report };
1588
+ }
1589
+ async function exportDocx(input, options = {}) {
1590
+ const { parts, report } = createDocxParts(input, options);
1591
+ const data = await writeZip(parts, { compression: options.compression });
1592
+ return { data, report };
1593
+ }
1594
+ async function exportDocxBlob(input, options = {}) {
1595
+ const { data, report } = await exportDocx(input, options);
1596
+ return { blob: new Blob([data], { type: DOCX_EXPORT_MIME_TYPE }), report };
1597
+ }
1598
+
1599
+ // src/export/controller.ts
1600
+ function downloadBlob(blob, fileName) {
1601
+ if (typeof document === "undefined" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function") {
1602
+ return false;
1603
+ }
1604
+ const url = URL.createObjectURL(blob);
1605
+ const link = document.createElement("a");
1606
+ link.href = url;
1607
+ link.download = fileName;
1608
+ link.rel = "noopener";
1609
+ link.style.display = "none";
1610
+ document.body.appendChild(link);
1611
+ link.click();
1612
+ link.remove();
1613
+ setTimeout(() => URL.revokeObjectURL(url), 0);
1614
+ return true;
1615
+ }
1616
+ function docxFileName(name) {
1617
+ const base = (name ?? "").replace(/[\u0000-\u001f\u007f<>:"/\\|?*]+/g, " ").replace(/\s+/g, " ").trim().replace(/\.docx$/i, "").slice(0, 120);
1618
+ return `${base || "document"}.docx`;
1619
+ }
1620
+ var DocxExportController = class {
1621
+ constructor(getSource, options = {}) {
1622
+ this.getSource = getSource;
1623
+ this.options = options;
1624
+ }
1625
+ getSource;
1626
+ options;
1627
+ state = { isExporting: false, error: null, lastReport: null };
1628
+ listeners = /* @__PURE__ */ new Set();
1629
+ pending = 0;
1630
+ setOptions(options) {
1631
+ this.options = options;
1632
+ }
1633
+ getState = () => this.state;
1634
+ subscribe = (listener) => {
1635
+ this.listeners.add(listener);
1636
+ return () => {
1637
+ this.listeners.delete(listener);
1638
+ };
1639
+ };
1640
+ setState(patch) {
1641
+ this.state = { ...this.state, ...patch };
1642
+ this.listeners.forEach((listener) => listener(this.state));
1643
+ }
1644
+ /** Builds the `.docx` without downloading it. Resolves with `null` when there is no editor or it fails. */
1645
+ async exportBlob() {
1646
+ const source = this.getSource();
1647
+ if (!source) return null;
1648
+ this.pending++;
1649
+ this.setState({ isExporting: true, error: null });
1650
+ try {
1651
+ const { fileName: _fileName, onExport: _onExport, onError: _onError, ...exportOptions } = this.options;
1652
+ const result = await exportDocxBlob(source, exportOptions);
1653
+ this.setState({ lastReport: result.report });
1654
+ this.options.onExport?.(result.report, result.blob);
1655
+ return result;
1656
+ } catch (error) {
1657
+ const err = error instanceof Error ? error : new Error(String(error));
1658
+ this.setState({ error: err });
1659
+ if (this.options.onError) this.options.onError(err);
1660
+ else console.error("[Editor] DOCX export failed:", err);
1661
+ return null;
1662
+ } finally {
1663
+ this.pending--;
1664
+ if (this.pending === 0) this.setState({ isExporting: false });
1665
+ }
1666
+ }
1667
+ /** Exports and downloads the document. Resolves with the report, or `null`. */
1668
+ async download(fileName) {
1669
+ const result = await this.exportBlob();
1670
+ if (!result) return null;
1671
+ downloadBlob(result.blob, docxFileName(fileName ?? this.options.fileName ?? this.options.title));
1672
+ return result.report;
1673
+ }
1674
+ };
1675
+ function createDocxExportController(getSource, options) {
1676
+ return new DocxExportController(getSource, options);
1677
+ }
1678
+
1679
+ export {
1680
+ stripInvalidXmlChars,
1681
+ escapeXmlText,
1682
+ escapeXmlAttr,
1683
+ XmlWriter,
1684
+ deflateRaw,
1685
+ writeZip,
1686
+ DOCX_EXPORT_MIME_TYPE,
1687
+ DOCX_ANONYMOUS_AUTHOR,
1688
+ createDocxParts,
1689
+ exportDocx,
1690
+ exportDocxBlob,
1691
+ downloadBlob,
1692
+ docxFileName,
1693
+ DocxExportController,
1694
+ createDocxExportController
1695
+ };
1696
+ //# sourceMappingURL=chunk-KRGXUQ2Y.js.map