@4399ywkf/editor 0.1.2 → 0.3.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 CHANGED
@@ -2,6 +2,20 @@ import { Node, mergeAttributes, Extension } from '@tiptap/core';
2
2
  import { unzipSync, zipSync } from 'fflate';
3
3
 
4
4
  // src/docx/index.ts
5
+
6
+ // src/docx/measure.ts
7
+ function measureContentWidth(dom) {
8
+ const el = dom;
9
+ if (!el || typeof globalThis.getComputedStyle !== "function") return void 0;
10
+ const cs = globalThis.getComputedStyle(el);
11
+ const w = el.clientWidth - (Number.parseFloat(cs.paddingLeft) || 0) - (Number.parseFloat(cs.paddingRight) || 0);
12
+ return Number.isFinite(w) && w > 0 ? w : void 0;
13
+ }
14
+ function tableCellMinWidth(editor) {
15
+ const ext = editor.extensionManager?.extensions?.find((e) => e.name === "table");
16
+ const v = ext?.options?.cellMinWidth;
17
+ return typeof v === "number" && v > 0 ? v : void 0;
18
+ }
5
19
  var DocxField = Node.create({
6
20
  name: "docxField",
7
21
  group: "inline",
@@ -86,12 +100,34 @@ var DocxInk = Node.create({
86
100
 
87
101
  // src/docx/parse.ts
88
102
  var EMU_PER_PX = 9525;
103
+ var HIGHLIGHT = {
104
+ black: "#000000",
105
+ blue: "#0000FF",
106
+ cyan: "#00FFFF",
107
+ green: "#008000",
108
+ magenta: "#FF00FF",
109
+ red: "#FF0000",
110
+ yellow: "#FFFF00",
111
+ white: "#FFFFFF",
112
+ darkBlue: "#000080",
113
+ darkCyan: "#008080",
114
+ darkGreen: "#006400",
115
+ darkMagenta: "#800080",
116
+ darkRed: "#800000",
117
+ darkYellow: "#808000",
118
+ darkGray: "#808080",
119
+ lightGray: "#C0C0C0"
120
+ };
89
121
  var decoder = new TextDecoder("utf-8");
90
122
  function parseDocx(entries, opts) {
91
123
  const { DOMParser } = opts;
92
124
  const recovered = {
93
125
  styleFormat: 0,
94
126
  directFormat: 0,
127
+ charStyles: 0,
128
+ fontSizes: 0,
129
+ fontFamilies: 0,
130
+ highlights: 0,
95
131
  fields: 0,
96
132
  ink: 0,
97
133
  floatImages: 0,
@@ -102,6 +138,9 @@ function parseDocx(entries, opts) {
102
138
  tableCells: 0,
103
139
  mergedCells: 0,
104
140
  cellShading: 0,
141
+ paraShading: 0,
142
+ paraSpacing: 0,
143
+ paraIndent: 0,
105
144
  lists: 0,
106
145
  listItems: 0
107
146
  };
@@ -125,6 +164,7 @@ function parseDocx(entries, opts) {
125
164
  }
126
165
  const STYLE_NAME = {};
127
166
  const STYLE_FMT = {};
167
+ const STYLE_PPR = {};
128
168
  const STYLE_BASE = {};
129
169
  const stylesDoc = xml("word/styles.xml");
130
170
  if (stylesDoc) {
@@ -134,9 +174,10 @@ function parseDocx(entries, opts) {
134
174
  const name = s.getElementsByTagName("w:name")[0]?.getAttribute("w:val");
135
175
  if (name) STYLE_NAME[id] = name;
136
176
  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
- );
177
+ const own = Array.from(s.childNodes).filter((c) => c.nodeType === 1);
178
+ const pPr = own.find((c) => c.nodeName === "w:pPr");
179
+ if (pPr) STYLE_PPR[id] = pPr;
180
+ const rPr = own.find((c) => c.nodeName === "w:rPr");
140
181
  if (!rPr) continue;
141
182
  STYLE_FMT[id] = readFmt(rPr);
142
183
  }
@@ -146,15 +187,29 @@ function parseDocx(entries, opts) {
146
187
  const e = rPr.getElementsByTagName(t)[0];
147
188
  if (!e) return void 0;
148
189
  const v = e.getAttribute("w:val");
149
- return v !== "0" && v !== "false" && v !== "none" ? true : void 0;
190
+ return v === "0" || v === "false" || v === "none" || v === "off" ? false : true;
150
191
  };
151
192
  const color = rPr.getElementsByTagName("w:color")[0]?.getAttribute("w:val");
193
+ const szRaw = rPr.getElementsByTagName("w:sz")[0]?.getAttribute("w:val");
194
+ const szNum = szRaw ? Number(szRaw) : Number.NaN;
195
+ const size = Number.isFinite(szNum) && szNum > 0 ? `${szNum / 2}pt` : void 0;
196
+ const rf = rPr.getElementsByTagName("w:rFonts")[0];
197
+ const stack = [rf?.getAttribute("w:ascii"), rf?.getAttribute("w:hAnsi"), rf?.getAttribute("w:eastAsia")].filter((n) => Boolean(n)).filter((n, i, a) => a.indexOf(n) === i).map((n) => /[\s,]/.test(n) ? `"${n}"` : n);
198
+ const font = stack.length ? stack.join(", ") : void 0;
199
+ const hlName = rPr.getElementsByTagName("w:highlight")[0]?.getAttribute("w:val");
200
+ const shdFill = rPr.getElementsByTagName("w:shd")[0]?.getAttribute("w:fill");
201
+ const highlight = hlName && hlName !== "none" ? HIGHLIGHT[hlName] ?? void 0 : shdFill && shdFill !== "auto" && shdFill !== "FFFFFF" ? `#${shdFill}` : void 0;
202
+ const va = rPr.getElementsByTagName("w:vertAlign")[0]?.getAttribute("w:val");
152
203
  return {
153
204
  b: on("w:b"),
154
205
  i: on("w:i"),
155
206
  u: on("w:u"),
156
207
  strike: on("w:strike"),
157
- color: color && color !== "auto" ? `#${color}` : void 0
208
+ color: color && color !== "auto" ? `#${color}` : void 0,
209
+ size,
210
+ font,
211
+ highlight,
212
+ vertAlign: va === "superscript" || va === "subscript" ? va : void 0
158
213
  };
159
214
  }
160
215
  function resolveStyleFmt(id, seen = /* @__PURE__ */ new Set()) {
@@ -172,7 +227,22 @@ function parseDocx(entries, opts) {
172
227
  if (f.i) marks.push({ type: "italic" });
173
228
  if (f.u) marks.push({ type: "underline" });
174
229
  if (f.strike) marks.push({ type: "strike" });
175
- if (f.color) marks.push({ type: "textStyle", attrs: { color: f.color } });
230
+ const ts = {};
231
+ if (f.color) ts.color = f.color;
232
+ if (f.size) {
233
+ ts.fontSize = f.size;
234
+ recovered.fontSizes += 1;
235
+ }
236
+ if (f.font) {
237
+ ts.fontFamily = f.font;
238
+ recovered.fontFamilies += 1;
239
+ }
240
+ if (Object.keys(ts).length) marks.push({ type: "textStyle", attrs: ts });
241
+ if (f.highlight) {
242
+ marks.push({ type: "highlight", attrs: { color: f.highlight } });
243
+ recovered.highlights += 1;
244
+ }
245
+ if (f.vertAlign) marks.push({ type: f.vertAlign });
176
246
  return marks;
177
247
  }
178
248
  const mediaCache = /* @__PURE__ */ new Map();
@@ -192,6 +262,7 @@ function parseDocx(entries, opts) {
192
262
  }
193
263
  const docXml = xml("word/document.xml");
194
264
  if (!docXml) throw new Error("这不是一个有效的 .docx:缺少 word/document.xml");
265
+ const documentXml = docXml;
195
266
  const kids = (n) => Array.from(n.childNodes).filter((c) => c.nodeType === 1);
196
267
  const first = (n, name) => n.getElementsByTagName(name)[0] ?? null;
197
268
  function readDrawing(dr) {
@@ -264,7 +335,14 @@ function parseDocx(entries, opts) {
264
335
  const rPr = first(r, "w:rPr");
265
336
  const direct = rPr ? readFmt(rPr) : {};
266
337
  if (rPr && Object.values(direct).some((v) => v !== void 0)) recovered.directFormat += 1;
267
- const marks = fmtToMarks({ ...inherited, ...stripUndefined(direct) });
338
+ const rStyleId = rPr ? rPr.getElementsByTagName("w:rStyle")[0]?.getAttribute("w:val") ?? null : null;
339
+ const charFmt = rStyleId ? resolveStyleFmt(rStyleId) : {};
340
+ if (rStyleId && Object.values(charFmt).some((v) => v !== void 0)) recovered.charStyles += 1;
341
+ const marks = fmtToMarks({
342
+ ...inherited,
343
+ ...stripUndefined(charFmt),
344
+ ...stripUndefined(direct)
345
+ });
268
346
  for (const c of kids(r)) {
269
347
  const t = c.nodeName;
270
348
  if (t === "w:t") {
@@ -340,17 +418,63 @@ function parseDocx(entries, opts) {
340
418
  }
341
419
  }
342
420
  }
421
+ function paraLayout(pPr, count = true) {
422
+ const attrs = {};
423
+ const jc = first(pPr, "w:jc")?.getAttribute("w:val");
424
+ if (jc) attrs.textAlign = jc === "both" ? "justify" : jc;
425
+ const fill = first(pPr, "w:shd")?.getAttribute("w:fill");
426
+ if (fill && fill !== "auto" && fill !== "FFFFFF") {
427
+ attrs.backgroundColor = `#${fill}`;
428
+ if (count) recovered.paraShading += 1;
429
+ }
430
+ const ind = first(pPr, "w:ind");
431
+ const left = Number(ind?.getAttribute("w:left") ?? ind?.getAttribute("w:start") ?? 0);
432
+ if (left > 0) {
433
+ attrs.indent = Math.min(8, Math.max(1, Math.round(left / 420)));
434
+ if (count) recovered.paraIndent += 1;
435
+ }
436
+ const sp = first(pPr, "w:spacing");
437
+ if (sp) {
438
+ let touched = false;
439
+ const line = Number(sp.getAttribute("w:line") ?? 0);
440
+ if (line > 0) {
441
+ const rule = sp.getAttribute("w:lineRule") ?? "auto";
442
+ attrs.lineHeight = rule === "auto" ? String(Math.round(line / 240 * 100) / 100) : `${line / 20}pt`;
443
+ touched = true;
444
+ }
445
+ const before = sp.getAttribute("w:before");
446
+ if (before != null && sp.getAttribute("w:beforeAutospacing") !== "1") {
447
+ attrs.spaceBefore = `${Number(before) / 20}pt`;
448
+ touched = true;
449
+ }
450
+ const after = sp.getAttribute("w:after");
451
+ if (after != null && sp.getAttribute("w:afterAutospacing") !== "1") {
452
+ attrs.spaceAfter = `${Number(after) / 20}pt`;
453
+ touched = true;
454
+ }
455
+ if (touched && count) recovered.paraSpacing += 1;
456
+ }
457
+ return attrs;
458
+ }
459
+ function resolveStylePara(id, seen = /* @__PURE__ */ new Set()) {
460
+ if (!id || seen.has(id)) return {};
461
+ seen.add(id);
462
+ const pPr = STYLE_PPR[id];
463
+ return {
464
+ ...resolveStylePara(STYLE_BASE[id] ?? null, seen),
465
+ ...pPr ? paraLayout(pPr, false) : {}
466
+ };
467
+ }
343
468
  function paragraph(p) {
344
469
  const pPr = first(p, "w:pPr");
345
470
  const styleId = pPr ? first(pPr, "w:pStyle")?.getAttribute("w:val") ?? null : null;
346
471
  const styleName = styleId ? STYLE_NAME[styleId] ?? styleId : null;
347
- const jc = pPr ? first(pPr, "w:jc")?.getAttribute("w:val") : null;
348
472
  const styleFmt = resolveStyleFmt(styleId);
349
473
  if (Object.values(styleFmt).some((v) => v !== void 0)) recovered.styleFormat += 1;
350
474
  const out = { inline: [], blocks: [] };
351
475
  childrenInto(p, out, styleFmt);
352
- const attrs = {};
353
- if (jc) attrs.textAlign = jc;
476
+ const attrs = { ...resolveStylePara(styleId) };
477
+ if (pPr) Object.assign(attrs, paraLayout(pPr));
354
478
  const heading = /^heading\s*(\d)$/i.exec(styleName ?? "");
355
479
  const blocks = [];
356
480
  const hasContent = out.inline.length > 0;
@@ -367,6 +491,45 @@ function parseDocx(entries, opts) {
367
491
  return blocks;
368
492
  }
369
493
  const dxaToPx = (v) => v ? Math.round(Number(v) / 15) : null;
494
+ const sectionTextWidthPx = (() => {
495
+ const sects = documentXml.getElementsByTagName("w:sectPr");
496
+ const sect = sects[sects.length - 1];
497
+ if (!sect) return null;
498
+ const num = (tag, attr) => Number(sect.getElementsByTagName(tag)[0]?.getAttribute(attr) ?? Number.NaN);
499
+ const text2 = num("w:pgSz", "w:w") - num("w:pgMar", "w:left") - num("w:pgMar", "w:right");
500
+ return Number.isFinite(text2) && text2 > 0 ? text2 / 15 : null;
501
+ })();
502
+ function fitColumns(pxWidths) {
503
+ const basis = opts.contentWidthPx;
504
+ const usable = typeof basis === "number" && Number.isFinite(basis) && basis > 0 && sectionTextWidthPx !== null && sectionTextWidthPx > 0;
505
+ const scale = usable ? basis / sectionTextWidthPx : 1;
506
+ let out = pxWidths.map((w) => w * scale);
507
+ const min = opts.minColumnWidthPx ?? 0;
508
+ if (min > 0) {
509
+ const total = out.reduce((a, b) => a + b, 0);
510
+ const pinned = out.map(() => false);
511
+ for (let round = 0; round < out.length; round++) {
512
+ const freeTotal = total - out.reduce((a, w, i) => a + (pinned[i] ? min : 0), 0);
513
+ const freeSum = out.reduce((a, w, i) => a + (pinned[i] ? 0 : w), 0);
514
+ if (freeSum <= 0 || freeTotal <= 0) break;
515
+ const k = freeTotal / freeSum;
516
+ const short = out.map((w, i) => !pinned[i] && w * k < min);
517
+ if (!short.some(Boolean)) {
518
+ out = out.map((w, i) => pinned[i] ? min : w * k);
519
+ break;
520
+ }
521
+ short.forEach((s, i) => {
522
+ if (s) pinned[i] = true;
523
+ });
524
+ }
525
+ out = out.map((w, i) => pinned[i] ? Math.max(w, min) : w);
526
+ }
527
+ const target = Math.round(out.reduce((a, b) => a + b, 0));
528
+ const rounded = out.map((w) => Math.round(w));
529
+ const drift = target - rounded.reduce((a, b) => a + b, 0);
530
+ if (drift !== 0 && rounded.length) rounded[rounded.length - 1] += drift;
531
+ return rounded.map((w) => Math.max(1, w));
532
+ }
370
533
  function readRows(tbl) {
371
534
  const rows = [];
372
535
  for (const tr of kids(tbl)) {
@@ -404,7 +567,7 @@ function parseDocx(entries, opts) {
404
567
  const grid = first(tbl, "w:tblGrid");
405
568
  const rawWidths = grid ? kids(grid).filter((g) => g.nodeName === "w:gridCol").map((g) => dxaToPx(g.getAttribute("w:w"))) : [];
406
569
  const sane = rawWidths.length > 0 && rawWidths.every((w) => w != null && w >= 20);
407
- const colWidths = useAbsoluteWidths && sane ? rawWidths : [];
570
+ const colWidths = useAbsoluteWidths && sane ? fitColumns(rawWidths) : [];
408
571
  if (rawWidths.length > 0 && colWidths.length === 0) {
409
572
  warnings.push(
410
573
  `表格列宽未采信(tblW type=${tblWType ?? "未指定"}),改为自动分配 —— gridCol 原值 ${rawWidths.slice(0, 4).join("/")}`
@@ -543,12 +706,89 @@ function parseDocx(entries, opts) {
543
706
  if (!body) throw new Error("docx 缺少 w:body");
544
707
  const content = blocksOf(body);
545
708
  const doc = remapTypes({ type: "doc", content }, opts.prosemirrorNodes, opts.prosemirrorMarks);
709
+ const section = readSection();
710
+ const defaultHeader = section.refs.find((r) => r.kind === "header" && r.type === "default");
711
+ const defaultFooter = section.refs.find((r) => r.kind === "footer" && r.type === "default");
546
712
  return {
547
713
  doc,
548
714
  recovered,
549
715
  warnings,
550
- ...opts.cssStyles ? { css: extractCss(stylesDoc) } : {}
716
+ ...opts.cssStyles ? { css: extractCss(stylesDoc) } : {},
717
+ header: defaultHeader ? partContent(defaultHeader.path, "w:hdr") : null,
718
+ footer: defaultFooter ? partContent(defaultFooter.path, "w:ftr") : null,
719
+ section
551
720
  };
721
+ function partContent(path, root) {
722
+ const d = xml(path);
723
+ const el = d?.getElementsByTagName(root)[0];
724
+ if (!el) return null;
725
+ return remapTypes(
726
+ { type: "doc", content: blocksOf(el) },
727
+ opts.prosemirrorNodes,
728
+ opts.prosemirrorMarks
729
+ );
730
+ }
731
+ function resolveRelative(fromPart, target) {
732
+ const segs = fromPart.split("/").slice(0, -1);
733
+ for (const s of target.split("/")) {
734
+ if (s === "" || s === ".") continue;
735
+ if (s === "..") segs.pop();
736
+ else segs.push(s);
737
+ }
738
+ return segs.join("/");
739
+ }
740
+ function collectPart(path, into) {
741
+ const bytes = entries.read(path);
742
+ if (!bytes) {
743
+ warnings.push(`页眉/页脚部件缺失,已跳过:${path}`);
744
+ return false;
745
+ }
746
+ into[path] = bytes;
747
+ const relsPath = path.replace(/([^/]+)$/, "_rels/$1.rels");
748
+ const relBytes = entries.read(relsPath);
749
+ if (!relBytes) return true;
750
+ into[relsPath] = relBytes;
751
+ const relsDom = new DOMParser().parseFromString(decoder.decode(relBytes), "text/xml");
752
+ for (const r of Array.from(relsDom.getElementsByTagName("Relationship"))) {
753
+ if (r.getAttribute("TargetMode") === "External") continue;
754
+ const target = r.getAttribute("Target");
755
+ if (!target || /^[a-z]+:\/\//i.test(target)) continue;
756
+ const dep = resolveRelative(path, target);
757
+ const b = entries.read(dep);
758
+ if (b) into[dep] = b;
759
+ }
760
+ return true;
761
+ }
762
+ function readSection() {
763
+ const files = {};
764
+ const refs = [];
765
+ const sects = documentXml.getElementsByTagName("w:sectPr");
766
+ const sect = sects[sects.length - 1];
767
+ if (sect) {
768
+ for (const kind of ["header", "footer"]) {
769
+ for (const ref of Array.from(sect.getElementsByTagName(`w:${kind}Reference`))) {
770
+ const rid = ref.getAttribute("r:id");
771
+ const target = rid ? REL[rid] : void 0;
772
+ if (!rid || !target) continue;
773
+ const path = `word/${target.replace(/^\.\//, "")}`;
774
+ if (!collectPart(path, files)) continue;
775
+ refs.push({ kind, type: ref.getAttribute("w:type") ?? "default", rid, path });
776
+ }
777
+ }
778
+ }
779
+ return {
780
+ sectPrXml: rawSectPr(),
781
+ refs,
782
+ files,
783
+ settingsXml: text("word/settings.xml")
784
+ };
785
+ }
786
+ function rawSectPr() {
787
+ const src = text("word/document.xml");
788
+ if (!src) return null;
789
+ const all = src.match(/<w:sectPr\b[^>]*(?:\/>|>[\s\S]*?<\/w:sectPr>)/g);
790
+ return all?.length ? all[all.length - 1] : null;
791
+ }
552
792
  function extractCss(sd) {
553
793
  if (!sd) return "";
554
794
  const defRPr = sd.getElementsByTagName("w:rPrDefault")[0]?.getElementsByTagName("w:rPr")[0];
@@ -635,6 +875,52 @@ function serializeDocx(doc, opts = {}) {
635
875
  const mediaExts = /* @__PURE__ */ new Set();
636
876
  let relSeq = 0;
637
877
  const imageCache = /* @__PURE__ */ new Map();
878
+ const carried = opts.section?.files ?? {};
879
+ for (const [p, b] of Object.entries(carried)) files[p] = b;
880
+ const pageTextWidthDxa = (() => {
881
+ const raw = opts.section?.sectPrXml;
882
+ const attr = (tag, name) => {
883
+ const el = raw ? new RegExp(`<w:${tag}\\b[^>]*>`).exec(raw)?.[0] : null;
884
+ const m = el ? new RegExp(`w:${name}="(\\d+)"`).exec(el) : null;
885
+ return m ? Number(m[1]) : null;
886
+ };
887
+ const w = attr("pgSz", "w");
888
+ const l = attr("pgMar", "left");
889
+ const r = attr("pgMar", "right");
890
+ const text = w !== null && l !== null && r !== null ? w - l - r : null;
891
+ return text !== null && text > 0 ? text : 11906 - 1440 - 1440;
892
+ })();
893
+ function columnsToDxa(px) {
894
+ const basis = opts.contentWidthPx;
895
+ const clamp = (arr) => {
896
+ const total = arr.reduce((a, b) => a + b, 0);
897
+ if (total <= pageTextWidthDxa || total <= 0) return arr;
898
+ const k = pageTextWidthDxa / total;
899
+ return arr.map((w) => Math.max(1, Math.round(w * k)));
900
+ };
901
+ if (typeof basis === "number" && Number.isFinite(basis) && basis > 0) {
902
+ const k = pageTextWidthDxa / basis;
903
+ return clamp(px.map((w) => Math.max(1, Math.round(w * k))));
904
+ }
905
+ return clamp(px.map((w) => Math.max(1, Math.round(w * PX_TO_DXA))));
906
+ }
907
+ const ridMap = /* @__PURE__ */ new Map();
908
+ const hdrFtrOverrides = [];
909
+ for (const ref of opts.section?.refs ?? []) {
910
+ if (!files[ref.path]) continue;
911
+ const id = `rHF${ridMap.size + 1}`;
912
+ ridMap.set(ref.rid, id);
913
+ rels.push(
914
+ `<Relationship Id="${id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/${ref.kind}" Target="${esc(ref.path.replace(/^word\//, ""))}"/>`
915
+ );
916
+ hdrFtrOverrides.push(
917
+ `<Override PartName="/${esc(ref.path)}" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.${ref.kind}+xml"/>`
918
+ );
919
+ }
920
+ for (const p of Object.keys(carried)) {
921
+ const ext = /\.([a-z0-9]+)$/i.exec(p)?.[1]?.toLowerCase();
922
+ if (ext && ext !== "xml" && ext !== "rels") mediaExts.add(ext);
923
+ }
638
924
  function addImage(src) {
639
925
  if (imageCache.has(src)) return imageCache.get(src) ?? null;
640
926
  const bytes = opts.resolveImage?.(src) ?? null;
@@ -646,7 +932,9 @@ function serializeDocx(doc, opts = {}) {
646
932
  const ext = sniffExt(bytes);
647
933
  mediaExts.add(ext);
648
934
  const id = `rImg${++relSeq}`;
649
- const name = `image${relSeq}.${ext}`;
935
+ let n = relSeq;
936
+ while (files[`word/media/image${n}.${ext}`]) n += 1;
937
+ const name = `image${n}.${ext}`;
650
938
  files[`word/media/${name}`] = bytes;
651
939
  rels.push(
652
940
  `<Relationship Id="${id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${name}"/>`
@@ -673,16 +961,25 @@ function serializeDocx(doc, opts = {}) {
673
961
  function runProps(marks = []) {
674
962
  const p = [];
675
963
  const has = (t) => marks.some((m) => m.type === t);
964
+ const ts = marks.find((m) => m.type === "textStyle")?.attrs ?? {};
965
+ const fam = cssFontStack(ts.fontFamily);
966
+ if (fam) {
967
+ p.push(
968
+ `<w:rFonts w:ascii="${esc(fam.ascii)}" w:hAnsi="${esc(fam.ascii)}" w:eastAsia="${esc(fam.eastAsia)}" w:cs="${esc(fam.ascii)}"/>`
969
+ );
970
+ }
676
971
  if (has("bold")) p.push("<w:b/>");
677
972
  if (has("italic")) p.push("<w:i/>");
678
- if (has("underline")) p.push('<w:u w:val="single"/>');
679
973
  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;
974
+ const color = ts.color;
683
975
  if (color) p.push(`<w:color w:val="${String(color).replace("#", "")}"/>`);
976
+ const halfPt = cssToHalfPoints(ts.fontSize);
977
+ if (halfPt) p.push(`<w:sz w:val="${halfPt}"/><w:szCs w:val="${halfPt}"/>`);
978
+ if (has("underline")) p.push('<w:u w:val="single"/>');
684
979
  const hl = marks.find((m) => m.type === "highlight")?.attrs?.color;
685
980
  if (hl) p.push(`<w:shd w:val="clear" w:fill="${String(hl).replace("#", "")}"/>`);
981
+ if (has("superscript")) p.push('<w:vertAlign w:val="superscript"/>');
982
+ if (has("subscript")) p.push('<w:vertAlign w:val="subscript"/>');
686
983
  return p.length ? `<w:rPr>${p.join("")}</w:rPr>` : "";
687
984
  }
688
985
  function drawing(relId, wPx, hPx, alt, float) {
@@ -744,21 +1041,35 @@ function serializeDocx(doc, opts = {}) {
744
1041
  }
745
1042
  return out;
746
1043
  }
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)}"/>`);
1044
+ function paraProps(node, lead = [], indentTwips) {
1045
+ const p = [...lead];
751
1046
  const bg = node.attrs?.backgroundColor;
752
1047
  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}"/>`);
1048
+ const sp = [];
1049
+ const before = cssToTwips(node.attrs?.spaceBefore);
1050
+ if (before !== null) sp.push(`w:before="${before}"`);
1051
+ const after = cssToTwips(node.attrs?.spaceAfter);
1052
+ if (after !== null) sp.push(`w:after="${after}"`);
1053
+ const lh = String(node.attrs?.lineHeight ?? "").trim();
1054
+ if (lh) {
1055
+ const abs = cssToTwips(lh);
1056
+ if (/^[\d.]+$/.test(lh)) sp.push(`w:line="${Math.round(Number(lh) * 240)}" w:lineRule="auto"`);
1057
+ else if (abs !== null) sp.push(`w:line="${abs}" w:lineRule="atLeast"`);
1058
+ }
1059
+ if (sp.length) p.push(`<w:spacing ${sp.join(" ")}/>`);
1060
+ const ind = indentTwips ?? Number(node.attrs?.indent ?? 0) * 420;
1061
+ if (ind > 0) p.push(`<w:ind w:left="${ind}"/>`);
1062
+ const align = node.attrs?.textAlign ?? node.attrs?.nodeTextAlign;
1063
+ if (align && align !== "left") {
1064
+ p.push(`<w:jc w:val="${esc(align === "justify" ? "both" : align)}"/>`);
1065
+ }
755
1066
  const ov = opts.paragraphOverrides?.(node);
756
1067
  for (const [k, v] of Object.entries(ov ?? {})) p.push(`<w:${k} w:val="${esc(v)}"/>`);
757
1068
  return p.length ? `<w:pPr>${p.join("")}</w:pPr>` : "";
758
1069
  }
759
- function paragraph(node, extraProps = []) {
1070
+ function paragraph(node, lead = [], indentTwips) {
760
1071
  stats.paragraphs += 1;
761
- return `<w:p>${paraProps(node, extraProps)}${inlineToRuns(node.content)}</w:p>`;
1072
+ return `<w:p>${paraProps(node, lead, indentTwips)}${inlineToRuns(node.content)}</w:p>`;
762
1073
  }
763
1074
  function heading(node) {
764
1075
  stats.headings += 1;
@@ -778,8 +1089,9 @@ function serializeDocx(doc, opts = {}) {
778
1089
  list(child, level + 1, out);
779
1090
  continue;
780
1091
  }
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>`);
1092
+ const lead = firstDone ? [] : [`<w:numPr><w:ilvl w:val="${level}"/><w:numId w:val="${numId}"/></w:numPr>`];
1093
+ const indent = firstDone ? (level + 1) * 720 : void 0;
1094
+ out.push(`<w:p>${paraProps(child, lead, indent)}${inlineToRuns(child.content)}</w:p>`);
783
1095
  firstDone = true;
784
1096
  }
785
1097
  if (!firstDone) {
@@ -814,20 +1126,24 @@ function serializeDocx(doc, opts = {}) {
814
1126
  c += occupied[r][c];
815
1127
  }
816
1128
  });
817
- const widths = [];
1129
+ const px = [];
818
1130
  for (const { cell, colSpan } of grid[0] ?? []) {
819
1131
  const cw = cell?.attrs?.colwidth ?? [];
820
- for (let i = 0; i < colSpan; i++) widths.push(Math.round((cw[i] ?? 100) * PX_TO_DXA));
1132
+ for (let i = 0; i < colSpan; i++) px.push(cw[i] ?? 100);
821
1133
  }
1134
+ const widths = columnsToDxa(px);
822
1135
  const gridXml = widths.map((w) => `<w:gridCol w:w="${w}"/>`).join("");
823
1136
  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>`;
1137
+ let xml = `<w:tbl><w:tblPr><w:tblW w:type="dxa" w:w="${widths.reduce((a, b) => a + b, 0)}"/>${borders}<w:tblLayout w:type="fixed"/></w:tblPr><w:tblGrid>${gridXml}</w:tblGrid>`;
825
1138
  grid.forEach((cells, r) => {
826
1139
  const isHeader = (rows[r].content ?? []).some((c) => c.type === "tableHeader");
827
1140
  xml += "<w:tr>";
828
1141
  if (isHeader) xml += "<w:trPr><w:tblHeader/></w:trPr>";
1142
+ let col = 0;
829
1143
  for (const { cell, colSpan } of cells) {
830
- const props = [];
1144
+ const tcW = widths.slice(col, col + colSpan).reduce((a, b) => a + b, 0);
1145
+ col += colSpan;
1146
+ const props = [`<w:tcW w:type="dxa" w:w="${tcW || 100}"/>`];
831
1147
  if (colSpan > 1) props.push(`<w:gridSpan w:val="${colSpan}"/>`);
832
1148
  if (!cell) {
833
1149
  props.push("<w:vMerge/>");
@@ -864,7 +1180,7 @@ function serializeDocx(doc, opts = {}) {
864
1180
  out.push(table(n));
865
1181
  break;
866
1182
  case "blockquote":
867
- for (const c of n.content ?? []) out.push(paragraph(c, ['<w:ind w:left="720"/>']));
1183
+ for (const c of n.content ?? []) out.push(paragraph(c, [], 720));
868
1184
  break;
869
1185
  case "codeBlock":
870
1186
  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>`);
@@ -885,9 +1201,23 @@ function serializeDocx(doc, opts = {}) {
885
1201
  }
886
1202
  return out.join("");
887
1203
  }
1204
+ function sectPr() {
1205
+ const raw = opts.section?.sectPrXml;
1206
+ const refs = (opts.section?.refs ?? []).filter((r) => ridMap.has(r.rid)).map(
1207
+ (r) => `<w:${r.kind}Reference w:type="${esc(r.type)}" r:id="${ridMap.get(r.rid)}"/>`
1208
+ ).join("");
1209
+ if (!raw) {
1210
+ const geometry = '<w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/>';
1211
+ return `<w:sectPr>${refs}${geometry}</w:sectPr>`;
1212
+ }
1213
+ const stripped = raw.replace(/<w:(?:header|footer)Reference\b[^>]*\/>/g, "");
1214
+ const selfClosing = /^<w:sectPr\b([^>]*)\/>$/.exec(stripped);
1215
+ if (selfClosing) return `<w:sectPr${selfClosing[1]}>${refs}</w:sectPr>`;
1216
+ return stripped.replace(/^(<w:sectPr\b[^>]*>)/, `$1${refs}`);
1217
+ }
888
1218
  const bodyXml = blocks(doc.content ?? []);
889
1219
  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>`
1220
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document ${NS}><w:body>${bodyXml}${sectPr()}</w:body></w:document>`
891
1221
  );
892
1222
  const headingStyles = Array.from({ length: 9 }, (_, i) => {
893
1223
  const lv = i + 1;
@@ -909,6 +1239,12 @@ function serializeDocx(doc, opts = {}) {
909
1239
  files["word/numbering.xml"] = enc.encode(
910
1240
  `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:numbering ${NS}>${abstractNums}${nums}</w:numbering>`
911
1241
  );
1242
+ files["word/settings.xml"] = enc.encode(
1243
+ opts.section?.settingsXml ?? `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:settings ${NS}><w:compat><w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="15"/></w:compat></w:settings>`
1244
+ );
1245
+ rels.push(
1246
+ `<Relationship Id="rSettings" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>`
1247
+ );
912
1248
  files["word/_rels/document.xml.rels"] = enc.encode(
913
1249
  `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${rels.join("")}</Relationships>`
914
1250
  );
@@ -917,10 +1253,33 @@ function serializeDocx(doc, opts = {}) {
917
1253
  );
918
1254
  const defaults = ["rels", "xml", ...mediaExts];
919
1255
  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>`
1256
+ `<?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"/><Override PartName="/word/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"/>` + hdrFtrOverrides.join("") + `</Types>`
921
1257
  );
922
1258
  return { files, warnings, stats };
923
1259
  }
1260
+ function cssToPoints(v) {
1261
+ const m = /^\s*(-?[\d.]+)\s*(pt|px|in|cm|mm)\s*$/.exec(String(v ?? ""));
1262
+ if (!m) return null;
1263
+ const n = Number(m[1]);
1264
+ if (!Number.isFinite(n)) return null;
1265
+ const perUnit = { pt: 1, px: 0.75, in: 72, cm: 72 / 2.54, mm: 7.2 / 2.54 };
1266
+ return n * perUnit[m[2]];
1267
+ }
1268
+ function cssToTwips(v) {
1269
+ const pt = cssToPoints(v);
1270
+ return pt === null ? null : Math.round(pt * 20);
1271
+ }
1272
+ function cssToHalfPoints(v) {
1273
+ const pt = cssToPoints(v);
1274
+ return pt === null || pt <= 0 ? null : Math.round(pt * 2);
1275
+ }
1276
+ function cssFontStack(v) {
1277
+ const raw = String(v ?? "").trim();
1278
+ if (!raw) return null;
1279
+ const parts = raw.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
1280
+ if (!parts.length) return null;
1281
+ return { ascii: parts[0], eastAsia: parts[parts.length - 1] };
1282
+ }
924
1283
  function collectText(n) {
925
1284
  if (n?.type === "text") return n.text ?? "";
926
1285
  return (n?.content ?? []).map(collectText).join("");
@@ -968,18 +1327,30 @@ var ExportDocx = Extension.create({
968
1327
  verbose: 0,
969
1328
  resolveImage: void 0,
970
1329
  styleOverrides: void 0,
971
- paragraphOverrides: void 0
1330
+ paragraphOverrides: void 0,
1331
+ section: void 0,
1332
+ contentWidthPx: void 0
972
1333
  };
973
1334
  },
974
1335
  addCommands() {
975
1336
  return {
976
1337
  exportDocx: (override = {}) => ({ editor }) => {
977
1338
  const o = { ...this.options, ...override };
1339
+ if (o.section === void 0) {
1340
+ o.section = editor.storage.importDocx?.section ?? void 0;
1341
+ }
1342
+ if (o.contentWidthPx === void 0) {
1343
+ o.contentWidthPx = measureContentWidth(editor.view?.dom);
1344
+ }
978
1345
  try {
979
1346
  const { bytes, warnings, stats } = docxFromJSON(editor.getJSON(), o);
980
1347
  if (o.verbose >= 1) console.info("[exportDocx] 统计", stats);
981
1348
  if (o.verbose >= 2) for (const w of warnings) console.warn("[exportDocx]", w);
982
- o.onCompleteExport?.(deliver(bytes, o.exportType));
1349
+ o.onCompleteExport?.(deliver(bytes, o.exportType), {
1350
+ stats,
1351
+ warnings,
1352
+ bytes: bytes.length
1353
+ });
983
1354
  } catch (e) {
984
1355
  if (o.verbose >= 1) console.error("[exportDocx] 失败", e);
985
1356
  return false;
@@ -1091,12 +1462,16 @@ var ImportDocx = Extension.create({
1091
1462
  cssStyles: false,
1092
1463
  verbose: 0,
1093
1464
  preserveUnsupported: true,
1094
- injectStyles: true
1465
+ injectStyles: true,
1466
+ tableWidthBasis: "auto"
1095
1467
  };
1096
1468
  },
1097
1469
  addExtensions() {
1098
1470
  return this.options.preserveUnsupported === false ? [] : [DocxInk, DocxField];
1099
1471
  },
1472
+ addStorage() {
1473
+ return { section: null };
1474
+ },
1100
1475
  onCreate() {
1101
1476
  if (this.options.injectStyles) injectStyles();
1102
1477
  },
@@ -1118,6 +1493,7 @@ var ImportDocx = Extension.create({
1118
1493
  warnings: [],
1119
1494
  header: null,
1120
1495
  footer: null,
1496
+ section: { sectPrXml: null, refs: [], files: {}, settingsXml: null },
1121
1497
  footnotes: {},
1122
1498
  endnotes: {}
1123
1499
  });
@@ -1128,6 +1504,7 @@ var ImportDocx = Extension.create({
1128
1504
  if (!opts.resolveMedia && opts.imageUploadConfig) {
1129
1505
  uploaded = await uploadImages(files, opts.imageUploadConfig);
1130
1506
  }
1507
+ const contentWidthPx = opts.tableWidthBasis === "auto" ? measureContentWidth(editor.view?.dom) : opts.tableWidthBasis ?? void 0;
1131
1508
  const result = parseDocx(
1132
1509
  { read: (p) => files[p] ?? null },
1133
1510
  {
@@ -1135,7 +1512,9 @@ var ImportDocx = Extension.create({
1135
1512
  resolveMedia: opts.resolveMedia ?? (uploaded ? (_b, entry) => uploaded.get(entry) ?? "" : void 0),
1136
1513
  prosemirrorNodes: opts.prosemirrorNodes ?? void 0,
1137
1514
  prosemirrorMarks: opts.prosemirrorMarks ?? void 0,
1138
- cssStyles: opts.cssStyles
1515
+ cssStyles: opts.cssStyles,
1516
+ contentWidthPx,
1517
+ minColumnWidthPx: tableCellMinWidth(editor)
1139
1518
  }
1140
1519
  );
1141
1520
  const warned = /* @__PURE__ */ new Set();
@@ -1153,14 +1532,16 @@ var ImportDocx = Extension.create({
1153
1532
  if (opts.verbose >= 2) {
1154
1533
  for (const w of result.warnings) console.warn("[importDocx]", w);
1155
1534
  }
1535
+ this.storage.section = result.section;
1156
1536
  finish({
1157
1537
  content: result.doc,
1158
1538
  setEditorContent: (c) => editor.commands.setContent(c ?? result.doc, { emitUpdate: true }),
1159
1539
  recovered: result.recovered,
1160
1540
  warnings: result.warnings,
1161
1541
  css: result.css,
1162
- header: null,
1163
- footer: null,
1542
+ header: result.header,
1543
+ footer: result.footer,
1544
+ section: result.section,
1164
1545
  footnotes: {},
1165
1546
  endnotes: {}
1166
1547
  });