@coco-vexel/coco-office-oojson 0.0.0-stage → 1.0.1
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/LICENSE +202 -0
- package/README.md +99 -2
- package/THIRD_PARTY_NOTICES.md +1188 -0
- package/dist/oojson.global.js +30 -0
- package/package.json +45 -3
- package/src/color.mjs +38 -0
- package/src/extra.mjs +37 -0
- package/src/flat-opc.mjs +394 -0
- package/src/index.mjs +39 -0
- package/src/json-to-ooxml.mjs +605 -0
- package/src/long-tail-parts.mjs +424 -0
- package/src/numbering-to-ooxml.mjs +120 -0
- package/src/ooxml-drawing-to-json.mjs +209 -0
- package/src/ooxml-to-json.mjs +305 -0
- package/src/ppt-content.mjs +346 -0
- package/src/ppt-deck.mjs +248 -0
- package/src/styles-to-ooxml.mjs +302 -0
- package/src/writeback.mjs +1082 -0
- package/src/xml.mjs +66 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
// 工具①:ONLYOFFICE-JSON → OOXML(WordprocessingML body)。
|
|
2
|
+
// 读链路用:编辑器 ToJSON 出 ONLYOFFICE-JSON → 本工具转 OOXML → 喂 agent。
|
|
3
|
+
// 无损保证:每个属性袋/元素把未建模的剩余键塞进 oo:x 兜底属性(见 extra.mjs)。
|
|
4
|
+
import { esc, attrStr } from "./xml.mjs";
|
|
5
|
+
import { colorToOoxml, highlightToOoxml, autoRgbAttr } from "./color.mjs";
|
|
6
|
+
import { extraAttr, onlyKeys } from "./extra.mjs";
|
|
7
|
+
|
|
8
|
+
function shdXml(shd) {
|
|
9
|
+
if (!shd) return "";
|
|
10
|
+
const fill = shd.fill ? ` w:fill="${colorToOoxml(shd.fill)}"${autoRgbAttr("oo:frgb", shd.fill)}` : "";
|
|
11
|
+
return `<w:shd w:val="${shd.val || "clear"}" w:color="${colorToOoxml(shd.color)}"${autoRgbAttr("oo:crgb", shd.color)}${fill}${extraAttr(shd, ["val", "color", "fill"])}/>`;
|
|
12
|
+
}
|
|
13
|
+
function ppToXml(pPr) {
|
|
14
|
+
if (!pPr) return "";
|
|
15
|
+
let x = "";
|
|
16
|
+
if (pPr.pStyle != null) x += `<w:pStyle w:val="${esc(pPr.pStyle)}"/>`;
|
|
17
|
+
for (const t of ["keepNext", "keepLines", "pageBreakBefore", "widowControl", "contextualSpacing", "suppressLineNumbers"])
|
|
18
|
+
if (pPr[t] === true) x += `<w:${t}/>`; // false 由兜底还原(编辑器恒输出的默认键)
|
|
19
|
+
x += shdXml(pPr.shd);
|
|
20
|
+
if (pPr.spacing) {
|
|
21
|
+
const s = pPr.spacing, a = {};
|
|
22
|
+
if (s.before != null) a["w:before"] = s.before;
|
|
23
|
+
if (s.after != null) a["w:after"] = s.after;
|
|
24
|
+
if (s.line != null) a["w:line"] = s.line;
|
|
25
|
+
if (s.lineRule != null) a["w:lineRule"] = s.lineRule;
|
|
26
|
+
if (s.beforeAutoSpacing != null) a["w:beforeAutospacing"] = s.beforeAutoSpacing === "off" ? 0 : 1;
|
|
27
|
+
if (s.afterAutoSpacing != null) a["w:afterAutospacing"] = s.afterAutoSpacing === "off" ? 0 : 1;
|
|
28
|
+
x += `<w:spacing${attrStr(a)}/>`;
|
|
29
|
+
}
|
|
30
|
+
if (pPr.ind) {
|
|
31
|
+
const d = pPr.ind, a = {};
|
|
32
|
+
if (d.left != null) a["w:left"] = d.left;
|
|
33
|
+
if (d.right != null) a["w:right"] = d.right;
|
|
34
|
+
if (d.firstLine != null) a["w:firstLine"] = d.firstLine;
|
|
35
|
+
x += `<w:ind${attrStr(a)}/>`;
|
|
36
|
+
}
|
|
37
|
+
if (pPr.jc != null) x += `<w:jc w:val="${pPr.jc}"/>`;
|
|
38
|
+
return x; // 返回 pPr 内层(不含 <w:pPr> 包裹),由 paraToXml 拼上段落 mark rPr + oo:x 后统一包裹
|
|
39
|
+
}
|
|
40
|
+
function rpInner(rPr) {
|
|
41
|
+
let x = "";
|
|
42
|
+
if (rPr.rFonts) {
|
|
43
|
+
const f = rPr.rFonts, a = {};
|
|
44
|
+
for (const k of ["ascii", "hAnsi", "eastAsia", "cs", "hint", "asciiTheme", "hAnsiTheme", "eastAsiaTheme", "cstheme"]) if (f[k] != null) a["w:" + k] = f[k];
|
|
45
|
+
x += `<w:rFonts${attrStr(a)}/>`;
|
|
46
|
+
}
|
|
47
|
+
const tgl = (name, v) => (v === true ? `<w:${name}/>` : v === false ? `<w:${name} w:val="false"/>` : "");
|
|
48
|
+
x += tgl("b", rPr.b) + tgl("bCs", rPr.bCs) + tgl("i", rPr.i) + tgl("iCs", rPr.iCs);
|
|
49
|
+
if (rPr.color) x += `<w:color w:val="${colorToOoxml(rPr.color)}"${autoRgbAttr("oo:rgb", rPr.color)}/>`;
|
|
50
|
+
if (rPr.spacing != null) x += `<w:spacing w:val="${rPr.spacing}"/>`;
|
|
51
|
+
if (rPr.sz != null) x += `<w:sz w:val="${rPr.sz}"/>`;
|
|
52
|
+
if (rPr.szCs != null) x += `<w:szCs w:val="${rPr.szCs}"/>`;
|
|
53
|
+
if (rPr.highlight) { const h = highlightToOoxml(rPr.highlight); if (h) x += `<w:highlight w:val="${h}"/>`; }
|
|
54
|
+
if (rPr.vertAlign != null) x += `<w:vertAlign w:val="${typeof rPr.vertAlign === "object" ? rPr.vertAlign.val || rPr.vertAlign.value : rPr.vertAlign}"/>`;
|
|
55
|
+
x += shdXml(rPr.shd);
|
|
56
|
+
if (rPr.lang) {
|
|
57
|
+
const a = {};
|
|
58
|
+
for (const k of ["val", "eastAsia", "bidi"]) if (rPr.lang[k] != null) a["w:" + k] = rPr.lang[k];
|
|
59
|
+
x += `<w:lang${attrStr(a)}/>`;
|
|
60
|
+
}
|
|
61
|
+
return x;
|
|
62
|
+
}
|
|
63
|
+
function rpToXml(rPr) {
|
|
64
|
+
if (!rPr) return "";
|
|
65
|
+
const inner = rpInner(rPr);
|
|
66
|
+
const ex = extraAttr(rPr);
|
|
67
|
+
return inner || ex ? `<w:rPr${ex}>${inner}</w:rPr>` : "";
|
|
68
|
+
}
|
|
69
|
+
// run 内容项:文本/分页符/制表符 → 真 OOXML;其余(drawing 等)→ oo:ci 无损兜底。
|
|
70
|
+
function drawingDocPrAttrs(d) {
|
|
71
|
+
const docPr = d?.docPr || {};
|
|
72
|
+
const attrs = { id: docPr.id ?? 1, name: docPr.name ?? "Picture" };
|
|
73
|
+
if (docPr.descr != null) attrs.descr = docPr.descr;
|
|
74
|
+
if (docPr.title != null) attrs.title = docPr.title;
|
|
75
|
+
return attrStr(attrs);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function srcRectXml(blipFill) {
|
|
79
|
+
const rect = blipFill?.srcRect;
|
|
80
|
+
if (!rect || typeof rect !== "object") return "";
|
|
81
|
+
const attrs = {};
|
|
82
|
+
for (const key of ["l", "t", "r", "b"]) if (rect[key] != null) attrs[key] = rect[key];
|
|
83
|
+
return Object.keys(attrs).length ? `<a:srcRect${attrStr(attrs)}/>` : "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function presetGeometryXml(spPr) {
|
|
87
|
+
const raw =
|
|
88
|
+
spPr?.prstGeom?.prst ??
|
|
89
|
+
spPr?.custGeom?.preset ??
|
|
90
|
+
spPr?.geometry?.prst ??
|
|
91
|
+
spPr?.geometry?.preset ??
|
|
92
|
+
spPr?.prstGeom;
|
|
93
|
+
const prst = typeof raw === "string" ? raw : "rect";
|
|
94
|
+
return `<a:prstGeom prst="${esc(prst)}"><a:avLst/></a:prstGeom>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function effectExtentXml(effectExtent) {
|
|
98
|
+
if (!effectExtent || typeof effectExtent !== "object") return "";
|
|
99
|
+
const attrs = {};
|
|
100
|
+
for (const key of ["l", "t", "r", "b"]) if (effectExtent[key] != null) attrs[key] = effectExtent[key];
|
|
101
|
+
return Object.keys(attrs).length ? `<wp:effectExtent${attrStr(attrs)}/>` : "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function drawingPositionXml(tag, pos, fallbackRelativeFrom) {
|
|
105
|
+
if (!pos || typeof pos !== "object") return `<wp:${tag} relativeFrom="${fallbackRelativeFrom}"><wp:posOffset>0</wp:posOffset></wp:${tag}>`;
|
|
106
|
+
const relativeFrom = pos.relativeFrom || fallbackRelativeFrom;
|
|
107
|
+
const child = pos.align != null ? `<wp:align>${esc(pos.align)}</wp:align>` : `<wp:posOffset>${esc(pos.posOffset ?? pos.offset ?? 0)}</wp:posOffset>`;
|
|
108
|
+
return `<wp:${tag} relativeFrom="${esc(relativeFrom)}">${child}</wp:${tag}>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function anchorWrapXml(d) {
|
|
112
|
+
const type = d?.wrapType || d?.wrap?.type || d?.wrappingType;
|
|
113
|
+
const wrapText = d?.wrapText || d?.wrap?.wrapText || "bothSides";
|
|
114
|
+
if (type === "none") return "<wp:wrapNone/>";
|
|
115
|
+
if (type === "topAndBottom" || type === "topBottom") return "<wp:wrapTopAndBottom/>";
|
|
116
|
+
if (type === "tight") return `<wp:wrapTight wrapText="${esc(wrapText)}"><wp:wrapPolygon edited="0"><wp:start x="0" y="0"/></wp:wrapPolygon></wp:wrapTight>`;
|
|
117
|
+
if (type === "through") {
|
|
118
|
+
const edited = boolAttr(d?.wrapThrough?.edited ?? d?.wrap?.edited ?? false);
|
|
119
|
+
return `<wp:wrapThrough wrapText="${esc(wrapText)}"><wp:wrapPolygon edited="${esc(edited)}"><wp:start x="0" y="0"/></wp:wrapPolygon></wp:wrapThrough>`;
|
|
120
|
+
}
|
|
121
|
+
return `<wp:wrapSquare wrapText="${esc(wrapText)}"/>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function drawingContainerXml(d, cx, cy, docPrAttrs, graphicXml) {
|
|
125
|
+
const distAttrs = { distT: d?.distT ?? 0, distB: d?.distB ?? 0, distL: d?.distL ?? 0, distR: d?.distR ?? 0 };
|
|
126
|
+
if (d?.drawingType !== "anchor" && d?.inline !== false && d?.anchor !== true) {
|
|
127
|
+
return (
|
|
128
|
+
`<wp:inline${attrStr(distAttrs)}>` +
|
|
129
|
+
`<wp:extent cx="${esc(cx)}" cy="${esc(cy)}"/>` +
|
|
130
|
+
effectExtentXml(d?.effectExtent) +
|
|
131
|
+
`<wp:docPr${docPrAttrs}/>` +
|
|
132
|
+
graphicXml +
|
|
133
|
+
`</wp:inline>`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const simplePos = d?.simplePos && typeof d.simplePos === "object" ? d.simplePos : {};
|
|
137
|
+
const anchorAttrs = {
|
|
138
|
+
...distAttrs,
|
|
139
|
+
simplePos: boolAttr(d?.simplePos === true || d?.simplePos?.enabled === true),
|
|
140
|
+
relativeHeight: d?.relativeHeight ?? 0,
|
|
141
|
+
behindDoc: boolAttr(d?.behindDoc ?? false),
|
|
142
|
+
locked: boolAttr(d?.locked ?? false),
|
|
143
|
+
layoutInCell: boolAttr(d?.layoutInCell ?? true),
|
|
144
|
+
allowOverlap: boolAttr(d?.allowOverlap ?? true),
|
|
145
|
+
};
|
|
146
|
+
return (
|
|
147
|
+
`<wp:anchor${attrStr(anchorAttrs)}>` +
|
|
148
|
+
`<wp:simplePos x="${esc(simplePos.x ?? 0)}" y="${esc(simplePos.y ?? 0)}"/>` +
|
|
149
|
+
drawingPositionXml("positionH", d?.positionH, "column") +
|
|
150
|
+
drawingPositionXml("positionV", d?.positionV, "paragraph") +
|
|
151
|
+
`<wp:extent cx="${esc(cx)}" cy="${esc(cy)}"/>` +
|
|
152
|
+
effectExtentXml(d?.effectExtent) +
|
|
153
|
+
anchorWrapXml(d) +
|
|
154
|
+
`<wp:docPr${docPrAttrs}/>` +
|
|
155
|
+
`<wp:cNvGraphicFramePr/>` +
|
|
156
|
+
graphicXml +
|
|
157
|
+
`</wp:anchor>`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
function drawingXml(d) {
|
|
161
|
+
const graphic = d?.graphic;
|
|
162
|
+
const blipFill = graphic?.blipFill || {};
|
|
163
|
+
const spPr = graphic?.spPr || {};
|
|
164
|
+
const relId = blipFill.rasterImageId;
|
|
165
|
+
const cx = d?.extent?.cx ?? spPr?.xfrm?.ext?.cx ?? 1;
|
|
166
|
+
const cy = d?.extent?.cy ?? spPr?.xfrm?.ext?.cy ?? 1;
|
|
167
|
+
const docPrAttrs = drawingDocPrAttrs(d);
|
|
168
|
+
if (
|
|
169
|
+
(graphic?.type === "shape" || graphic?.type === "connectShape") &&
|
|
170
|
+
!relId
|
|
171
|
+
) {
|
|
172
|
+
const content = d?.content || graphic?.content;
|
|
173
|
+
const inner = Array.isArray(content?.content) ? content.content : [];
|
|
174
|
+
const text = inner.map(blockPlainText).join("");
|
|
175
|
+
const graphicXml =
|
|
176
|
+
`<a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">` +
|
|
177
|
+
`<wps:wsp>${shapeNonVisualXml(graphic)}<wps:spPr>${shapePropertiesXml(d, cx, cy)}</wps:spPr>${drawingTextboxXml(text, "wps")}</wps:wsp>` +
|
|
178
|
+
`</a:graphicData></a:graphic>`;
|
|
179
|
+
return `<w:drawing>${drawingContainerXml(d, cx, cy, docPrAttrs, graphicXml)}</w:drawing>`;
|
|
180
|
+
}
|
|
181
|
+
if (!relId || !String(relId).startsWith("rId")) return null;
|
|
182
|
+
const graphicXml =
|
|
183
|
+
`<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">` +
|
|
184
|
+
`<pic:pic><pic:nvPicPr><pic:cNvPr${docPrAttrs}/><pic:cNvPicPr/></pic:nvPicPr>` +
|
|
185
|
+
`<pic:blipFill><a:blip r:embed="${esc(relId)}"/>${srcRectXml(blipFill)}<a:stretch><a:fillRect/></a:stretch></pic:blipFill>` +
|
|
186
|
+
`<pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${esc(cx)}" cy="${esc(cy)}"/></a:xfrm>${presetGeometryXml(spPr)}</pic:spPr>` +
|
|
187
|
+
`</pic:pic></a:graphicData></a:graphic>`;
|
|
188
|
+
return `<w:drawing>${drawingContainerXml(d, cx, cy, docPrAttrs, graphicXml)}</w:drawing>`;
|
|
189
|
+
}
|
|
190
|
+
function noteReferenceXml(tag, item) {
|
|
191
|
+
const custom = item.customMarkFollows === true ? ` w:customMarkFollows="1"` : "";
|
|
192
|
+
return `<w:${tag} w:id="${esc(item.footnote)}"${custom}/>`;
|
|
193
|
+
}
|
|
194
|
+
function runItemXml(it) {
|
|
195
|
+
if (typeof it === "string") return `<w:t xml:space="preserve">${esc(it)}</w:t>`;
|
|
196
|
+
if (it && it.type === "break" && onlyKeys(it, ["type", "breakType"]) && (it.breakType === "page" || it.breakType === "column"))
|
|
197
|
+
return `<w:br w:type="${it.breakType}"/>`; // 仅 page/column 确定可逆;line/textWrapping 等走 oo:ci 兜底
|
|
198
|
+
if (it && it.type === "tab" && onlyKeys(it, ["type"])) return `<w:tab/>`;
|
|
199
|
+
if (it && it.type === "separator" && onlyKeys(it, ["type"])) return `<w:separator/>`;
|
|
200
|
+
if (it && it.type === "continuationSeparator" && onlyKeys(it, ["type"])) return `<w:continuationSeparator/>`;
|
|
201
|
+
if (it && it.type === "footnoteRef" && it.footnote != null) return noteReferenceXml("footnoteReference", it);
|
|
202
|
+
if (it && it.type === "endnoteRef" && it.footnote != null) return noteReferenceXml("endnoteReference", it);
|
|
203
|
+
if (it && it.type === "paraDrawing") {
|
|
204
|
+
const xml = drawingXml(it);
|
|
205
|
+
if (xml) return xml;
|
|
206
|
+
}
|
|
207
|
+
return `<oo:ci oo:v="${esc(JSON.stringify(it))}"/>`;
|
|
208
|
+
}
|
|
209
|
+
function runToXml(run) {
|
|
210
|
+
const items = (run.content || []).map(runItemXml).join("");
|
|
211
|
+
const ex = extraAttr(run, ["rPr", "content", "footnotes", "endnotes"]);
|
|
212
|
+
return `<w:r${ex}>${rpToXml(run.rPr)}${items}</w:r>`;
|
|
213
|
+
}
|
|
214
|
+
function hyperlinkToXml(link) {
|
|
215
|
+
const attrs = [];
|
|
216
|
+
const relId = link.rId || link.id || link["r:id"];
|
|
217
|
+
if (relId) attrs.push(`r:id="${esc(relId)}"`);
|
|
218
|
+
else if (link.value) attrs.push(`w:anchor="${esc(String(link.value).replace(/^#/, ""))}"`);
|
|
219
|
+
if (link.tooltip) attrs.push(`w:tooltip="${esc(link.tooltip)}"`);
|
|
220
|
+
const inner = (link.content || []).map((item) => (item?.type === "run" ? runToXml(item) : `<oo:pi oo:v="${esc(JSON.stringify(item))}"/>`)).join("");
|
|
221
|
+
return `<w:hyperlink${attrs.length ? " " + attrs.join(" ") : ""}>${inner}</w:hyperlink>`;
|
|
222
|
+
}
|
|
223
|
+
// 段落内容项:run→<w:r>;endRun(段落标记)→不输出(体现在 pPr/rPr);
|
|
224
|
+
// 其它(commentRangeStart/End、hyperlink、math 等)→ oo:pi 按序无损占位。
|
|
225
|
+
function paraItemXml(it) {
|
|
226
|
+
if (it.type === "endRun") return "";
|
|
227
|
+
if (it.type === "run") return runToXml(it);
|
|
228
|
+
if (it.type === "hyperlink") return hyperlinkToXml(it);
|
|
229
|
+
if (it.type === "commentRangeStart" && it.id != null) return `<w:commentRangeStart w:id="${esc(it.id)}"/>`;
|
|
230
|
+
if (it.type === "commentRangeEnd" && it.id != null) {
|
|
231
|
+
return `<w:commentRangeEnd w:id="${esc(it.id)}"/><w:r><w:commentReference w:id="${esc(it.id)}"/></w:r>`;
|
|
232
|
+
}
|
|
233
|
+
if (it.type === "commentReference" && it.id != null) return `<w:r><w:commentReference w:id="${esc(it.id)}"/></w:r>`;
|
|
234
|
+
return `<oo:pi oo:v="${esc(JSON.stringify(it))}"/>`;
|
|
235
|
+
}
|
|
236
|
+
function paraToXml(p) {
|
|
237
|
+
const inner = ppToXml(p.pPr) + rpToXml(p.rPr); // pPr 属性 + 段落 mark rPr(= endRun)
|
|
238
|
+
const ppEx = extraAttr(p.pPr);
|
|
239
|
+
const pPrXml = inner || ppEx ? `<w:pPr${ppEx}>${inner}</w:pPr>` : "";
|
|
240
|
+
const ex = extraAttr(p, ["pPr", "rPr", "content"]);
|
|
241
|
+
// endRun(段落标记)可带修订信息(reviewInfo/reviewType=remove,即段落标记的删除被追踪),rPr 已在 pPr 里;
|
|
242
|
+
// 其余非默认字段挂到 oo:er 由读侧还原到合成的 endRun。
|
|
243
|
+
const endRun = (p.content || []).find((x) => x.type === "endRun");
|
|
244
|
+
const erEx = endRun ? extraAttr(endRun, ["rPr", "content"], "oo:er") : "";
|
|
245
|
+
return `<w:p${ex}${erEx}>${pPrXml}${(p.content || []).map(paraItemXml).join("")}</w:p>`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 块级内容数组(body 或单元格):table/paragraph→真元素;其它(blockLvlSdt 等)→ oo:blk 无损占位。
|
|
249
|
+
export function blocksToXml(arr) {
|
|
250
|
+
return (arr || []).map((el) => (el.type === "table" ? tableToXml(el) : el.type === "paragraph" ? paraToXml(el) : `<oo:blk oo:v="${esc(JSON.stringify(el))}"/>`)).join("");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── 表格 ──
|
|
254
|
+
function bdrElXml(tag, b) {
|
|
255
|
+
if (!b) return "";
|
|
256
|
+
const a = {};
|
|
257
|
+
const v = b.val != null ? b.val : b.value; if (v != null) a["w:val"] = v;
|
|
258
|
+
if (b.sz != null) a["w:sz"] = b.sz;
|
|
259
|
+
if (b.space != null) a["w:space"] = b.space;
|
|
260
|
+
if (b.color) a["w:color"] = colorToOoxml(b.color);
|
|
261
|
+
return `<w:${tag}${attrStr(a)}${b.color ? autoRgbAttr("oo:rgb", b.color) : ""}${extraAttr(b, ["color", "sz", "space", "value", "val"])}/>`;
|
|
262
|
+
}
|
|
263
|
+
const BDR_SIDES = ["top", "left", "bottom", "right", "insideH", "insideV", "start", "end", "between", "bar"];
|
|
264
|
+
function bordersXml(tag, o) {
|
|
265
|
+
if (!o) return "";
|
|
266
|
+
let x = "";
|
|
267
|
+
for (const s of BDR_SIDES) if (o[s]) x += bdrElXml(s, o[s]);
|
|
268
|
+
// 空 {} 也要发出包裹元素,让读侧产出 {} 而非缺失
|
|
269
|
+
return Object.keys(o).length ? `<w:${tag}>${x}</w:${tag}>` : `<w:${tag}/>`;
|
|
270
|
+
}
|
|
271
|
+
function widthXml(tag, w) {
|
|
272
|
+
if (!w) return "";
|
|
273
|
+
const a = {};
|
|
274
|
+
if (w.type != null) a["w:type"] = w.type;
|
|
275
|
+
const v = w.w != null ? w.w : w.value; if (v != null) a["w:w"] = v;
|
|
276
|
+
return `<w:${tag}${attrStr(a)}/>`;
|
|
277
|
+
}
|
|
278
|
+
function marXml(tag, m) {
|
|
279
|
+
if (!m) return "";
|
|
280
|
+
let x = "";
|
|
281
|
+
for (const s of ["top", "left", "bottom", "right", "start", "end"]) if (m[s]) x += widthXml(s, m[s]);
|
|
282
|
+
return x ? `<w:${tag}>${x}</w:${tag}>` : "";
|
|
283
|
+
}
|
|
284
|
+
function tblPrXml(t) {
|
|
285
|
+
if (!t) return "<w:tblPr/>";
|
|
286
|
+
let x = "";
|
|
287
|
+
if (t.tblStyle != null) x += `<w:tblStyle w:val="${esc(t.tblStyle)}"/>`;
|
|
288
|
+
x += widthXml("tblW", t.tblW);
|
|
289
|
+
if (t.jc != null) x += `<w:jc w:val="${t.jc}"/>`;
|
|
290
|
+
x += bordersXml("tblBorders", t.tblBorders);
|
|
291
|
+
x += marXml("tblCellMar", t.tblCellMar);
|
|
292
|
+
if (t.tblLayout != null) x += `<w:tblLayout w:type="${typeof t.tblLayout === "object" ? t.tblLayout.type : t.tblLayout}"/>`;
|
|
293
|
+
if (t.tblLook != null && typeof t.tblLook === "object") { const a = {}; for (const k of Object.keys(t.tblLook)) if (k !== "type") a["w:" + k] = t.tblLook[k] === true ? 1 : t.tblLook[k] === false ? 0 : t.tblLook[k]; x += `<w:tblLook${attrStr(a)}/>`; }
|
|
294
|
+
// tblInd/tblOverlap/tblpPr/inline 等 → 走 oo:x 兜底(形态古怪/非 OOXML 标准,建模易错)
|
|
295
|
+
return `<w:tblPr${extraAttr(t)}>${x}</w:tblPr>`;
|
|
296
|
+
}
|
|
297
|
+
function cellToXml(c) {
|
|
298
|
+
const p = c.tcPr || {};
|
|
299
|
+
let pr = "";
|
|
300
|
+
pr += widthXml("tcW", p.tcW);
|
|
301
|
+
if (p.gridSpan != null) pr += `<w:gridSpan w:val="${p.gridSpan}"/>`;
|
|
302
|
+
if (p.vMerge != null) pr += `<w:vMerge${p.vMerge && p.vMerge !== "continue" ? ` w:val="${p.vMerge}"` : ""}/>`;
|
|
303
|
+
if (p.tcBorders) pr += bordersXml("tcBorders", p.tcBorders);
|
|
304
|
+
pr += shdXml(p.shd);
|
|
305
|
+
if (p.vAlign != null) pr += `<w:vAlign w:val="${typeof p.vAlign === "object" ? p.vAlign.val || p.vAlign.value : p.vAlign}"/>`;
|
|
306
|
+
// tcMar/noWrap 等 → oo:x 兜底
|
|
307
|
+
const prXml = `<w:tcPr${extraAttr(p)}>${pr}</w:tcPr>`;
|
|
308
|
+
// cell.content 是 DocumentContent 壳 {type:"docContent", content:[段落]}
|
|
309
|
+
const cont = c.content && Array.isArray(c.content.content) ? c.content.content : Array.isArray(c.content) ? c.content : [];
|
|
310
|
+
const body = blocksToXml(cont) || "<w:p/>";
|
|
311
|
+
const ex = extraAttr(c, ["tcPr", "content"]); // id 也进兜底,保内部计数器精确还原
|
|
312
|
+
return `<w:tc${ex}>${prXml}${body}</w:tc>`;
|
|
313
|
+
}
|
|
314
|
+
function rowToXml(r) {
|
|
315
|
+
const p = r.trPr || {};
|
|
316
|
+
let pr = "";
|
|
317
|
+
if (p.tblHeader) pr += `<w:tblHeader/>`;
|
|
318
|
+
if (p.jc != null) pr += `<w:jc w:val="${p.jc}"/>`;
|
|
319
|
+
if (p.trHeight) pr += `<w:trHeight${attrStr({ "w:val": p.trHeight.value != null ? p.trHeight.value : p.trHeight.val, "w:hRule": p.trHeight.hRule })}/>`;
|
|
320
|
+
const trPrXml = pr || extraAttr(p) ? `<w:trPr${extraAttr(p)}>${pr}</w:trPr>` : "";
|
|
321
|
+
const cells = Array.isArray(r.content) ? r.content : [];
|
|
322
|
+
const ex = extraAttr(r, ["trPr", "content"]);
|
|
323
|
+
return `<w:tr${ex}>${trPrXml}${cells.map(cellToXml).join("")}</w:tr>`;
|
|
324
|
+
}
|
|
325
|
+
function tableToXml(t) {
|
|
326
|
+
const grid = (t.tblGrid || []).map((g) => `<w:gridCol w:w="${g.w}"/>`).join("");
|
|
327
|
+
const rows = Array.isArray(t.content) ? t.content : [];
|
|
328
|
+
const ex = extraAttr(t, ["tblPr", "tblGrid", "content"]);
|
|
329
|
+
return `<w:tbl${ex}>${tblPrXml(t.tblPr)}<w:tblGrid>${grid}</w:tblGrid>${rows.map(rowToXml).join("")}</w:tbl>`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function blockPlainText(block) {
|
|
333
|
+
const out = [];
|
|
334
|
+
const visit = (value) => {
|
|
335
|
+
if (typeof value === "string") out.push(value);
|
|
336
|
+
else if (Array.isArray(value)) value.forEach(visit);
|
|
337
|
+
else if (value && typeof value === "object") {
|
|
338
|
+
if (value.type === "run" || value.type === "paragraph") visit(value.content);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
visit(block);
|
|
342
|
+
return out.join("");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function drawingTextboxXml(text, prefix) {
|
|
346
|
+
if (!text) return "";
|
|
347
|
+
return `<${prefix}:txbx><w:txbxContent><w:p><w:r><w:t xml:space="preserve">${esc(text)}</w:t></w:r></w:p></w:txbxContent></${prefix}:txbx>`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function shouldEmitTopLevelDrawing(drawing) {
|
|
351
|
+
return drawing?.graphic?.type !== "image" && !drawing?.graphic?.blipFill?.rasterImageId;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function emuToPt(value) {
|
|
355
|
+
const n = Number(value);
|
|
356
|
+
if (!Number.isFinite(n)) return null;
|
|
357
|
+
return Math.max(0, n / 12700).toFixed(2).replace(/\.00$/, "");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function rgbaColor(value) {
|
|
361
|
+
const rgba =
|
|
362
|
+
value?.color?.color?.rgba ??
|
|
363
|
+
value?.fill?.color?.color?.rgba ??
|
|
364
|
+
value?.fill?.fill?.color?.color?.rgba ??
|
|
365
|
+
value?.rgba ??
|
|
366
|
+
value?.color?.rgba ??
|
|
367
|
+
value?.fill?.color?.rgba ??
|
|
368
|
+
value?.fill?.fill?.color?.rgba;
|
|
369
|
+
if (!rgba || typeof rgba !== "object") return null;
|
|
370
|
+
return { auto: false, r: rgba.red ?? rgba.r ?? 0, g: rgba.green ?? rgba.g ?? 0, b: rgba.blue ?? rgba.b ?? 0 };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function fillKind(value) {
|
|
374
|
+
return value?.type ?? value?.fill?.type ?? value?.fill?.fill?.type;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function shapeFillXml(fill) {
|
|
378
|
+
if (fillKind(fill) === "noFill") return "<a:noFill/>";
|
|
379
|
+
const color = rgbaColor(fill);
|
|
380
|
+
return color ? `<a:solidFill><a:srgbClr val="${colorToOoxml(color)}"/></a:solidFill>` : "";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function shapeLineXml(line) {
|
|
384
|
+
if (!line || typeof line !== "object") return "";
|
|
385
|
+
const attrs = {};
|
|
386
|
+
if (line.w != null) attrs.w = line.w;
|
|
387
|
+
if (line.cap != null) attrs.cap = line.cap;
|
|
388
|
+
if (line.cmpd != null) attrs.cmpd = line.cmpd;
|
|
389
|
+
const fill = shapeFillXml(line.fill);
|
|
390
|
+
const dash = line.prstDash ? `<a:prstDash val="${esc(line.prstDash)}"/>` : "";
|
|
391
|
+
const join = line.lineJoin?.type === "round" ? "<a:round/>" : "";
|
|
392
|
+
return `<a:ln${attrStr(attrs)}>${fill}${dash}${join}</a:ln>`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function shapePropertiesXml(drawing, cx, cy) {
|
|
396
|
+
const spPr = drawing?.graphic?.spPr ?? {};
|
|
397
|
+
const xfrm = spPr.xfrm ?? {};
|
|
398
|
+
const xfrmAttrs = {};
|
|
399
|
+
for (const key of ["flipH", "flipV", "rot"]) {
|
|
400
|
+
if (xfrm[key] != null) xfrmAttrs[key] = boolAttr(xfrm[key]);
|
|
401
|
+
}
|
|
402
|
+
const off = xfrm.off ?? {};
|
|
403
|
+
const ext = xfrm.ext ?? {};
|
|
404
|
+
return (
|
|
405
|
+
`<a:xfrm${attrStr(xfrmAttrs)}>` +
|
|
406
|
+
`<a:off x="${esc(off.x ?? 0)}" y="${esc(off.y ?? 0)}"/>` +
|
|
407
|
+
`<a:ext cx="${esc(ext.cx ?? cx)}" cy="${esc(ext.cy ?? cy)}"/>` +
|
|
408
|
+
`</a:xfrm>` +
|
|
409
|
+
presetGeometryXml(spPr) +
|
|
410
|
+
shapeFillXml(spPr.fill) +
|
|
411
|
+
shapeLineXml(spPr.ln)
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function shapeNonVisualXml(graphic) {
|
|
416
|
+
if (graphic?.nvCxnSpPr) {
|
|
417
|
+
const locks = graphic.nvCxnSpPr?.cNvCxnSpPr?.cxnSpLocks ?? {};
|
|
418
|
+
return `<wps:cNvCnPr><a:cxnSpLocks${attrStr({
|
|
419
|
+
noChangeShapeType:
|
|
420
|
+
locks.noChangeShapeType == null
|
|
421
|
+
? undefined
|
|
422
|
+
: boolAttr(locks.noChangeShapeType),
|
|
423
|
+
})}/></wps:cNvCnPr>`;
|
|
424
|
+
}
|
|
425
|
+
return "<wps:cNvSpPr/>";
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function vmlShapeAttrs(name, cx, cy, spPr) {
|
|
429
|
+
const width = emuToPt(cx);
|
|
430
|
+
const height = emuToPt(cy);
|
|
431
|
+
const attrs = { id: name };
|
|
432
|
+
if (width != null && height != null) attrs.style = `width:${width}pt;height:${height}pt`;
|
|
433
|
+
const fill = spPr?.fill;
|
|
434
|
+
if (fillKind(fill) === "noFill") attrs.filled = "f";
|
|
435
|
+
else {
|
|
436
|
+
const fillColor = rgbaColor(fill);
|
|
437
|
+
if (fillColor) attrs.fillcolor = `#${colorToOoxml(fillColor)}`;
|
|
438
|
+
}
|
|
439
|
+
const ln = spPr?.ln;
|
|
440
|
+
if (fillKind(ln?.fill) === "noFill") attrs.stroked = "f";
|
|
441
|
+
else {
|
|
442
|
+
const strokeColor = rgbaColor(ln?.fill);
|
|
443
|
+
if (strokeColor) attrs.strokecolor = `#${colorToOoxml(strokeColor)}`;
|
|
444
|
+
const strokeWeight = emuToPt(ln?.w);
|
|
445
|
+
if (strokeWeight != null) attrs.strokeweight = `${strokeWeight}pt`;
|
|
446
|
+
}
|
|
447
|
+
return attrStr(attrs);
|
|
448
|
+
}
|
|
449
|
+
function topLevelDrawingXml(drawing, index) {
|
|
450
|
+
const content = drawing && (drawing.content || drawing.graphic?.content);
|
|
451
|
+
const inner = Array.isArray(content?.content) ? content.content : [];
|
|
452
|
+
const text = inner.map(blockPlainText).join("");
|
|
453
|
+
const id = drawing?.docPr?.id ?? index + 1;
|
|
454
|
+
const name = drawing?.docPr?.name ?? `Drawing ${index + 1}`;
|
|
455
|
+
const cx = drawing?.extent?.cx ?? drawing?.graphic?.spPr?.xfrm?.ext?.cx ?? 1;
|
|
456
|
+
const cy = drawing?.extent?.cy ?? drawing?.graphic?.spPr?.xfrm?.ext?.cy ?? 1;
|
|
457
|
+
const drawingText = drawingTextboxXml(text, "wps");
|
|
458
|
+
const pictText = drawingTextboxXml(text, "v");
|
|
459
|
+
const graphicXml =
|
|
460
|
+
`<a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">` +
|
|
461
|
+
`<wps:wsp>${shapeNonVisualXml(drawing?.graphic)}<wps:spPr>${shapePropertiesXml(drawing, cx, cy)}</wps:spPr>${drawingText}</wps:wsp>` +
|
|
462
|
+
`</a:graphicData></a:graphic>`;
|
|
463
|
+
return (
|
|
464
|
+
`<w:p oo:drawingObject="${esc(index)}"><w:r>` +
|
|
465
|
+
`<w:drawing>${drawingContainerXml(drawing, cx, cy, attrStr({ id, name }), graphicXml)}</w:drawing>` +
|
|
466
|
+
`<w:pict><v:shape${vmlShapeAttrs(name, cx, cy, drawing?.graphic?.spPr)}>${pictText}</v:shape></w:pict>` +
|
|
467
|
+
`</w:r></w:p>`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function drawingTextBlocksToXml(doc) {
|
|
472
|
+
const drawings = Array.isArray(doc?.drawingObjects) ? doc.drawingObjects : [];
|
|
473
|
+
const bodyDrawingIds = new Set();
|
|
474
|
+
const collect = (value) => {
|
|
475
|
+
if (!value || typeof value !== "object") return;
|
|
476
|
+
if (value.type === "paraDrawing" && value.docPr?.id != null) {
|
|
477
|
+
bodyDrawingIds.add(String(value.docPr.id));
|
|
478
|
+
}
|
|
479
|
+
if (Array.isArray(value)) {
|
|
480
|
+
value.forEach(collect);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
Object.values(value).forEach(collect);
|
|
484
|
+
};
|
|
485
|
+
collect(doc?.content);
|
|
486
|
+
return drawings
|
|
487
|
+
.filter(
|
|
488
|
+
(drawing) =>
|
|
489
|
+
shouldEmitTopLevelDrawing(drawing) &&
|
|
490
|
+
(drawing?.docPr?.id == null ||
|
|
491
|
+
!bodyDrawingIds.has(String(drawing.docPr.id))),
|
|
492
|
+
)
|
|
493
|
+
.map((drawing, index) => topLevelDrawingXml(drawing, index))
|
|
494
|
+
.join("");
|
|
495
|
+
}function boolAttr(v) {
|
|
496
|
+
return v === true ? 1 : v === false ? 0 : v;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function sectRefXml(tag, refs) {
|
|
500
|
+
if (!refs || typeof refs !== "object") return "";
|
|
501
|
+
let out = "";
|
|
502
|
+
for (const type of ["first", "default", "even"]) {
|
|
503
|
+
const ref = refs[type];
|
|
504
|
+
if (!ref) continue;
|
|
505
|
+
const id = typeof ref === "string" ? ref : ref.id || ref.rId || ref["r:id"];
|
|
506
|
+
if (!id) continue;
|
|
507
|
+
out += `<w:${tag} w:type="${type}" r:id="${esc(id)}"/>`;
|
|
508
|
+
}
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function sectBorderXml(tag, b) {
|
|
513
|
+
if (!b) return "";
|
|
514
|
+
const a = {};
|
|
515
|
+
const v = b.val != null ? b.val : b.value;
|
|
516
|
+
if (v != null) a["w:val"] = v;
|
|
517
|
+
if (b.sz != null) a["w:sz"] = b.sz;
|
|
518
|
+
if (b.space != null) a["w:space"] = b.space;
|
|
519
|
+
if (b.color) a["w:color"] = colorToOoxml(b.color);
|
|
520
|
+
return `<w:${tag}${attrStr(a)}${b.color ? autoRgbAttr("oo:rgb", b.color) : ""}${extraAttr(b, ["color", "sz", "space", "value", "val"])}/>`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function sectValOf(value) {
|
|
524
|
+
return value && typeof value === "object" ? value.val ?? value.value ?? value.type : value;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function notePrChildXml(tag, value) {
|
|
528
|
+
const v = sectValOf(value);
|
|
529
|
+
return v == null ? "" : `<w:${tag} w:val="${esc(String(v))}"/>`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function notePrXml(tag, pr) {
|
|
533
|
+
if (!pr || typeof pr !== "object" || !Object.keys(pr).length) return "";
|
|
534
|
+
let inner = "";
|
|
535
|
+
inner += notePrChildXml("pos", pr.pos ?? pr.position);
|
|
536
|
+
inner += notePrChildXml("numFmt", pr.numFmt);
|
|
537
|
+
inner += notePrChildXml("numStart", pr.numStart ?? pr.start);
|
|
538
|
+
inner += notePrChildXml("numRestart", pr.numRestart ?? pr.restart);
|
|
539
|
+
return `<w:${tag}${extraAttr(pr, ["pos", "position", "numFmt", "numStart", "start", "numRestart", "restart"])}>${inner}</w:${tag}>`;
|
|
540
|
+
}
|
|
541
|
+
function sectPrXml(sectPr) {
|
|
542
|
+
if (!sectPr || typeof sectPr !== "object") return "";
|
|
543
|
+
let x = "";
|
|
544
|
+
x += sectRefXml("headerReference", sectPr.headerReference);
|
|
545
|
+
x += sectRefXml("footerReference", sectPr.footerReference);
|
|
546
|
+
x += notePrXml("footnotePr", sectPr.footnotePr);
|
|
547
|
+
x += notePrXml("endnotePr", sectPr.endnotePr);
|
|
548
|
+
if (sectPr.type != null) x += `<w:type w:val="${esc(sectPr.type)}"/>`;
|
|
549
|
+
if (sectPr.pgSz) {
|
|
550
|
+
const p = sectPr.pgSz;
|
|
551
|
+
const a = {};
|
|
552
|
+
if (p.w != null) a["w:w"] = p.w;
|
|
553
|
+
if (p.h != null) a["w:h"] = p.h;
|
|
554
|
+
const orient = p.orient ?? p.orinet;
|
|
555
|
+
if (orient != null) a["w:orient"] = orient;
|
|
556
|
+
x += `<w:pgSz${attrStr(a)}${extraAttr(p, ["w", "h", "orient", "orinet"])}/>`;
|
|
557
|
+
}
|
|
558
|
+
if (sectPr.pgMar) {
|
|
559
|
+
const m = sectPr.pgMar;
|
|
560
|
+
const a = {};
|
|
561
|
+
for (const k of ["top", "right", "bottom", "left", "header", "footer", "gutter"]) if (m[k] != null) a["w:" + k] = m[k];
|
|
562
|
+
x += `<w:pgMar${attrStr(a)}${extraAttr(m, ["top", "right", "bottom", "left", "header", "footer", "gutter"])}/>`;
|
|
563
|
+
}
|
|
564
|
+
if (sectPr.pgBorders) {
|
|
565
|
+
const b = sectPr.pgBorders;
|
|
566
|
+
let inner = "";
|
|
567
|
+
for (const side of ["top", "left", "bottom", "right"]) inner += sectBorderXml(side, b[side]);
|
|
568
|
+
const a = {};
|
|
569
|
+
for (const k of ["display", "offsetFrom", "zOrder"]) if (b[k] != null) a["w:" + k] = b[k];
|
|
570
|
+
x += `<w:pgBorders${attrStr(a)}${extraAttr(b, ["top", "left", "bottom", "right", "display", "offsetFrom", "zOrder"])}>${inner}</w:pgBorders>`;
|
|
571
|
+
}
|
|
572
|
+
if (sectPr.cols) {
|
|
573
|
+
const c = sectPr.cols;
|
|
574
|
+
const a = {};
|
|
575
|
+
for (const k of ["equalWidth", "num", "sep", "space"]) if (c[k] != null) a["w:" + k] = boolAttr(c[k]);
|
|
576
|
+
const cols = Array.isArray(c.col) ? c.col.map((col) => {
|
|
577
|
+
const ca = {};
|
|
578
|
+
for (const [k, v] of Object.entries(col)) ca["w:" + k] = boolAttr(v);
|
|
579
|
+
return `<w:col${attrStr(ca)}/>`;
|
|
580
|
+
}).join("") : "";
|
|
581
|
+
x += `<w:cols${attrStr(a)}${extraAttr(c, ["col", "equalWidth", "num", "sep", "space"])}>${cols}</w:cols>`;
|
|
582
|
+
}
|
|
583
|
+
if (sectPr.titlePg) x += `<w:titlePg/>`;
|
|
584
|
+
if (sectPr.pgNumType) {
|
|
585
|
+
const n = sectPr.pgNumType;
|
|
586
|
+
const a = {};
|
|
587
|
+
for (const k of ["start", "fmt", "chapStyle", "chapSep"]) if (n[k] != null && n[k] !== -1) a["w:" + k] = n[k];
|
|
588
|
+
if (Object.keys(a).length || Object.keys(n).some((k) => !["start", "fmt", "chapStyle", "chapSep"].includes(k))) {
|
|
589
|
+
x += `<w:pgNumType${attrStr(a)}${extraAttr(n, ["start", "fmt", "chapStyle", "chapSep"])}/>`;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (sectPr.rtlGutter) x += `<w:rtlGutter/>`;
|
|
593
|
+
return `<w:sectPr${extraAttr(sectPr, ["headerReference", "footerReference", "footnotePr", "endnotePr", "type", "pgSz", "pgMar", "pgBorders", "cols", "titlePg", "pgNumType", "rtlGutter"])}>${x}</w:sectPr>`;
|
|
594
|
+
}
|
|
595
|
+
/** ONLYOFFICE-JSON 文档对象 → OOXML(document.xml) 字符串 */
|
|
596
|
+
export function jsonToOoxml(doc) {
|
|
597
|
+
const body = drawingTextBlocksToXml(doc) + blocksToXml(doc.content) + sectPrXml(doc.sectPr || doc.sectionPr);
|
|
598
|
+
// 文档级默认属性 paraPr(源自 styles.xml docDefaults,不在 body 里)→ 挂 <w:body> 的 oo:x 无损保留
|
|
599
|
+
const docEx = extraAttr(doc, ["content", "sectPr", "sectionPr", "drawingObjects", "commentMetadata", "theme", "styles", "numbering"]);
|
|
600
|
+
return (
|
|
601
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
602
|
+
`<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:oo="urn:coco:oojson">` +
|
|
603
|
+
`<w:body${docEx}>${body}</w:body></w:document>`
|
|
604
|
+
);
|
|
605
|
+
}
|