@4399ywkf/editor 0.1.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.
package/dist/docx.js ADDED
@@ -0,0 +1,1181 @@
1
+ import { Node, mergeAttributes, Extension } from '@tiptap/core';
2
+ import { unzipSync, zipSync } from 'fflate';
3
+
4
+ // src/docx/index.ts
5
+ var DocxField = Node.create({
6
+ name: "docxField",
7
+ group: "inline",
8
+ inline: true,
9
+ atom: true,
10
+ selectable: true,
11
+ addAttributes() {
12
+ return {
13
+ code: { default: null, parseHTML: (el) => el.getAttribute("data-field-code") },
14
+ cached: { default: "", parseHTML: (el) => el.textContent ?? "" }
15
+ };
16
+ },
17
+ parseHTML() {
18
+ return [{ tag: "span[data-docx-field]" }];
19
+ },
20
+ renderHTML({ HTMLAttributes, node }) {
21
+ const a = node.attrs;
22
+ return [
23
+ "span",
24
+ mergeAttributes(HTMLAttributes, {
25
+ "data-docx-field": "",
26
+ "data-field-code": a.code ?? "",
27
+ class: "docx-field",
28
+ title: a.code ? `Word 域:${a.code}` : "Word 域"
29
+ }),
30
+ a.cached || ""
31
+ ];
32
+ }
33
+ });
34
+ var DocxInk = Node.create({
35
+ name: "docxInk",
36
+ group: "inline",
37
+ inline: true,
38
+ atom: true,
39
+ draggable: false,
40
+ selectable: true,
41
+ addAttributes() {
42
+ return {
43
+ src: { default: null },
44
+ strokes: { default: null },
45
+ /** 矢量墨迹的原始 XML 内容。存内容而非路径,才能脱离原始 .docx 往返 */
46
+ strokesXml: { default: null },
47
+ x: { default: 0, parseHTML: (el) => Number(el.getAttribute("data-x")) || 0 },
48
+ y: { default: 0, parseHTML: (el) => Number(el.getAttribute("data-y")) || 0 },
49
+ width: { default: 0, parseHTML: (el) => Number(el.getAttribute("data-w")) || 0 },
50
+ height: { default: 0, parseHTML: (el) => Number(el.getAttribute("data-h")) || 0 },
51
+ wrap: { default: "none" },
52
+ behindDoc: { default: false }
53
+ };
54
+ },
55
+ parseHTML() {
56
+ return [{ tag: "span[data-docx-ink]" }];
57
+ },
58
+ renderHTML({ HTMLAttributes, node }) {
59
+ const a = node.attrs;
60
+ const floating = a.wrap === "none";
61
+ const style = floating ? [
62
+ "position:absolute",
63
+ `left:${a.x}px`,
64
+ `top:${a.y}px`,
65
+ a.width ? `width:${a.width}px` : "",
66
+ a.height ? `height:${a.height}px` : "",
67
+ `z-index:${a.behindDoc ? -1 : 1}`,
68
+ "pointer-events:none"
69
+ ].filter(Boolean).join(";") : [`float:${a.x > 0 ? "right" : "left"}`, a.width ? `width:${a.width}px` : ""].filter(Boolean).join(";");
70
+ return [
71
+ "span",
72
+ mergeAttributes(HTMLAttributes, {
73
+ "data-docx-ink": "",
74
+ "data-x": String(a.x),
75
+ "data-y": String(a.y),
76
+ "data-w": String(a.width),
77
+ "data-h": String(a.height),
78
+ "data-strokes": a.strokes ?? "",
79
+ class: "docx-ink",
80
+ style
81
+ }),
82
+ a.src ? ["img", { src: a.src, alt: "手写墨迹", style: "width:100%;height:100%;display:block" }] : ["span", { class: "docx-ink-placeholder" }, "✍︎"]
83
+ ];
84
+ }
85
+ });
86
+
87
+ // src/docx/parse.ts
88
+ var EMU_PER_PX = 9525;
89
+ var decoder = new TextDecoder("utf-8");
90
+ function parseDocx(entries, opts) {
91
+ const { DOMParser } = opts;
92
+ const recovered = {
93
+ styleFormat: 0,
94
+ directFormat: 0,
95
+ fields: 0,
96
+ ink: 0,
97
+ floatImages: 0,
98
+ inlineImages: 0,
99
+ trackedIns: 0,
100
+ trackedDel: 0,
101
+ tables: 0,
102
+ tableCells: 0,
103
+ mergedCells: 0,
104
+ cellShading: 0,
105
+ lists: 0,
106
+ listItems: 0
107
+ };
108
+ const warnings = [];
109
+ const text = (p) => {
110
+ const b = entries.read(p);
111
+ return b ? decoder.decode(b) : null;
112
+ };
113
+ const xml = (p) => {
114
+ const t = text(p);
115
+ return t ? new DOMParser().parseFromString(t, "text/xml") : null;
116
+ };
117
+ const REL = {};
118
+ const relsDoc = xml("word/_rels/document.xml.rels");
119
+ if (relsDoc) {
120
+ for (const r of Array.from(relsDoc.getElementsByTagName("Relationship"))) {
121
+ const id = r.getAttribute("Id");
122
+ const target = r.getAttribute("Target");
123
+ if (id && target) REL[id] = target;
124
+ }
125
+ }
126
+ const STYLE_NAME = {};
127
+ const STYLE_FMT = {};
128
+ const STYLE_BASE = {};
129
+ const stylesDoc = xml("word/styles.xml");
130
+ if (stylesDoc) {
131
+ for (const s of Array.from(stylesDoc.getElementsByTagName("w:style"))) {
132
+ const id = s.getAttribute("w:styleId");
133
+ if (!id) continue;
134
+ const name = s.getElementsByTagName("w:name")[0]?.getAttribute("w:val");
135
+ if (name) STYLE_NAME[id] = name;
136
+ STYLE_BASE[id] = s.getElementsByTagName("w:basedOn")[0]?.getAttribute("w:val") ?? null;
137
+ const rPr = Array.from(s.childNodes).find(
138
+ (c) => c.nodeType === 1 && c.nodeName === "w:rPr"
139
+ );
140
+ if (!rPr) continue;
141
+ STYLE_FMT[id] = readFmt(rPr);
142
+ }
143
+ }
144
+ function readFmt(rPr) {
145
+ const on = (t) => {
146
+ const e = rPr.getElementsByTagName(t)[0];
147
+ if (!e) return void 0;
148
+ const v = e.getAttribute("w:val");
149
+ return v !== "0" && v !== "false" && v !== "none" ? true : void 0;
150
+ };
151
+ const color = rPr.getElementsByTagName("w:color")[0]?.getAttribute("w:val");
152
+ return {
153
+ b: on("w:b"),
154
+ i: on("w:i"),
155
+ u: on("w:u"),
156
+ strike: on("w:strike"),
157
+ color: color && color !== "auto" ? `#${color}` : void 0
158
+ };
159
+ }
160
+ function resolveStyleFmt(id, seen = /* @__PURE__ */ new Set()) {
161
+ if (!id || seen.has(id)) return {};
162
+ seen.add(id);
163
+ const merged = { ...resolveStyleFmt(STYLE_BASE[id] ?? null, seen) };
164
+ for (const [k, v] of Object.entries(STYLE_FMT[id] ?? {})) {
165
+ if (v !== void 0) merged[k] = v;
166
+ }
167
+ return merged;
168
+ }
169
+ function fmtToMarks(f) {
170
+ const marks = [];
171
+ if (f.b) marks.push({ type: "bold" });
172
+ if (f.i) marks.push({ type: "italic" });
173
+ if (f.u) marks.push({ type: "underline" });
174
+ if (f.strike) marks.push({ type: "strike" });
175
+ if (f.color) marks.push({ type: "textStyle", attrs: { color: f.color } });
176
+ return marks;
177
+ }
178
+ const mediaCache = /* @__PURE__ */ new Map();
179
+ function mediaUrl(target) {
180
+ if (!target) return null;
181
+ const entry = `word/${target.replace(/^\.\//, "")}`;
182
+ if (mediaCache.has(entry)) return mediaCache.get(entry) ?? null;
183
+ const bytes = entries.read(entry);
184
+ if (!bytes) {
185
+ warnings.push(`找不到媒体条目 ${entry}`);
186
+ mediaCache.set(entry, null);
187
+ return null;
188
+ }
189
+ const url = opts.resolveMedia ? opts.resolveMedia(bytes, entry) : `data:image/png;base64,${bytesToBase64(bytes)}`;
190
+ mediaCache.set(entry, url);
191
+ return url;
192
+ }
193
+ const docXml = xml("word/document.xml");
194
+ if (!docXml) throw new Error("这不是一个有效的 .docx:缺少 word/document.xml");
195
+ const kids = (n) => Array.from(n.childNodes).filter((c) => c.nodeType === 1);
196
+ const first = (n, name) => n.getElementsByTagName(name)[0] ?? null;
197
+ function readDrawing(dr) {
198
+ const anchor = first(dr, "wp:anchor");
199
+ const inline = first(dr, "wp:inline");
200
+ const holder = anchor ?? inline;
201
+ if (!holder) return null;
202
+ const blip = first(holder, "a:blip");
203
+ const rid = blip?.getAttribute("r:embed") ?? blip?.getAttribute("r:link") ?? void 0;
204
+ const src = mediaUrl(rid ? REL[rid] : void 0);
205
+ const ext = first(holder, "wp:extent");
206
+ const width = Math.round(Number(ext?.getAttribute("cx") ?? 0) / EMU_PER_PX);
207
+ const height = Math.round(Number(ext?.getAttribute("cy") ?? 0) / EMU_PER_PX);
208
+ const alt = first(holder, "wp:docPr")?.getAttribute("descr") ?? null;
209
+ if (!anchor) return { src, width, height, alt, floating: false };
210
+ const offset = (axis) => {
211
+ const p = first(anchor, axis);
212
+ const off = p ? first(p, "wp:posOffset") : null;
213
+ return off ? Math.round(Number(off.textContent) / EMU_PER_PX) : 0;
214
+ };
215
+ const wrapEl = ["wp:wrapNone", "wp:wrapSquare", "wp:wrapTight", "wp:wrapThrough", "wp:wrapTopAndBottom"].find(
216
+ (w) => first(anchor, w)
217
+ );
218
+ return {
219
+ src,
220
+ width,
221
+ height,
222
+ alt,
223
+ floating: true,
224
+ x: offset("wp:positionH"),
225
+ y: offset("wp:positionV"),
226
+ wrap: wrapEl ? wrapEl.replace("wp:wrap", "").toLowerCase() : "none",
227
+ behindDoc: anchor.getAttribute("behindDoc") === "1"
228
+ };
229
+ }
230
+ function readAlternate(alt) {
231
+ const choice = first(alt, "mc:Choice");
232
+ const fallback = first(alt, "mc:Fallback");
233
+ const cp = choice ? first(choice, "w14:contentPart") : null;
234
+ const fbDrawing = fallback ? first(fallback, "w:drawing") : null;
235
+ const geo = fbDrawing ? readDrawing(fbDrawing) : null;
236
+ if (cp) {
237
+ recovered.ink += 1;
238
+ const rid = cp.getAttribute("r:id");
239
+ const strokesPath = rid ? REL[rid] ?? null : null;
240
+ const strokesXml = strokesPath ? text(`word/${strokesPath.replace(/^\.\//, "")}`) : null;
241
+ return {
242
+ type: "docxInk",
243
+ attrs: {
244
+ src: geo?.src ?? null,
245
+ strokes: strokesPath,
246
+ strokesXml,
247
+ x: geo && geo.floating ? geo.x : 0,
248
+ y: geo && geo.floating ? geo.y : 0,
249
+ width: geo?.width ?? 0,
250
+ height: geo?.height ?? 0,
251
+ wrap: geo && geo.floating ? geo.wrap : "none",
252
+ behindDoc: geo && geo.floating ? geo.behindDoc : false
253
+ }
254
+ };
255
+ }
256
+ if (geo?.src) {
257
+ if (geo.floating) recovered.floatImages += 1;
258
+ else recovered.inlineImages += 1;
259
+ return { type: "image", attrs: { src: geo.src, alt: geo.alt } };
260
+ }
261
+ return null;
262
+ }
263
+ function runInto(r, out, inherited) {
264
+ const rPr = first(r, "w:rPr");
265
+ const direct = rPr ? readFmt(rPr) : {};
266
+ if (rPr && Object.values(direct).some((v) => v !== void 0)) recovered.directFormat += 1;
267
+ const marks = fmtToMarks({ ...inherited, ...stripUndefined(direct) });
268
+ for (const c of kids(r)) {
269
+ const t = c.nodeName;
270
+ if (t === "w:t") {
271
+ const s = c.textContent ?? "";
272
+ if (s) out.inline.push(marks.length ? { type: "text", text: s, marks } : { type: "text", text: s });
273
+ } else if (t === "w:br") {
274
+ out.inline.push({ type: "hardBreak" });
275
+ } else if (t === "w:tab") {
276
+ out.inline.push({ type: "text", text: " " });
277
+ } else if (t === "w:drawing") {
278
+ const g = readDrawing(c);
279
+ if (!g?.src) continue;
280
+ if (g.floating) {
281
+ recovered.floatImages += 1;
282
+ out.inline.push({
283
+ type: "docxInk",
284
+ attrs: {
285
+ src: g.src,
286
+ strokes: null,
287
+ x: g.x,
288
+ y: g.y,
289
+ width: g.width,
290
+ height: g.height,
291
+ wrap: g.wrap,
292
+ behindDoc: g.behindDoc
293
+ }
294
+ });
295
+ } else {
296
+ recovered.inlineImages += 1;
297
+ out.blocks.push({ type: "image", attrs: { src: g.src, alt: g.alt } });
298
+ }
299
+ } else if (t === "mc:AlternateContent") {
300
+ const n = readAlternate(c);
301
+ if (!n) continue;
302
+ if (n.type === "image") out.blocks.push(n);
303
+ else out.inline.push(n);
304
+ } else if (t === "w:pict") {
305
+ warnings.push("遇到 VML 图形(w:pict),未处理");
306
+ }
307
+ }
308
+ }
309
+ function childrenInto(node, out, inherited) {
310
+ for (const c of kids(node)) {
311
+ const t = c.nodeName;
312
+ if (t === "w:r") runInto(c, out, inherited);
313
+ else if (t === "w:hyperlink") {
314
+ const rid = c.getAttribute("r:id");
315
+ const anchorName = c.getAttribute("w:anchor");
316
+ const href = rid ? REL[rid] ?? "" : anchorName ? `#${anchorName}` : "";
317
+ const sub = { inline: [], blocks: [] };
318
+ childrenInto(c, sub, inherited);
319
+ for (const n of sub.inline) {
320
+ if (n.type === "text") {
321
+ const marks = n.marks ?? [];
322
+ n.marks = [...marks, { type: "link", attrs: { href } }];
323
+ }
324
+ out.inline.push(n);
325
+ }
326
+ out.blocks.push(...sub.blocks);
327
+ } else if (t === "w:fldSimple") {
328
+ recovered.fields += 1;
329
+ const code = (c.getAttribute("w:instr") ?? "").trim();
330
+ const cached = Array.from(c.getElementsByTagName("w:t")).map((n) => n.textContent ?? "").join("");
331
+ out.inline.push({ type: "docxField", attrs: { code, cached } });
332
+ } else if (t === "w:ins") {
333
+ recovered.trackedIns += 1;
334
+ childrenInto(c, out, inherited);
335
+ } else if (t === "w:del") {
336
+ recovered.trackedDel += 1;
337
+ } else if (t === "mc:AlternateContent") {
338
+ const n = readAlternate(c);
339
+ if (n) (n.type === "image" ? out.blocks : out.inline).push(n);
340
+ }
341
+ }
342
+ }
343
+ function paragraph(p) {
344
+ const pPr = first(p, "w:pPr");
345
+ const styleId = pPr ? first(pPr, "w:pStyle")?.getAttribute("w:val") ?? null : null;
346
+ const styleName = styleId ? STYLE_NAME[styleId] ?? styleId : null;
347
+ const jc = pPr ? first(pPr, "w:jc")?.getAttribute("w:val") : null;
348
+ const styleFmt = resolveStyleFmt(styleId);
349
+ if (Object.values(styleFmt).some((v) => v !== void 0)) recovered.styleFormat += 1;
350
+ const out = { inline: [], blocks: [] };
351
+ childrenInto(p, out, styleFmt);
352
+ const attrs = {};
353
+ if (jc) attrs.textAlign = jc;
354
+ const heading = /^heading\s*(\d)$/i.exec(styleName ?? "");
355
+ const blocks = [];
356
+ const hasContent = out.inline.length > 0;
357
+ if (heading) {
358
+ blocks.push({
359
+ type: "heading",
360
+ attrs: { ...attrs, level: Number(heading[1]) },
361
+ ...hasContent ? { content: out.inline } : {}
362
+ });
363
+ } else {
364
+ blocks.push({ type: "paragraph", attrs, ...hasContent ? { content: out.inline } : {} });
365
+ }
366
+ blocks.push(...out.blocks);
367
+ return blocks;
368
+ }
369
+ const dxaToPx = (v) => v ? Math.round(Number(v) / 15) : null;
370
+ function readRows(tbl) {
371
+ const rows = [];
372
+ for (const tr of kids(tbl)) {
373
+ if (tr.nodeName !== "w:tr") continue;
374
+ const cells = [];
375
+ let col = 0;
376
+ for (const tc of kids(tr)) {
377
+ if (tc.nodeName !== "w:tc") continue;
378
+ const tcPr = first(tc, "w:tcPr");
379
+ const span = Number(tcPr ? first(tcPr, "w:gridSpan")?.getAttribute("w:val") ?? 1 : 1) || 1;
380
+ const vm = tcPr ? first(tcPr, "w:vMerge") : null;
381
+ const vMerge = vm ? vm.getAttribute("w:val") === "restart" ? "restart" : "continue" : null;
382
+ cells.push({ el: tc, colStart: col, colSpan: span, vMerge });
383
+ col += span;
384
+ }
385
+ rows.push(cells);
386
+ }
387
+ return rows;
388
+ }
389
+ function rowSpanOf(rows, rowIdx, colStart) {
390
+ let n = 1;
391
+ for (let r = rowIdx + 1; r < rows.length; r++) {
392
+ const hit = rows[r].find((c) => c.colStart === colStart);
393
+ if (hit?.vMerge === "continue") n += 1;
394
+ else break;
395
+ }
396
+ return n;
397
+ }
398
+ function table(tbl) {
399
+ recovered.tables += 1;
400
+ const tblPr = first(tbl, "w:tblPr");
401
+ const firstRowIsHeader = tblPr ? first(tblPr, "w:tblLook")?.getAttribute("w:firstRow") === "1" : false;
402
+ const tblWType = tblPr ? first(tblPr, "w:tblW")?.getAttribute("w:type") : null;
403
+ const useAbsoluteWidths = tblWType !== "pct";
404
+ const grid = first(tbl, "w:tblGrid");
405
+ const rawWidths = grid ? kids(grid).filter((g) => g.nodeName === "w:gridCol").map((g) => dxaToPx(g.getAttribute("w:w"))) : [];
406
+ const sane = rawWidths.length > 0 && rawWidths.every((w) => w != null && w >= 20);
407
+ const colWidths = useAbsoluteWidths && sane ? rawWidths : [];
408
+ if (rawWidths.length > 0 && colWidths.length === 0) {
409
+ warnings.push(
410
+ `表格列宽未采信(tblW type=${tblWType ?? "未指定"}),改为自动分配 —— gridCol 原值 ${rawWidths.slice(0, 4).join("/")}`
411
+ );
412
+ }
413
+ const rows = readRows(tbl);
414
+ const rowNodes = [];
415
+ rows.forEach((cells, rowIdx) => {
416
+ const tr = kids(tbl).filter((n) => n.nodeName === "w:tr")[rowIdx];
417
+ const trPr = tr ? first(tr, "w:trPr") : null;
418
+ const isHeaderRow = Boolean(trPr && first(trPr, "w:tblHeader")) || rowIdx === 0 && firstRowIsHeader;
419
+ const cellNodes = [];
420
+ for (const c of cells) {
421
+ if (c.vMerge === "continue") continue;
422
+ recovered.tableCells += 1;
423
+ const tcPr = first(c.el, "w:tcPr");
424
+ const attrs = {};
425
+ if (c.colSpan > 1) {
426
+ attrs.colspan = c.colSpan;
427
+ recovered.mergedCells += 1;
428
+ }
429
+ const rs = c.vMerge === "restart" ? rowSpanOf(rows, rowIdx, c.colStart) : 1;
430
+ if (rs > 1) {
431
+ attrs.rowspan = rs;
432
+ recovered.mergedCells += 1;
433
+ }
434
+ const widths = colWidths.slice(c.colStart, c.colStart + c.colSpan).filter((w) => w != null);
435
+ if (widths.length) attrs.colwidth = widths;
436
+ const fill = tcPr ? first(tcPr, "w:shd")?.getAttribute("w:fill") : null;
437
+ if (fill && fill !== "auto") {
438
+ attrs.backgroundColor = `#${fill}`;
439
+ recovered.cellShading += 1;
440
+ }
441
+ const vAlign = tcPr ? first(tcPr, "w:vAlign")?.getAttribute("w:val") : null;
442
+ if (vAlign) attrs.nodeVerticalAlign = vAlign;
443
+ const inner = blocksOf(c.el);
444
+ if (inner.length === 0) inner.push({ type: "paragraph", attrs: {} });
445
+ cellNodes.push({
446
+ type: isHeaderRow ? "tableHeader" : "tableCell",
447
+ attrs,
448
+ content: inner
449
+ });
450
+ }
451
+ if (cellNodes.length) rowNodes.push({ type: "tableRow", content: cellNodes });
452
+ });
453
+ return { type: "table", content: rowNodes };
454
+ }
455
+ const NUM = {};
456
+ const numberingDoc = xml("word/numbering.xml");
457
+ if (numberingDoc) {
458
+ const abstract = {};
459
+ for (const an of Array.from(numberingDoc.getElementsByTagName("w:abstractNum"))) {
460
+ const id = an.getAttribute("w:abstractNumId");
461
+ if (!id) continue;
462
+ const levels = [];
463
+ for (const lvl of Array.from(an.getElementsByTagName("w:lvl"))) {
464
+ const i = Number(lvl.getAttribute("w:ilvl") ?? 0);
465
+ levels[i] = {
466
+ fmt: lvl.getElementsByTagName("w:numFmt")[0]?.getAttribute("w:val") ?? "bullet",
467
+ start: Number(lvl.getElementsByTagName("w:start")[0]?.getAttribute("w:val") ?? 1)
468
+ };
469
+ }
470
+ abstract[id] = levels;
471
+ }
472
+ for (const n of Array.from(numberingDoc.getElementsByTagName("w:num"))) {
473
+ const numId = n.getAttribute("w:numId");
474
+ const aid = n.getElementsByTagName("w:abstractNumId")[0]?.getAttribute("w:val");
475
+ if (numId && aid && abstract[aid]) NUM[numId] = abstract[aid];
476
+ }
477
+ }
478
+ function numPrOf(p) {
479
+ const pPr = first(p, "w:pPr");
480
+ const numPr = pPr ? first(pPr, "w:numPr") : null;
481
+ if (!numPr) return null;
482
+ const numId = first(numPr, "w:numId")?.getAttribute("w:val");
483
+ if (!numId || numId === "0") return null;
484
+ return { ilvl: Number(first(numPr, "w:ilvl")?.getAttribute("w:val") ?? 0), numId };
485
+ }
486
+ const levelSpec = (numId, ilvl) => NUM[numId]?.[ilvl] ?? NUM[numId]?.[0] ?? { fmt: "bullet", start: 1 };
487
+ function buildList(items, from, level) {
488
+ const spec = levelSpec(items[from].numId, level);
489
+ const ordered = spec.fmt !== "bullet" && spec.fmt !== "none";
490
+ const listNode = {
491
+ type: ordered ? "orderedList" : "bulletList",
492
+ attrs: ordered && spec.start !== 1 ? { start: spec.start } : {},
493
+ content: []
494
+ };
495
+ const children = listNode.content;
496
+ let i = from;
497
+ while (i < items.length && items[i].level >= level) {
498
+ if (items[i].level === level) {
499
+ recovered.listItems += 1;
500
+ children.push({ type: "listItem", content: items[i].blocks });
501
+ i += 1;
502
+ } else {
503
+ const [sub, next] = buildList(items, i, items[i].level);
504
+ const last = children[children.length - 1];
505
+ if (last?.content) last.content.push(sub);
506
+ else children.push({ type: "listItem", content: [sub] });
507
+ i = next;
508
+ }
509
+ }
510
+ return [listNode, i];
511
+ }
512
+ function flushList(items, out) {
513
+ let i = 0;
514
+ while (i < items.length) {
515
+ recovered.lists += 1;
516
+ const [node, next] = buildList(items, i, items[i].level);
517
+ out.push(node);
518
+ i = next;
519
+ }
520
+ items.length = 0;
521
+ }
522
+ function blocksOf(container) {
523
+ const out = [];
524
+ const pending = [];
525
+ for (const c of kids(container)) {
526
+ if (c.nodeName === "w:p") {
527
+ const np = numPrOf(c);
528
+ if (np) {
529
+ pending.push({ level: np.ilvl, numId: np.numId, blocks: paragraph(c) });
530
+ continue;
531
+ }
532
+ if (pending.length) flushList(pending, out);
533
+ out.push(...paragraph(c));
534
+ } else if (c.nodeName === "w:tbl") {
535
+ if (pending.length) flushList(pending, out);
536
+ out.push(table(c));
537
+ }
538
+ }
539
+ if (pending.length) flushList(pending, out);
540
+ return out;
541
+ }
542
+ const body = docXml.getElementsByTagName("w:body")[0];
543
+ if (!body) throw new Error("docx 缺少 w:body");
544
+ const content = blocksOf(body);
545
+ const doc = remapTypes({ type: "doc", content }, opts.prosemirrorNodes, opts.prosemirrorMarks);
546
+ return {
547
+ doc,
548
+ recovered,
549
+ warnings,
550
+ ...opts.cssStyles ? { css: extractCss(stylesDoc) } : {}
551
+ };
552
+ function extractCss(sd) {
553
+ if (!sd) return "";
554
+ const defRPr = sd.getElementsByTagName("w:rPrDefault")[0]?.getElementsByTagName("w:rPr")[0];
555
+ const rules = [];
556
+ if (defRPr) {
557
+ const font = defRPr.getElementsByTagName("w:rFonts")[0]?.getAttribute("w:ascii");
558
+ const sz = defRPr.getElementsByTagName("w:sz")[0]?.getAttribute("w:val");
559
+ const decl = [
560
+ font ? `font-family:"${font}"` : "",
561
+ sz ? `font-size:${Number(sz) / 2}pt` : ""
562
+ ].filter(Boolean);
563
+ if (decl.length) rules.push(`.tiptap{${decl.join(";")}}`);
564
+ }
565
+ for (const [id, f] of Object.entries(STYLE_FMT)) {
566
+ const name = STYLE_NAME[id];
567
+ if (!name || !f.color) continue;
568
+ const sel = /^heading\s*(\d)$/i.exec(name);
569
+ if (sel) rules.push(`.tiptap h${sel[1]}{color:${f.color}}`);
570
+ }
571
+ return rules.join("\n");
572
+ }
573
+ }
574
+ function remapTypes(node, nodeMap, markMap) {
575
+ if (!nodeMap && !markMap) return node;
576
+ const t = node.type;
577
+ const out = { ...node };
578
+ if (t && nodeMap?.[t]) out.type = nodeMap[t];
579
+ if (Array.isArray(node.marks) && markMap) {
580
+ out.marks = node.marks.map((m) => {
581
+ const mt = m.type;
582
+ return markMap[mt] ? { ...m, type: markMap[mt] } : m;
583
+ });
584
+ }
585
+ if (Array.isArray(node.content)) {
586
+ out.content = node.content.map(
587
+ (c) => remapTypes(c, nodeMap, markMap)
588
+ );
589
+ }
590
+ return out;
591
+ }
592
+ function stripUndefined(o) {
593
+ return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
594
+ }
595
+ function bytesToBase64(bytes) {
596
+ let bin = "";
597
+ for (let i = 0; i < bytes.length; i += 32768) {
598
+ bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
599
+ }
600
+ return typeof btoa === "function" ? btoa(bin) : Buffer.from(bytes).toString("base64");
601
+ }
602
+
603
+ // src/docx/serialize.ts
604
+ var EMU_PER_PX2 = 9525;
605
+ var PX_TO_DXA = 15;
606
+ var enc = new TextEncoder();
607
+ var esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
608
+ var NS = [
609
+ 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"',
610
+ 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"',
611
+ 'xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"',
612
+ 'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"',
613
+ 'xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"',
614
+ 'xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"',
615
+ 'xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"',
616
+ 'mc:Ignorable="w14"'
617
+ ].join(" ");
618
+ function serializeDocx(doc, opts = {}) {
619
+ const warnings = [];
620
+ const stats = {
621
+ paragraphs: 0,
622
+ headings: 0,
623
+ tables: 0,
624
+ lists: 0,
625
+ listItems: 0,
626
+ images: 0,
627
+ fields: 0,
628
+ ink: 0
629
+ };
630
+ const rels = [
631
+ `<Relationship Id="rStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`,
632
+ `<Relationship Id="rNumbering" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>`
633
+ ];
634
+ const files = {};
635
+ const mediaExts = /* @__PURE__ */ new Set();
636
+ let relSeq = 0;
637
+ const imageCache = /* @__PURE__ */ new Map();
638
+ function addImage(src) {
639
+ if (imageCache.has(src)) return imageCache.get(src) ?? null;
640
+ const bytes = opts.resolveImage?.(src) ?? null;
641
+ if (!bytes) {
642
+ warnings.push(`图片无法取字节,已跳过:${src.slice(0, 60)}`);
643
+ imageCache.set(src, null);
644
+ return null;
645
+ }
646
+ const ext = sniffExt(bytes);
647
+ mediaExts.add(ext);
648
+ const id = `rImg${++relSeq}`;
649
+ const name = `image${relSeq}.${ext}`;
650
+ files[`word/media/${name}`] = bytes;
651
+ rels.push(
652
+ `<Relationship Id="${id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${name}"/>`
653
+ );
654
+ imageCache.set(src, id);
655
+ return id;
656
+ }
657
+ function addInkPart(xmlText) {
658
+ const id = `rInk${++relSeq}`;
659
+ const name = `ink${relSeq}.xml`;
660
+ files[`word/ink/${name}`] = enc.encode(xmlText);
661
+ rels.push(
662
+ `<Relationship Id="${id}" Type="http://schemas.microsoft.com/office/2010/relationships/customXml" Target="ink/${name}"/>`
663
+ );
664
+ return id;
665
+ }
666
+ const numDefs = [];
667
+ function numIdFor(ordered, start) {
668
+ const i = numDefs.findIndex((d) => d.ordered === ordered && d.start === start);
669
+ if (i !== -1) return i + 1;
670
+ numDefs.push({ ordered, start });
671
+ return numDefs.length;
672
+ }
673
+ function runProps(marks = []) {
674
+ const p = [];
675
+ const has = (t) => marks.some((m) => m.type === t);
676
+ if (has("bold")) p.push("<w:b/>");
677
+ if (has("italic")) p.push("<w:i/>");
678
+ if (has("underline")) p.push('<w:u w:val="single"/>');
679
+ if (has("strike")) p.push("<w:strike/>");
680
+ if (has("superscript")) p.push('<w:vertAlign w:val="superscript"/>');
681
+ if (has("subscript")) p.push('<w:vertAlign w:val="subscript"/>');
682
+ const color = marks.find((m) => m.type === "textStyle")?.attrs?.color;
683
+ if (color) p.push(`<w:color w:val="${String(color).replace("#", "")}"/>`);
684
+ const hl = marks.find((m) => m.type === "highlight")?.attrs?.color;
685
+ if (hl) p.push(`<w:shd w:val="clear" w:fill="${String(hl).replace("#", "")}"/>`);
686
+ return p.length ? `<w:rPr>${p.join("")}</w:rPr>` : "";
687
+ }
688
+ function drawing(relId, wPx, hPx, alt, float) {
689
+ const cx = Math.max(1, Math.round(wPx * EMU_PER_PX2));
690
+ const cy = Math.max(1, Math.round(hPx * EMU_PER_PX2));
691
+ const docPr = `<wp:docPr id="${++relSeq}" name="img${relSeq}" descr="${esc(alt)}"/>`;
692
+ const graphic = `<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="0" name=""/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic>`;
693
+ if (!float) {
694
+ return `<w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/>${docPr}${graphic}</wp:inline></w:drawing>`;
695
+ }
696
+ return `<w:drawing><wp:anchor distT="0" distB="0" distL="0" distR="0" simplePos="0" relativeHeight="1" behindDoc="${float.behind ? 1 : 0}" locked="0" layoutInCell="1" allowOverlap="1"><wp:simplePos x="0" y="0"/><wp:positionH relativeFrom="column"><wp:posOffset>${Math.round(float.x * EMU_PER_PX2)}</wp:posOffset></wp:positionH><wp:positionV relativeFrom="paragraph"><wp:posOffset>${Math.round(float.y * EMU_PER_PX2)}</wp:posOffset></wp:positionV><wp:extent cx="${cx}" cy="${cy}"/><wp:wrapNone/>${docPr}${graphic}</wp:anchor></w:drawing>`;
697
+ }
698
+ function inlineToRuns(nodes = []) {
699
+ let out = "";
700
+ for (const n of nodes) {
701
+ if (n.type === "text") {
702
+ const link = (n.marks ?? []).find((m) => m.type === "link");
703
+ const run = `<w:r>${runProps(n.marks)}<w:t xml:space="preserve">${esc(n.text)}</w:t></w:r>`;
704
+ out += link?.attrs?.href ? `<w:hyperlink w:tooltip="${esc(link.attrs.href)}">${run}</w:hyperlink>` : run;
705
+ continue;
706
+ }
707
+ if (n.type === "hardBreak") {
708
+ out += "<w:r><w:br/></w:r>";
709
+ continue;
710
+ }
711
+ if (n.type === "docxField") {
712
+ stats.fields += 1;
713
+ out += `<w:fldSimple w:instr=" ${esc(n.attrs?.code ?? "")} "><w:r><w:t>${esc(n.attrs?.cached ?? "")}</w:t></w:r></w:fldSimple>`;
714
+ continue;
715
+ }
716
+ if (n.type === "docxInk") {
717
+ stats.ink += 1;
718
+ const relImg = n.attrs?.src ? addImage(n.attrs.src) : null;
719
+ const fallback = relImg ? drawing(relImg, n.attrs.width || 100, n.attrs.height || 100, "手写墨迹", {
720
+ x: n.attrs.x ?? 0,
721
+ y: n.attrs.y ?? 0,
722
+ behind: Boolean(n.attrs.behindDoc)
723
+ }) : "";
724
+ if (n.attrs?.strokesXml) {
725
+ const inkRel = addInkPart(n.attrs.strokesXml);
726
+ out += `<mc:AlternateContent><mc:Choice Requires="w14"><w:r><w14:contentPart r:id="${inkRel}"/></w:r></mc:Choice><mc:Fallback><w:r>${fallback}</w:r></mc:Fallback></mc:AlternateContent>`;
727
+ } else if (fallback) {
728
+ warnings.push("墨迹缺少矢量数据(strokesXml),只写回了位图");
729
+ out += `<w:r>${fallback}</w:r>`;
730
+ }
731
+ continue;
732
+ }
733
+ if (n.type === "image") {
734
+ const rel = n.attrs?.src ? addImage(n.attrs.src) : null;
735
+ if (rel) {
736
+ stats.images += 1;
737
+ out += `<w:r>${drawing(rel, n.attrs.width || 400, n.attrs.height || 300, n.attrs.alt ?? "")}</w:r>`;
738
+ }
739
+ continue;
740
+ }
741
+ const t = collectText(n);
742
+ if (t) out += `<w:r><w:t xml:space="preserve">${esc(t)}</w:t></w:r>`;
743
+ else warnings.push(`未知内联节点 "${n.type}",已跳过`);
744
+ }
745
+ return out;
746
+ }
747
+ function paraProps(node, extra = []) {
748
+ const p = [...extra];
749
+ const align = node.attrs?.textAlign ?? node.attrs?.nodeTextAlign;
750
+ if (align && align !== "left") p.push(`<w:jc w:val="${esc(align)}"/>`);
751
+ const bg = node.attrs?.backgroundColor;
752
+ if (bg) p.push(`<w:shd w:val="clear" w:fill="${String(bg).replace("#", "")}"/>`);
753
+ const ind = Number(node.attrs?.indent ?? 0);
754
+ if (ind > 0) p.push(`<w:ind w:left="${ind * 420}"/>`);
755
+ const ov = opts.paragraphOverrides?.(node);
756
+ for (const [k, v] of Object.entries(ov ?? {})) p.push(`<w:${k} w:val="${esc(v)}"/>`);
757
+ return p.length ? `<w:pPr>${p.join("")}</w:pPr>` : "";
758
+ }
759
+ function paragraph(node, extraProps = []) {
760
+ stats.paragraphs += 1;
761
+ return `<w:p>${paraProps(node, extraProps)}${inlineToRuns(node.content)}</w:p>`;
762
+ }
763
+ function heading(node) {
764
+ stats.headings += 1;
765
+ const lv = Math.min(9, Math.max(1, Number(node.attrs?.level ?? 1)));
766
+ return `<w:p>${paraProps(node, [`<w:pStyle w:val="Heading${lv}"/>`])}${inlineToRuns(node.content)}</w:p>`;
767
+ }
768
+ function list(node, level, out) {
769
+ const ordered = node.type === "orderedList";
770
+ if (level === 0) stats.lists += 1;
771
+ const numId = numIdFor(ordered, Number(node.attrs?.start ?? 1));
772
+ for (const item of node.content ?? []) {
773
+ if (item.type !== "listItem") continue;
774
+ stats.listItems += 1;
775
+ let firstDone = false;
776
+ for (const child of item.content ?? []) {
777
+ if (child.type === "bulletList" || child.type === "orderedList") {
778
+ list(child, level + 1, out);
779
+ continue;
780
+ }
781
+ const props = firstDone ? [`<w:ind w:left="${(level + 1) * 720}"/>`] : [`<w:numPr><w:ilvl w:val="${level}"/><w:numId w:val="${numId}"/></w:numPr>`];
782
+ out.push(`<w:p>${paraProps(child, props)}${inlineToRuns(child.content)}</w:p>`);
783
+ firstDone = true;
784
+ }
785
+ if (!firstDone) {
786
+ out.push(`<w:p><w:pPr><w:numPr><w:ilvl w:val="${level}"/><w:numId w:val="${numId}"/></w:numPr></w:pPr></w:p>`);
787
+ }
788
+ }
789
+ }
790
+ function table(node) {
791
+ stats.tables += 1;
792
+ const rows = (node.content ?? []).filter((r) => r.type === "tableRow");
793
+ const occupied = {};
794
+ const grid = [];
795
+ rows.forEach((row, r) => {
796
+ grid[r] = [];
797
+ let c = 0;
798
+ for (const cell of row.content ?? []) {
799
+ while (occupied[r]?.[c]) {
800
+ grid[r].push({ cell: null, colSpan: occupied[r][c] });
801
+ c += occupied[r][c];
802
+ }
803
+ const colSpan = Number(cell.attrs?.colspan ?? 1);
804
+ const rowSpan = Number(cell.attrs?.rowspan ?? 1);
805
+ grid[r].push({ cell, colSpan });
806
+ for (let rr = r + 1; rr < r + rowSpan; rr++) {
807
+ occupied[rr] = occupied[rr] ?? {};
808
+ occupied[rr][c] = colSpan;
809
+ }
810
+ c += colSpan;
811
+ }
812
+ while (occupied[r]?.[c]) {
813
+ grid[r].push({ cell: null, colSpan: occupied[r][c] });
814
+ c += occupied[r][c];
815
+ }
816
+ });
817
+ const widths = [];
818
+ for (const { cell, colSpan } of grid[0] ?? []) {
819
+ const cw = cell?.attrs?.colwidth ?? [];
820
+ for (let i = 0; i < colSpan; i++) widths.push(Math.round((cw[i] ?? 100) * PX_TO_DXA));
821
+ }
822
+ const gridXml = widths.map((w) => `<w:gridCol w:w="${w}"/>`).join("");
823
+ const borders = "<w:tblBorders>" + ["top", "left", "bottom", "right", "insideH", "insideV"].map((s) => `<w:${s} w:val="single" w:sz="4" w:color="auto"/>`).join("") + "</w:tblBorders>";
824
+ let xml = `<w:tbl><w:tblPr><w:tblW w:type="dxa" w:w="${widths.reduce((a, b) => a + b, 0)}"/>${borders}</w:tblPr><w:tblGrid>${gridXml}</w:tblGrid>`;
825
+ grid.forEach((cells, r) => {
826
+ const isHeader = (rows[r].content ?? []).some((c) => c.type === "tableHeader");
827
+ xml += "<w:tr>";
828
+ if (isHeader) xml += "<w:trPr><w:tblHeader/></w:trPr>";
829
+ for (const { cell, colSpan } of cells) {
830
+ const props = [];
831
+ if (colSpan > 1) props.push(`<w:gridSpan w:val="${colSpan}"/>`);
832
+ if (!cell) {
833
+ props.push("<w:vMerge/>");
834
+ xml += `<w:tc><w:tcPr>${props.join("")}</w:tcPr><w:p/></w:tc>`;
835
+ continue;
836
+ }
837
+ if (Number(cell.attrs?.rowspan ?? 1) > 1) props.push('<w:vMerge w:val="restart"/>');
838
+ const bg = cell.attrs?.backgroundColor;
839
+ if (bg) props.push(`<w:shd w:val="clear" w:fill="${String(bg).replace("#", "")}"/>`);
840
+ const va = cell.attrs?.nodeVerticalAlign;
841
+ if (va) props.push(`<w:vAlign w:val="${esc(va)}"/>`);
842
+ const inner = blocks(cell.content ?? []);
843
+ xml += `<w:tc><w:tcPr>${props.join("")}</w:tcPr>${inner || "<w:p/>"}</w:tc>`;
844
+ }
845
+ xml += "</w:tr>";
846
+ });
847
+ return `${xml}</w:tbl>`;
848
+ }
849
+ function blocks(nodes) {
850
+ const out = [];
851
+ for (const n of nodes) {
852
+ switch (n.type) {
853
+ case "heading":
854
+ out.push(heading(n));
855
+ break;
856
+ case "paragraph":
857
+ out.push(paragraph(n));
858
+ break;
859
+ case "bulletList":
860
+ case "orderedList":
861
+ list(n, 0, out);
862
+ break;
863
+ case "table":
864
+ out.push(table(n));
865
+ break;
866
+ case "blockquote":
867
+ for (const c of n.content ?? []) out.push(paragraph(c, ['<w:ind w:left="720"/>']));
868
+ break;
869
+ case "codeBlock":
870
+ out.push(`<w:p><w:pPr><w:pStyle w:val="Code"/></w:pPr><w:r><w:rPr><w:rFonts w:ascii="Consolas" w:hAnsi="Consolas"/></w:rPr><w:t xml:space="preserve">${esc(collectText(n))}</w:t></w:r></w:p>`);
871
+ break;
872
+ case "horizontalRule":
873
+ out.push('<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:color="auto"/></w:pBdr></w:pPr></w:p>');
874
+ break;
875
+ case "image":
876
+ case "docxInk":
877
+ out.push(`<w:p>${inlineToRuns([n])}</w:p>`);
878
+ break;
879
+ default: {
880
+ const t = collectText(n);
881
+ if (t) out.push(`<w:p><w:r><w:t xml:space="preserve">${esc(t)}</w:t></w:r></w:p>`);
882
+ else warnings.push(`未知块级节点 "${n.type}",已跳过`);
883
+ }
884
+ }
885
+ }
886
+ return out.join("");
887
+ }
888
+ const bodyXml = blocks(doc.content ?? []);
889
+ files["word/document.xml"] = enc.encode(
890
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document ${NS}><w:body>${bodyXml}<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr></w:body></w:document>`
891
+ );
892
+ const headingStyles = Array.from({ length: 9 }, (_, i) => {
893
+ const lv = i + 1;
894
+ const sz = [32, 28, 26, 24, 22, 21, 21, 21, 21][i];
895
+ return `<w:style w:type="paragraph" w:styleId="Heading${lv}"><w:name w:val="heading ${lv}"/><w:basedOn w:val="Normal"/><w:pPr><w:outlineLvl w:val="${i}"/><w:spacing w:before="240" w:after="120"/></w:pPr><w:rPr><w:b/><w:sz w:val="${sz}"/></w:rPr></w:style>`;
896
+ }).join("");
897
+ const dflt = opts.styleOverrides ?? {};
898
+ files["word/styles.xml"] = enc.encode(
899
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles ${NS}><w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="${esc(dflt.defaultFont ?? "Calibri")}" w:eastAsia="${esc(dflt.defaultFont ?? "等线")}"/><w:sz w:val="${(dflt.defaultSizePt ?? 11) * 2}"/></w:rPr></w:rPrDefault></w:docDefaults><w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/></w:style><w:style w:type="paragraph" w:styleId="Code"><w:name w:val="Code"/><w:basedOn w:val="Normal"/></w:style>` + headingStyles + `</w:styles>`
900
+ );
901
+ const abstractNums = numDefs.map((d, i) => {
902
+ const levels = Array.from(
903
+ { length: 9 },
904
+ (_, l) => d.ordered ? `<w:lvl w:ilvl="${l}"><w:start w:val="${l === 0 ? d.start : 1}"/><w:numFmt w:val="${["decimal", "lowerLetter", "lowerRoman"][l % 3]}"/><w:lvlText w:val="%${l + 1}."/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="${(l + 1) * 720}" w:hanging="360"/></w:pPr></w:lvl>` : `<w:lvl w:ilvl="${l}"><w:start w:val="1"/><w:numFmt w:val="bullet"/><w:lvlText w:val="•"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="${(l + 1) * 720}" w:hanging="360"/></w:pPr></w:lvl>`
905
+ ).join("");
906
+ return `<w:abstractNum w:abstractNumId="${i}">${levels}</w:abstractNum>`;
907
+ }).join("");
908
+ const nums = numDefs.map((_, i) => `<w:num w:numId="${i + 1}"><w:abstractNumId w:val="${i}"/></w:num>`).join("");
909
+ files["word/numbering.xml"] = enc.encode(
910
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:numbering ${NS}>${abstractNums}${nums}</w:numbering>`
911
+ );
912
+ files["word/_rels/document.xml.rels"] = enc.encode(
913
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${rels.join("")}</Relationships>`
914
+ );
915
+ files["_rels/.rels"] = enc.encode(
916
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`
917
+ );
918
+ const defaults = ["rels", "xml", ...mediaExts];
919
+ files["[Content_Types].xml"] = enc.encode(
920
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` + defaults.map((e) => `<Default Extension="${e}" ContentType="${contentTypeOf(e)}"/>`).join("") + `<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/></Types>`
921
+ );
922
+ return { files, warnings, stats };
923
+ }
924
+ function collectText(n) {
925
+ if (n?.type === "text") return n.text ?? "";
926
+ return (n?.content ?? []).map(collectText).join("");
927
+ }
928
+ function sniffExt(b) {
929
+ if (b[0] === 137 && b[1] === 80) return "png";
930
+ if (b[0] === 255 && b[1] === 216) return "jpeg";
931
+ if (b[0] === 71 && b[1] === 73) return "gif";
932
+ if (b[8] === 87 && b[9] === 69) return "webp";
933
+ return "png";
934
+ }
935
+ function contentTypeOf(ext) {
936
+ return {
937
+ rels: "application/vnd.openxmlformats-package.relationships+xml",
938
+ xml: "application/xml",
939
+ png: "image/png",
940
+ jpeg: "image/jpeg",
941
+ gif: "image/gif",
942
+ webp: "image/webp"
943
+ }[ext] ?? "application/octet-stream";
944
+ }
945
+
946
+ // src/docx/export.ts
947
+ function docxFromJSON(doc, opts = {}) {
948
+ const { files, warnings, stats } = serializeDocx(doc, opts);
949
+ return { bytes: zipSync(files, { level: 6 }), warnings, stats };
950
+ }
951
+ function deliver(bytes, type) {
952
+ if (type === "buffer") return bytes;
953
+ if (type === "string") {
954
+ let bin = "";
955
+ for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
956
+ return typeof btoa === "function" ? btoa(bin) : Buffer.from(bytes).toString("base64");
957
+ }
958
+ return new Blob([bytes], {
959
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
960
+ });
961
+ }
962
+ var ExportDocx = Extension.create({
963
+ name: "exportDocx",
964
+ addOptions() {
965
+ return {
966
+ onCompleteExport: null,
967
+ exportType: "blob",
968
+ verbose: 0,
969
+ resolveImage: void 0,
970
+ styleOverrides: void 0,
971
+ paragraphOverrides: void 0
972
+ };
973
+ },
974
+ addCommands() {
975
+ return {
976
+ exportDocx: (override = {}) => ({ editor }) => {
977
+ const o = { ...this.options, ...override };
978
+ try {
979
+ const { bytes, warnings, stats } = docxFromJSON(editor.getJSON(), o);
980
+ if (o.verbose >= 1) console.info("[exportDocx] 统计", stats);
981
+ if (o.verbose >= 2) for (const w of warnings) console.warn("[exportDocx]", w);
982
+ o.onCompleteExport?.(deliver(bytes, o.exportType));
983
+ } catch (e) {
984
+ if (o.verbose >= 1) console.error("[exportDocx] 失败", e);
985
+ return false;
986
+ }
987
+ return true;
988
+ }
989
+ };
990
+ }
991
+ });
992
+ function downloadDocx(editor, filename = "document.docx", opts = {}) {
993
+ const { bytes } = docxFromJSON(editor.getJSON(), opts);
994
+ const url = URL.createObjectURL(
995
+ new Blob([bytes], {
996
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
997
+ })
998
+ );
999
+ const a = document.createElement("a");
1000
+ a.href = url;
1001
+ a.download = filename;
1002
+ a.click();
1003
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
1004
+ }
1005
+
1006
+ // src/docx/index.ts
1007
+ var FLOATING_CSS = `
1008
+ .tiptap p:has(> .docx-ink),
1009
+ .tiptap h1:has(> .docx-ink), .tiptap h2:has(> .docx-ink),
1010
+ .tiptap h3:has(> .docx-ink), .tiptap h4:has(> .docx-ink),
1011
+ .tiptap h5:has(> .docx-ink), .tiptap h6:has(> .docx-ink) { position: relative; }
1012
+ .docx-ink { display: inline-block; }
1013
+ .docx-ink-placeholder { opacity: .4; }
1014
+ .docx-field {
1015
+ background: color-mix(in srgb, currentColor 8%, transparent);
1016
+ border-radius: 3px; padding: 0 2px; cursor: default;
1017
+ }
1018
+ `;
1019
+ var STYLE_ID = "tiptap-import-docx-styles";
1020
+ function injectStyles(extra) {
1021
+ if (typeof document === "undefined") return;
1022
+ let el = document.getElementById(STYLE_ID);
1023
+ if (!el) {
1024
+ el = document.createElement("style");
1025
+ el.id = STYLE_ID;
1026
+ document.head.appendChild(el);
1027
+ }
1028
+ el.textContent = FLOATING_CSS + (extra ? `
1029
+ ${extra}` : "");
1030
+ }
1031
+ function degradeToSchema(node, schema, warned, warnings) {
1032
+ const type = node.type;
1033
+ const kids = node.content ?? [];
1034
+ const degradedKids = kids.flatMap((c) => degradeToSchema(c, schema, warned, warnings));
1035
+ if (type && type !== "text" && !schema.nodes[type]) {
1036
+ if (!warned.has(`node:${type}`)) {
1037
+ warned.add(`node:${type}`);
1038
+ warnings.push(`宿主 schema 没有节点类型 "${type}",已降级为其子内容(宿主需注册对应扩展)`);
1039
+ }
1040
+ return degradedKids;
1041
+ }
1042
+ const out = { ...node };
1043
+ if (Array.isArray(node.marks)) {
1044
+ out.marks = node.marks.filter((m) => {
1045
+ const mt = m.type;
1046
+ if (schema.marks[mt]) return true;
1047
+ if (!warned.has(`mark:${mt}`)) {
1048
+ warned.add(`mark:${mt}`);
1049
+ warnings.push(`宿主 schema 没有标记类型 "${mt}",已摘除`);
1050
+ }
1051
+ return false;
1052
+ });
1053
+ }
1054
+ if (kids.length) out.content = degradedKids;
1055
+ return [out];
1056
+ }
1057
+ async function toBytes(file) {
1058
+ if (file instanceof Uint8Array) return file;
1059
+ if (file instanceof ArrayBuffer) return new Uint8Array(file);
1060
+ return new Uint8Array(await file.arrayBuffer());
1061
+ }
1062
+ async function uploadImages(files, cfg) {
1063
+ const out = /* @__PURE__ */ new Map();
1064
+ const entries = Object.keys(files).filter((p) => p.startsWith("word/media/"));
1065
+ await Promise.all(
1066
+ entries.map(async (entry) => {
1067
+ const url = new URL(cfg.url, typeof location === "undefined" ? "http://localhost" : location.href);
1068
+ for (const [k, v] of Object.entries(cfg.queryParams ?? {})) url.searchParams.set(k, v);
1069
+ const body = new FormData();
1070
+ body.append("file", new Blob([files[entry]]), entry.split("/").pop() ?? "image");
1071
+ const res = await fetch(url.toString(), {
1072
+ method: cfg.method ?? "POST",
1073
+ headers: cfg.headers,
1074
+ body
1075
+ });
1076
+ const raw = res.headers.get("content-type")?.includes("json") ? await res.json() : await res.text();
1077
+ const resolved = cfg.extractUrl ? cfg.extractUrl(raw) : typeof raw === "string" ? raw : raw?.url ?? raw?.data?.url ?? raw?.src ?? "";
1078
+ if (resolved) out.set(entry, resolved);
1079
+ })
1080
+ );
1081
+ return out;
1082
+ }
1083
+ var ImportDocx = Extension.create({
1084
+ name: "importDocx",
1085
+ addOptions() {
1086
+ return {
1087
+ imageUploadConfig: null,
1088
+ resolveMedia: null,
1089
+ prosemirrorNodes: null,
1090
+ prosemirrorMarks: null,
1091
+ cssStyles: false,
1092
+ verbose: 0,
1093
+ preserveUnsupported: true,
1094
+ injectStyles: true
1095
+ };
1096
+ },
1097
+ addExtensions() {
1098
+ return this.options.preserveUnsupported === false ? [] : [DocxInk, DocxField];
1099
+ },
1100
+ onCreate() {
1101
+ if (this.options.injectStyles) injectStyles();
1102
+ },
1103
+ addCommands() {
1104
+ return {
1105
+ importDocx: (args) => ({ editor }) => {
1106
+ const opts = this.options;
1107
+ void (async () => {
1108
+ const finish = (ctx) => {
1109
+ if (args.onImport) args.onImport(ctx);
1110
+ else if (ctx.content) ctx.setEditorContent(ctx.content);
1111
+ };
1112
+ const blank = (error) => ({
1113
+ error,
1114
+ content: null,
1115
+ setEditorContent: () => {
1116
+ },
1117
+ recovered: {},
1118
+ warnings: [],
1119
+ header: null,
1120
+ footer: null,
1121
+ footnotes: {},
1122
+ endnotes: {}
1123
+ });
1124
+ try {
1125
+ const bytes = await toBytes(args.file);
1126
+ const files = unzipSync(bytes);
1127
+ let uploaded = null;
1128
+ if (!opts.resolveMedia && opts.imageUploadConfig) {
1129
+ uploaded = await uploadImages(files, opts.imageUploadConfig);
1130
+ }
1131
+ const result = parseDocx(
1132
+ { read: (p) => files[p] ?? null },
1133
+ {
1134
+ DOMParser: globalThis.DOMParser,
1135
+ resolveMedia: opts.resolveMedia ?? (uploaded ? (_b, entry) => uploaded.get(entry) ?? "" : void 0),
1136
+ prosemirrorNodes: opts.prosemirrorNodes ?? void 0,
1137
+ prosemirrorMarks: opts.prosemirrorMarks ?? void 0,
1138
+ cssStyles: opts.cssStyles
1139
+ }
1140
+ );
1141
+ const warned = /* @__PURE__ */ new Set();
1142
+ const degraded = degradeToSchema(
1143
+ result.doc,
1144
+ editor.schema,
1145
+ warned,
1146
+ result.warnings
1147
+ )[0] ?? { type: "doc", content: [] };
1148
+ result.doc = degraded;
1149
+ if (opts.cssStyles && result.css && opts.injectStyles) injectStyles(result.css);
1150
+ if (opts.verbose >= 1) {
1151
+ console.info("[importDocx] 恢复项", result.recovered);
1152
+ }
1153
+ if (opts.verbose >= 2) {
1154
+ for (const w of result.warnings) console.warn("[importDocx]", w);
1155
+ }
1156
+ finish({
1157
+ content: result.doc,
1158
+ setEditorContent: (c) => editor.commands.setContent(c ?? result.doc, { emitUpdate: true }),
1159
+ recovered: result.recovered,
1160
+ warnings: result.warnings,
1161
+ css: result.css,
1162
+ header: null,
1163
+ footer: null,
1164
+ footnotes: {},
1165
+ endnotes: {}
1166
+ });
1167
+ } catch (e) {
1168
+ const err = e instanceof Error ? e : new Error(String(e));
1169
+ if (opts.verbose >= 1) console.error("[importDocx] 失败", err);
1170
+ finish(blank(err));
1171
+ }
1172
+ })();
1173
+ return true;
1174
+ }
1175
+ };
1176
+ }
1177
+ });
1178
+
1179
+ export { DocxField, DocxInk, ExportDocx, ImportDocx, docxFromJSON, downloadDocx, parseDocx, serializeDocx };
1180
+ //# sourceMappingURL=docx.js.map
1181
+ //# sourceMappingURL=docx.js.map