simple-super-doc 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.js +2221 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2221 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import JSZip from "jszip";
|
|
3
|
+
|
|
4
|
+
// src/types.ts
|
|
5
|
+
var DocxParseError = class extends Error {
|
|
6
|
+
constructor(message, code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "DocxParseError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/parser/relationships.ts
|
|
14
|
+
import { XMLParser } from "fast-xml-parser";
|
|
15
|
+
var parser = new XMLParser({
|
|
16
|
+
removeNSPrefix: true,
|
|
17
|
+
attributeNamePrefix: "",
|
|
18
|
+
ignoreAttributes: false,
|
|
19
|
+
parseAttributeValue: false,
|
|
20
|
+
isArray: (name) => name === "Relationship"
|
|
21
|
+
});
|
|
22
|
+
function parseRelationships(xml) {
|
|
23
|
+
const doc = parser.parse(xml);
|
|
24
|
+
const rels = {};
|
|
25
|
+
const relationships = doc?.Relationships?.Relationship ?? [];
|
|
26
|
+
for (const rel of relationships) {
|
|
27
|
+
if (rel.Id && rel.Type && rel.Target) {
|
|
28
|
+
rels[rel.Id] = { type: rel.Type, target: rel.Target };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return rels;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/parser/styles.ts
|
|
35
|
+
import { XMLParser as XMLParser2 } from "fast-xml-parser";
|
|
36
|
+
var parser2 = new XMLParser2({
|
|
37
|
+
removeNSPrefix: true,
|
|
38
|
+
attributeNamePrefix: "",
|
|
39
|
+
ignoreAttributes: false,
|
|
40
|
+
parseAttributeValue: false,
|
|
41
|
+
isArray: (name) => name === "style"
|
|
42
|
+
});
|
|
43
|
+
var HIGHLIGHT_COLORS = {
|
|
44
|
+
yellow: "yellow",
|
|
45
|
+
green: "lime",
|
|
46
|
+
cyan: "cyan",
|
|
47
|
+
magenta: "magenta",
|
|
48
|
+
red: "red",
|
|
49
|
+
blue: "blue",
|
|
50
|
+
white: "white",
|
|
51
|
+
black: "black",
|
|
52
|
+
darkBlue: "darkblue",
|
|
53
|
+
darkCyan: "darkcyan",
|
|
54
|
+
darkGreen: "darkgreen",
|
|
55
|
+
darkMagenta: "darkmagenta",
|
|
56
|
+
darkRed: "darkred",
|
|
57
|
+
darkYellow: "#808000",
|
|
58
|
+
darkGray: "#a9a9a9",
|
|
59
|
+
lightGray: "#d3d3d3"
|
|
60
|
+
};
|
|
61
|
+
function extractRPr(rPr) {
|
|
62
|
+
if (!rPr) return {};
|
|
63
|
+
const s = {};
|
|
64
|
+
if ("b" in rPr) {
|
|
65
|
+
const b = rPr.b;
|
|
66
|
+
const val = typeof b === "object" && b !== null ? b.val : void 0;
|
|
67
|
+
s.bold = val === "0" || val === "false" || val === "off" ? false : true;
|
|
68
|
+
}
|
|
69
|
+
if ("i" in rPr) {
|
|
70
|
+
const i = rPr.i;
|
|
71
|
+
const val = typeof i === "object" && i !== null ? i.val : void 0;
|
|
72
|
+
s.italic = val === "0" || val === "false" || val === "off" ? false : true;
|
|
73
|
+
}
|
|
74
|
+
if ("u" in rPr) {
|
|
75
|
+
const u = rPr.u;
|
|
76
|
+
const val = typeof u === "object" && u !== null ? u.val : void 0;
|
|
77
|
+
if (val === "none" || val === "0" || val === "false" || val === "off") {
|
|
78
|
+
s.underline = false;
|
|
79
|
+
} else {
|
|
80
|
+
s.underline = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if ("strike" in rPr) {
|
|
84
|
+
const st = rPr.strike;
|
|
85
|
+
const val = typeof st === "object" && st !== null ? st.val : void 0;
|
|
86
|
+
s.strike = !(val === "0" || val === "false" || val === "off");
|
|
87
|
+
}
|
|
88
|
+
if ("vertAlign" in rPr) {
|
|
89
|
+
const va = rPr.vertAlign?.val;
|
|
90
|
+
if (va === "superscript") s.vertAlign = "super";
|
|
91
|
+
else if (va === "subscript") s.vertAlign = "sub";
|
|
92
|
+
}
|
|
93
|
+
if ("rtl" in rPr) {
|
|
94
|
+
const val = rPr.rtl?.val;
|
|
95
|
+
s.rtl = !(val === "0" || val === "false" || val === "off");
|
|
96
|
+
}
|
|
97
|
+
if ("highlight" in rPr) {
|
|
98
|
+
const val = rPr.highlight?.val;
|
|
99
|
+
const css = HIGHLIGHT_COLORS[val ?? ""];
|
|
100
|
+
if (css) s.highlight = css;
|
|
101
|
+
}
|
|
102
|
+
if ("sz" in rPr) {
|
|
103
|
+
const sz = rPr.sz;
|
|
104
|
+
const raw = typeof sz === "object" && sz !== null ? sz.val : String(sz);
|
|
105
|
+
const half = parseInt(raw, 10);
|
|
106
|
+
if (!isNaN(half)) s.fontSize = half / 2;
|
|
107
|
+
}
|
|
108
|
+
if ("rFonts" in rPr) {
|
|
109
|
+
const fonts = rPr.rFonts;
|
|
110
|
+
const family = fonts.ascii ?? fonts.hAnsi;
|
|
111
|
+
if (family) s.fontFamily = family;
|
|
112
|
+
}
|
|
113
|
+
if ("color" in rPr) {
|
|
114
|
+
const color = rPr.color;
|
|
115
|
+
const val = typeof color === "object" && color !== null ? color.val : String(color);
|
|
116
|
+
if (val && val !== "auto") s.color = val;
|
|
117
|
+
}
|
|
118
|
+
if ("shd" in rPr) {
|
|
119
|
+
const shd = rPr.shd;
|
|
120
|
+
const fill = typeof shd === "object" && shd !== null ? shd.fill : void 0;
|
|
121
|
+
if (fill && fill !== "auto") s.backgroundColor = fill;
|
|
122
|
+
}
|
|
123
|
+
return s;
|
|
124
|
+
}
|
|
125
|
+
var twipsToPx = (twips) => Math.round(parseFloat(twips) * 96 / 1440);
|
|
126
|
+
function borderSideCss(b) {
|
|
127
|
+
if (!b || typeof b !== "object") return void 0;
|
|
128
|
+
const val = b.val;
|
|
129
|
+
if (!val) return void 0;
|
|
130
|
+
if (val === "nil" || val === "none") return "none";
|
|
131
|
+
const px = Math.max(1, Math.round((b.sz ? parseFloat(b.sz) / 8 : 0.5) * 96 / 72));
|
|
132
|
+
const lineStyle = val === "double" ? "double" : val === "dashed" ? "dashed" : val === "dotted" ? "dotted" : "solid";
|
|
133
|
+
const color = b.color && b.color !== "auto" ? `#${b.color}` : "#000";
|
|
134
|
+
return `${px}px ${lineStyle} ${color}`;
|
|
135
|
+
}
|
|
136
|
+
function extractBorders(node) {
|
|
137
|
+
if (!node) return {};
|
|
138
|
+
const out = {};
|
|
139
|
+
for (const side of ["top", "bottom", "left", "right", "insideH", "insideV"]) {
|
|
140
|
+
const css = borderSideCss(node[side]);
|
|
141
|
+
if (css !== void 0) out[side] = css;
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
function resolveTableBorders(styleId, rawStyles, visited) {
|
|
146
|
+
if (visited.has(styleId)) return {};
|
|
147
|
+
visited.add(styleId);
|
|
148
|
+
const raw = rawStyles[styleId];
|
|
149
|
+
if (!raw) return {};
|
|
150
|
+
let base = {};
|
|
151
|
+
const basedOn = raw.basedOn;
|
|
152
|
+
if (basedOn?.val) base = resolveTableBorders(basedOn.val, rawStyles, visited);
|
|
153
|
+
const tblPr = raw.tblPr;
|
|
154
|
+
return { ...base, ...extractBorders(tblPr?.tblBorders) };
|
|
155
|
+
}
|
|
156
|
+
function extractPPr(pPr) {
|
|
157
|
+
if (!pPr) return {};
|
|
158
|
+
const s = {};
|
|
159
|
+
if ("jc" in pPr) {
|
|
160
|
+
const jc = pPr.jc;
|
|
161
|
+
const val = typeof jc === "object" && jc !== null ? jc.val : String(jc);
|
|
162
|
+
if (val === "center") s.alignment = "center";
|
|
163
|
+
else if (val === "right" || val === "end") s.alignment = "right";
|
|
164
|
+
else if (val === "both" || val === "distribute" || val === "justify") s.alignment = "justify";
|
|
165
|
+
else s.alignment = "left";
|
|
166
|
+
}
|
|
167
|
+
if ("bidi" in pPr) {
|
|
168
|
+
const val = pPr.bidi?.val;
|
|
169
|
+
s.rtl = !(val === "0" || val === "false" || val === "off");
|
|
170
|
+
}
|
|
171
|
+
if ("pBdr" in pPr) {
|
|
172
|
+
const bdr = pPr.pBdr;
|
|
173
|
+
const sides = [["top", "borderTop"], ["bottom", "borderBottom"], ["left", "borderLeft"], ["right", "borderRight"]];
|
|
174
|
+
for (const [side, key] of sides) {
|
|
175
|
+
const b = bdr[side];
|
|
176
|
+
if (!b || typeof b !== "object") continue;
|
|
177
|
+
const val = b.val;
|
|
178
|
+
if (!val || val === "nil" || val === "none") continue;
|
|
179
|
+
const px = Math.max(1, Math.round((b.sz ? parseFloat(b.sz) / 8 : 0.5) * 96 / 72));
|
|
180
|
+
const lineStyle = val === "double" ? "double" : val === "dashed" ? "dashed" : val === "dotted" ? "dotted" : "solid";
|
|
181
|
+
const color = b.color && b.color !== "auto" ? `#${b.color}` : "#000";
|
|
182
|
+
s[key] = `${px}px ${lineStyle} ${color}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if ("ind" in pPr) {
|
|
186
|
+
const ind = pPr.ind;
|
|
187
|
+
if (ind && typeof ind === "object") {
|
|
188
|
+
if (ind.left != null) s.indentLeft = twipsToPx(ind.left);
|
|
189
|
+
if (ind.right != null) s.indentRight = twipsToPx(ind.right);
|
|
190
|
+
if (ind.hanging != null) s.indentHanging = twipsToPx(ind.hanging);
|
|
191
|
+
else if (ind.firstLine != null) s.indentFirstLine = twipsToPx(ind.firstLine);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if ("tabs" in pPr) {
|
|
195
|
+
const tabsNode = pPr.tabs?.tab;
|
|
196
|
+
const arr = Array.isArray(tabsNode) ? tabsNode : tabsNode ? [tabsNode] : [];
|
|
197
|
+
const stops = [];
|
|
198
|
+
for (const t of arr) {
|
|
199
|
+
if (!t || typeof t !== "object" || t.val === "clear" || t.pos == null) continue;
|
|
200
|
+
const v = t.val;
|
|
201
|
+
const val = v === "right" || v === "center" || v === "decimal" || v === "bar" ? v : "left";
|
|
202
|
+
const ld = t.leader;
|
|
203
|
+
const leader = ld === "dot" || ld === "middleDot" ? "dot" : ld === "hyphen" ? "hyphen" : ld === "underscore" ? "underscore" : "none";
|
|
204
|
+
stops.push({ posPx: twipsToPx(t.pos), val, leader });
|
|
205
|
+
}
|
|
206
|
+
if (stops.length) s.tabStops = stops.sort((a, b) => a.posPx - b.posPx);
|
|
207
|
+
}
|
|
208
|
+
if ("spacing" in pPr) {
|
|
209
|
+
const sp = pPr.spacing;
|
|
210
|
+
if (sp && typeof sp === "object") {
|
|
211
|
+
if (sp.before != null) s.spaceBefore = twipsToPx(sp.before);
|
|
212
|
+
if (sp.after != null) s.spaceAfter = twipsToPx(sp.after);
|
|
213
|
+
if (sp.line != null) {
|
|
214
|
+
if (sp.lineRule === "atLeast" || sp.lineRule === "exact") {
|
|
215
|
+
s.lineHeightPx = twipsToPx(sp.line);
|
|
216
|
+
} else {
|
|
217
|
+
s.lineHeight = parseFloat(sp.line) / 240;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return s;
|
|
223
|
+
}
|
|
224
|
+
function extractMarkRPr(pPr) {
|
|
225
|
+
if (!pPr || !("rPr" in pPr)) return {};
|
|
226
|
+
return extractRPr(pPr.rPr);
|
|
227
|
+
}
|
|
228
|
+
function resolveStyleChain(styleId, rawStyles, visited) {
|
|
229
|
+
if (visited.has(styleId)) {
|
|
230
|
+
console.warn(`[simple-super-doc] basedOn cycle detected at styleId "${styleId}"`);
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
visited.add(styleId);
|
|
234
|
+
const raw = rawStyles[styleId];
|
|
235
|
+
if (!raw) return {};
|
|
236
|
+
let base = {};
|
|
237
|
+
const basedOn = raw.basedOn;
|
|
238
|
+
if (basedOn?.val) {
|
|
239
|
+
base = resolveStyleChain(basedOn.val, rawStyles, visited);
|
|
240
|
+
}
|
|
241
|
+
const pPrStyle = extractPPr(raw.pPr);
|
|
242
|
+
const rPrStyle = extractRPr(raw.rPr);
|
|
243
|
+
return Object.assign({}, base, pPrStyle, rPrStyle);
|
|
244
|
+
}
|
|
245
|
+
function parseStyles(xml) {
|
|
246
|
+
const doc = parser2.parse(xml);
|
|
247
|
+
const styles = doc?.styles ?? {};
|
|
248
|
+
const docDefaults = {};
|
|
249
|
+
const rPrDefault = styles.docDefaults?.rPrDefault?.rPr;
|
|
250
|
+
if (rPrDefault) Object.assign(docDefaults, extractRPr(rPrDefault));
|
|
251
|
+
const pPrDefault = styles.docDefaults?.pPrDefault?.pPr;
|
|
252
|
+
if (pPrDefault) Object.assign(docDefaults, extractPPr(pPrDefault));
|
|
253
|
+
const rawStyles = {};
|
|
254
|
+
const styleList = styles.style ?? [];
|
|
255
|
+
for (const s of styleList) {
|
|
256
|
+
const style = s;
|
|
257
|
+
const id = style.styleId;
|
|
258
|
+
if (id) rawStyles[id] = style;
|
|
259
|
+
}
|
|
260
|
+
const styleMap = {};
|
|
261
|
+
for (const id of Object.keys(rawStyles)) {
|
|
262
|
+
styleMap[id] = resolveStyleChain(id, rawStyles, /* @__PURE__ */ new Set());
|
|
263
|
+
}
|
|
264
|
+
const tableBorderMap = {};
|
|
265
|
+
for (const id of Object.keys(rawStyles)) {
|
|
266
|
+
if (rawStyles[id].type !== "table") continue;
|
|
267
|
+
const borders = resolveTableBorders(id, rawStyles, /* @__PURE__ */ new Set());
|
|
268
|
+
if (Object.keys(borders).length) tableBorderMap[id] = borders;
|
|
269
|
+
}
|
|
270
|
+
return { styleMap, docDefaults, tableBorderMap };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/parser/numbering.ts
|
|
274
|
+
import { XMLParser as XMLParser3 } from "fast-xml-parser";
|
|
275
|
+
var parser3 = new XMLParser3({
|
|
276
|
+
removeNSPrefix: true,
|
|
277
|
+
attributeNamePrefix: "",
|
|
278
|
+
ignoreAttributes: false,
|
|
279
|
+
parseAttributeValue: false,
|
|
280
|
+
isArray: (name) => ["abstractNum", "num", "lvl", "lvlOverride"].includes(name)
|
|
281
|
+
});
|
|
282
|
+
function parseNumbering(xml) {
|
|
283
|
+
const doc = parser3.parse(xml);
|
|
284
|
+
const numbering = doc?.numbering ?? {};
|
|
285
|
+
const abstractNumMap = {};
|
|
286
|
+
for (const an of numbering.abstractNum ?? []) {
|
|
287
|
+
const id = String(an.abstractNumId);
|
|
288
|
+
abstractNumMap[id] = {};
|
|
289
|
+
for (const lvl of an.lvl ?? []) {
|
|
290
|
+
const ilvl = parseInt(String(lvl.ilvl), 10);
|
|
291
|
+
const fmtNode = lvl.numFmt;
|
|
292
|
+
const startNode = lvl.start;
|
|
293
|
+
const format = fmtNode?.val ?? "bullet";
|
|
294
|
+
const start = parseInt(startNode?.val ?? "1", 10);
|
|
295
|
+
abstractNumMap[id][ilvl] = { format, start: isNaN(start) ? 1 : start };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const numMap = {};
|
|
299
|
+
for (const n of numbering.num ?? []) {
|
|
300
|
+
const numId = String(n.numId);
|
|
301
|
+
const abstractRef = n.abstractNumId;
|
|
302
|
+
const abstractNumId = abstractRef?.val ?? "";
|
|
303
|
+
let startOverride;
|
|
304
|
+
for (const override of n.lvlOverride ?? []) {
|
|
305
|
+
const ilvl = parseInt(String(override.ilvl), 10);
|
|
306
|
+
if (ilvl === 0) {
|
|
307
|
+
const soNode = override.startOverride;
|
|
308
|
+
if (soNode?.val !== void 0) {
|
|
309
|
+
startOverride = parseInt(soNode.val, 10);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
numMap[numId] = { abstractNumId, ...startOverride !== void 0 ? { startOverride } : {} };
|
|
314
|
+
}
|
|
315
|
+
return { abstractNumMap, numMap };
|
|
316
|
+
}
|
|
317
|
+
function emptyNumbering() {
|
|
318
|
+
return { abstractNumMap: {}, numMap: {} };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/parser/document.ts
|
|
322
|
+
import { XMLParser as XMLParser4 } from "fast-xml-parser";
|
|
323
|
+
|
|
324
|
+
// src/parser/table.ts
|
|
325
|
+
function resolveVMerge(grid) {
|
|
326
|
+
if (grid.length === 0) return [];
|
|
327
|
+
const rowCount = grid.length;
|
|
328
|
+
let maxCols = 0;
|
|
329
|
+
for (const row of grid) {
|
|
330
|
+
let count = 0;
|
|
331
|
+
for (const cell of row) count += cell.colSpan;
|
|
332
|
+
maxCols = Math.max(maxCols, count);
|
|
333
|
+
}
|
|
334
|
+
const restartCell = new Array(maxCols).fill(null);
|
|
335
|
+
const output = [];
|
|
336
|
+
for (let r = 0; r < rowCount; r++) {
|
|
337
|
+
const outRow = [];
|
|
338
|
+
let col = 0;
|
|
339
|
+
for (const raw of grid[r]) {
|
|
340
|
+
const resolved = {
|
|
341
|
+
colSpan: raw.colSpan,
|
|
342
|
+
rowSpan: 1,
|
|
343
|
+
rawData: raw.rawData,
|
|
344
|
+
...raw.backgroundColor ? { backgroundColor: raw.backgroundColor } : {}
|
|
345
|
+
};
|
|
346
|
+
if (raw.vMerge === "restart") {
|
|
347
|
+
restartCell[col] = resolved;
|
|
348
|
+
outRow.push(resolved);
|
|
349
|
+
} else if (raw.vMerge === "continue") {
|
|
350
|
+
const starter = restartCell[col];
|
|
351
|
+
if (starter) {
|
|
352
|
+
starter.rowSpan++;
|
|
353
|
+
} else {
|
|
354
|
+
outRow.push(resolved);
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
restartCell[col] = null;
|
|
358
|
+
outRow.push(resolved);
|
|
359
|
+
}
|
|
360
|
+
col += raw.colSpan;
|
|
361
|
+
}
|
|
362
|
+
output.push(outRow);
|
|
363
|
+
}
|
|
364
|
+
return output;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/parser/images.ts
|
|
368
|
+
var MIME = {
|
|
369
|
+
png: "image/png",
|
|
370
|
+
jpg: "image/jpeg",
|
|
371
|
+
jpeg: "image/jpeg",
|
|
372
|
+
gif: "image/gif",
|
|
373
|
+
bmp: "image/bmp",
|
|
374
|
+
webp: "image/webp"
|
|
375
|
+
};
|
|
376
|
+
var SKIP_EXTS = /* @__PURE__ */ new Set(["emf", "wmf"]);
|
|
377
|
+
function uint8ArrayToBase64(bytes) {
|
|
378
|
+
let b64 = "";
|
|
379
|
+
for (let i = 0; i < bytes.length; i += 8192) {
|
|
380
|
+
b64 += String.fromCharCode(...bytes.subarray(i, i + 8192));
|
|
381
|
+
}
|
|
382
|
+
return btoa(b64);
|
|
383
|
+
}
|
|
384
|
+
async function resolveImage(rId, relationshipMap, zip) {
|
|
385
|
+
const rel = relationshipMap[rId];
|
|
386
|
+
if (!rel) return null;
|
|
387
|
+
if (!rel.type.includes("image")) return null;
|
|
388
|
+
if (/^https?:\/\//i.test(rel.target)) return { src: rel.target };
|
|
389
|
+
const ext = rel.target.split(".").pop()?.toLowerCase() ?? "";
|
|
390
|
+
if (SKIP_EXTS.has(ext)) return null;
|
|
391
|
+
const mime = MIME[ext];
|
|
392
|
+
if (!mime) {
|
|
393
|
+
console.warn(`[simple-super-doc] unknown image extension ".${ext}" for "${rel.target}"`);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
const entry = zip.file(`word/${rel.target}`) ?? zip.file(rel.target);
|
|
397
|
+
if (!entry) {
|
|
398
|
+
console.warn(`[simple-super-doc] image not found in zip: "${rel.target}"`);
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
const bytes = await entry.async("uint8array");
|
|
402
|
+
const src = `data:${mime};base64,${uint8ArrayToBase64(bytes)}`;
|
|
403
|
+
return { src };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/parser/document.ts
|
|
407
|
+
var parser4 = new XMLParser4({
|
|
408
|
+
removeNSPrefix: true,
|
|
409
|
+
attributeNamePrefix: "",
|
|
410
|
+
ignoreAttributes: false,
|
|
411
|
+
parseAttributeValue: false,
|
|
412
|
+
// Preserve significant whitespace in <w:t xml:space="preserve"> runs. The
|
|
413
|
+
// default (trimValues: true) strips leading/trailing spaces, which silently
|
|
414
|
+
// joins words across runs ("WHEREAS the" -> "WHEREASthe").
|
|
415
|
+
trimValues: false,
|
|
416
|
+
isArray: (name) => [
|
|
417
|
+
"p",
|
|
418
|
+
"r",
|
|
419
|
+
"tbl",
|
|
420
|
+
"tr",
|
|
421
|
+
"tc",
|
|
422
|
+
"style",
|
|
423
|
+
"abstractNum",
|
|
424
|
+
"num",
|
|
425
|
+
"lvl",
|
|
426
|
+
"lvlOverride",
|
|
427
|
+
"hyperlink",
|
|
428
|
+
"bookmarkStart",
|
|
429
|
+
"ins",
|
|
430
|
+
"del",
|
|
431
|
+
"moveTo",
|
|
432
|
+
"moveFrom",
|
|
433
|
+
"fldSimple",
|
|
434
|
+
"footnote",
|
|
435
|
+
"endnote"
|
|
436
|
+
].includes(name)
|
|
437
|
+
});
|
|
438
|
+
function getVal(node) {
|
|
439
|
+
if (node == null) return void 0;
|
|
440
|
+
if (typeof node === "object") return node.val;
|
|
441
|
+
return String(node);
|
|
442
|
+
}
|
|
443
|
+
function resolveNamedStyle(styleId, styleMap, docDefaults) {
|
|
444
|
+
if (!styleId) return { ...docDefaults };
|
|
445
|
+
const named = styleMap[styleId];
|
|
446
|
+
if (!named) {
|
|
447
|
+
console.warn(`[simple-super-doc] unknown style "${styleId}" \u2014 falling back to docDefaults`);
|
|
448
|
+
return { ...docDefaults };
|
|
449
|
+
}
|
|
450
|
+
return Object.assign({}, docDefaults, named);
|
|
451
|
+
}
|
|
452
|
+
function resolveListRef(pPr, ctx) {
|
|
453
|
+
if (!pPr) return void 0;
|
|
454
|
+
const numPr = pPr.numPr;
|
|
455
|
+
if (!numPr) return void 0;
|
|
456
|
+
const numId = getVal(numPr.numId);
|
|
457
|
+
const ilvlRaw = getVal(numPr.ilvl);
|
|
458
|
+
const ilvl = parseInt(ilvlRaw ?? "0", 10);
|
|
459
|
+
if (!numId || numId === "0") return void 0;
|
|
460
|
+
const numEntry = ctx.numMap[numId];
|
|
461
|
+
if (!numEntry) return void 0;
|
|
462
|
+
const levelMap = ctx.abstractNumMap[numEntry.abstractNumId];
|
|
463
|
+
if (!levelMap) return void 0;
|
|
464
|
+
const levelInfo = levelMap[ilvl] ?? levelMap[0];
|
|
465
|
+
if (!levelInfo) return void 0;
|
|
466
|
+
const start = numEntry.startOverride ?? levelInfo.start;
|
|
467
|
+
const ordered = levelInfo.format !== "bullet" && levelInfo.format !== "none";
|
|
468
|
+
return { numId, ilvl, ordered, start, format: levelInfo.format };
|
|
469
|
+
}
|
|
470
|
+
async function parseRun(r, paraStyle, ctx, href) {
|
|
471
|
+
const rPr = r.rPr;
|
|
472
|
+
if ("instrText" in r) return null;
|
|
473
|
+
if ("fldChar" in r) return null;
|
|
474
|
+
if ("footnoteRef" in r || "endnoteRef" in r) return null;
|
|
475
|
+
let lineBreak = false;
|
|
476
|
+
if ("br" in r) {
|
|
477
|
+
const br = r.br;
|
|
478
|
+
const brType = typeof br === "object" && br !== null ? br.type : void 0;
|
|
479
|
+
if (brType === "page") {
|
|
480
|
+
return { type: "run", text: "", style: Object.assign({}, paraStyle, extractRPr(rPr)), pageBreak: true };
|
|
481
|
+
}
|
|
482
|
+
if (brType === "column") return null;
|
|
483
|
+
lineBreak = true;
|
|
484
|
+
}
|
|
485
|
+
const rStyleId = getVal(rPr?.rStyle);
|
|
486
|
+
const charStyle = rStyleId ? ctx.styleMap[rStyleId] ?? {} : {};
|
|
487
|
+
const runStyle = Object.assign({}, paraStyle, charStyle, extractRPr(rPr));
|
|
488
|
+
const noteEl = r.footnoteReference ?? r.endnoteReference;
|
|
489
|
+
if (noteEl !== void 0) {
|
|
490
|
+
const isFoot = "footnoteReference" in r;
|
|
491
|
+
const id = String(noteEl.id ?? "");
|
|
492
|
+
const refs = isFoot ? ctx.footnoteRefs : ctx.endnoteRefs;
|
|
493
|
+
refs.push(id);
|
|
494
|
+
const marker = {
|
|
495
|
+
type: "run",
|
|
496
|
+
text: String(refs.length),
|
|
497
|
+
style: runStyle,
|
|
498
|
+
noteRef: { type: isFoot ? "footnote" : "endnote", number: refs.length }
|
|
499
|
+
};
|
|
500
|
+
return marker;
|
|
501
|
+
}
|
|
502
|
+
if ("drawing" in r) {
|
|
503
|
+
const drawing = r.drawing;
|
|
504
|
+
const anchor = drawing.anchor;
|
|
505
|
+
const inline = drawing.inline;
|
|
506
|
+
const isPageBackground = !inline && !!anchor && String(anchor.behindDoc ?? "0") === "1";
|
|
507
|
+
const drawingEl = inline ?? anchor;
|
|
508
|
+
if (!drawingEl) return null;
|
|
509
|
+
const extent = drawingEl.extent;
|
|
510
|
+
const cx = parseInt(extent?.cx ?? "0", 10);
|
|
511
|
+
const cy = parseInt(extent?.cy ?? "0", 10);
|
|
512
|
+
const rId = findBlipEmbed(drawingEl);
|
|
513
|
+
if (!rId) return null;
|
|
514
|
+
const resolved = await resolveImage(rId, ctx.relationshipMap, ctx.zip);
|
|
515
|
+
if (!resolved) return null;
|
|
516
|
+
const img = {
|
|
517
|
+
type: "image",
|
|
518
|
+
src: resolved.src,
|
|
519
|
+
widthPx: Math.round(cx / 9525),
|
|
520
|
+
heightPx: Math.round(cy / 9525),
|
|
521
|
+
...isPageBackground ? { isPageBackground: true } : {},
|
|
522
|
+
...href ? { href } : {}
|
|
523
|
+
};
|
|
524
|
+
return img;
|
|
525
|
+
}
|
|
526
|
+
if ("pict" in r) {
|
|
527
|
+
const vml = findVmlImage(r.pict);
|
|
528
|
+
if (vml) {
|
|
529
|
+
const resolved = await resolveImage(vml.id, ctx.relationshipMap, ctx.zip);
|
|
530
|
+
if (resolved) {
|
|
531
|
+
const ptToPx = (pt) => pt ? Math.round(parseFloat(pt) * 96 / 72) : 0;
|
|
532
|
+
return {
|
|
533
|
+
type: "image",
|
|
534
|
+
src: resolved.src,
|
|
535
|
+
widthPx: ptToPx(/width:([\d.]+)pt/.exec(vml.style ?? "")?.[1]),
|
|
536
|
+
heightPx: ptToPx(/height:([\d.]+)pt/.exec(vml.style ?? "")?.[1]),
|
|
537
|
+
...href ? { href } : {}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
const tNode = r.t ?? r.delText;
|
|
544
|
+
let text = "";
|
|
545
|
+
if (typeof tNode === "string") {
|
|
546
|
+
text = tNode;
|
|
547
|
+
} else if (typeof tNode === "object" && tNode !== null) {
|
|
548
|
+
const t = tNode;
|
|
549
|
+
text = String(t["#text"] ?? t._ ?? "");
|
|
550
|
+
} else if (tNode != null) {
|
|
551
|
+
text = String(tNode);
|
|
552
|
+
}
|
|
553
|
+
const tabNode = r.tab;
|
|
554
|
+
const tabs = Array.isArray(tabNode) ? tabNode.length : "tab" in r ? 1 : 0;
|
|
555
|
+
const textRun = {
|
|
556
|
+
type: "run",
|
|
557
|
+
text,
|
|
558
|
+
style: runStyle,
|
|
559
|
+
...href ? { href } : {},
|
|
560
|
+
...lineBreak ? { lineBreak: true } : {},
|
|
561
|
+
...tabs ? { tabs } : {}
|
|
562
|
+
};
|
|
563
|
+
return textRun;
|
|
564
|
+
}
|
|
565
|
+
function resolveHyperlinkHref(hl, ctx) {
|
|
566
|
+
const rId = hl.id;
|
|
567
|
+
if (typeof rId === "string") {
|
|
568
|
+
const rel = ctx.relationshipMap[rId];
|
|
569
|
+
if (rel?.target) return rel.target;
|
|
570
|
+
}
|
|
571
|
+
const anchor = hl.anchor;
|
|
572
|
+
if (typeof anchor === "string" && anchor) return `#${anchor}`;
|
|
573
|
+
return void 0;
|
|
574
|
+
}
|
|
575
|
+
function findBlipEmbed(node) {
|
|
576
|
+
if (!node || typeof node !== "object") return void 0;
|
|
577
|
+
const obj = node;
|
|
578
|
+
if ("embed" in obj && typeof obj.embed === "string") return obj.embed;
|
|
579
|
+
if ("link" in obj && typeof obj.link === "string") return obj.link;
|
|
580
|
+
for (const v of Object.values(obj)) {
|
|
581
|
+
const found = findBlipEmbed(v);
|
|
582
|
+
if (found) return found;
|
|
583
|
+
}
|
|
584
|
+
return void 0;
|
|
585
|
+
}
|
|
586
|
+
function findVmlImage(node) {
|
|
587
|
+
if (!node || typeof node !== "object") return void 0;
|
|
588
|
+
if (Array.isArray(node)) {
|
|
589
|
+
for (const x of node) {
|
|
590
|
+
const f = findVmlImage(x);
|
|
591
|
+
if (f) return f;
|
|
592
|
+
}
|
|
593
|
+
return void 0;
|
|
594
|
+
}
|
|
595
|
+
const obj = node;
|
|
596
|
+
if ("imagedata" in obj) {
|
|
597
|
+
const imgs = Array.isArray(obj.imagedata) ? obj.imagedata : [obj.imagedata];
|
|
598
|
+
for (const im of imgs) {
|
|
599
|
+
const styleRaw = Array.isArray(obj.style) ? obj.style[0] : obj.style;
|
|
600
|
+
if (im?.id) return { id: im.id, style: typeof styleRaw === "string" ? styleRaw : void 0 };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
for (const v of Object.values(obj)) {
|
|
604
|
+
const f = findVmlImage(v);
|
|
605
|
+
if (f) return f;
|
|
606
|
+
}
|
|
607
|
+
return void 0;
|
|
608
|
+
}
|
|
609
|
+
async function parseParagraph(p, ctx, rawXml) {
|
|
610
|
+
const pPr = p.pPr;
|
|
611
|
+
const pStyleId = getVal(pPr?.pStyle);
|
|
612
|
+
const namedStyle = resolveNamedStyle(pStyleId, ctx.styleMap, ctx.docDefaults);
|
|
613
|
+
const pPrStyle = extractPPr(pPr);
|
|
614
|
+
const paraStyle = Object.assign({}, namedStyle, pPrStyle);
|
|
615
|
+
const list = resolveListRef(pPr, ctx);
|
|
616
|
+
let pageBreakBefore = false;
|
|
617
|
+
if (pPr && "pageBreakBefore" in pPr) {
|
|
618
|
+
const val = getVal(pPr.pageBreakBefore);
|
|
619
|
+
pageBreakBefore = !(val === "0" || val === "false" || val === "off");
|
|
620
|
+
}
|
|
621
|
+
const directRuns = p.r ?? [];
|
|
622
|
+
const insList = p.ins ?? [];
|
|
623
|
+
const delList = p.del ?? [];
|
|
624
|
+
const moveToList = p.moveTo ?? [];
|
|
625
|
+
const moveFromList = p.moveFrom ?? [];
|
|
626
|
+
const hyperlinks = p.hyperlink ?? [];
|
|
627
|
+
const fldSimpleList = p.fldSimple ?? [];
|
|
628
|
+
const runChildInputs = (node, flag) => {
|
|
629
|
+
const inner = node.r;
|
|
630
|
+
const arr = Array.isArray(inner) ? inner : inner ? [inner] : [];
|
|
631
|
+
return arr.map((r) => ({ r, [flag]: true }));
|
|
632
|
+
};
|
|
633
|
+
const insInputs = (ins) => runChildInputs(ins, "inserted");
|
|
634
|
+
const delInputs = (del) => runChildInputs(del, "deleted");
|
|
635
|
+
const hlInputs = (hl) => {
|
|
636
|
+
const href = resolveHyperlinkHref(hl, ctx);
|
|
637
|
+
const inner = hl.r;
|
|
638
|
+
if (Array.isArray(inner)) return inner.map((r) => ({ r, href }));
|
|
639
|
+
return inner ? [{ r: inner, href }] : [];
|
|
640
|
+
};
|
|
641
|
+
const order = rawXml ? getRunOrder(rawXml) : [];
|
|
642
|
+
const counts = { r: 0, hyperlink: 0, ins: 0, del: 0, moveTo: 0, moveFrom: 0, fldSimple: 0 };
|
|
643
|
+
for (const t of order) counts[t]++;
|
|
644
|
+
const orderOk = order.length > 0 && counts.r === directRuns.length && counts.hyperlink === hyperlinks.length && counts.ins === insList.length && counts.del === delList.length && counts.moveTo === moveToList.length && counts.moveFrom === moveFromList.length && counts.fldSimple === fldSimpleList.length;
|
|
645
|
+
const inputs = [];
|
|
646
|
+
if (orderOk) {
|
|
647
|
+
let ri = 0, hi = 0, ii = 0, di = 0, mti = 0, mfi = 0, fi = 0;
|
|
648
|
+
for (const t of order) {
|
|
649
|
+
if (t === "r") inputs.push({ r: directRuns[ri++] });
|
|
650
|
+
else if (t === "hyperlink") inputs.push(...hlInputs(hyperlinks[hi++]));
|
|
651
|
+
else if (t === "ins") inputs.push(...insInputs(insList[ii++]));
|
|
652
|
+
else if (t === "del") inputs.push(...delInputs(delList[di++]));
|
|
653
|
+
else if (t === "moveTo") inputs.push(...runChildInputs(moveToList[mti++], "inserted"));
|
|
654
|
+
else if (t === "moveFrom") inputs.push(...runChildInputs(moveFromList[mfi++], "deleted"));
|
|
655
|
+
else inputs.push({ fldSimple: fldSimpleList[fi++] });
|
|
656
|
+
}
|
|
657
|
+
} else {
|
|
658
|
+
for (const r of directRuns) inputs.push({ r });
|
|
659
|
+
for (const ins of insList) inputs.push(...insInputs(ins));
|
|
660
|
+
for (const del of delList) inputs.push(...delInputs(del));
|
|
661
|
+
for (const mt of moveToList) inputs.push(...runChildInputs(mt, "inserted"));
|
|
662
|
+
for (const mf of moveFromList) inputs.push(...runChildInputs(mf, "deleted"));
|
|
663
|
+
for (const hl of hyperlinks) inputs.push(...hlInputs(hl));
|
|
664
|
+
for (const fs of fldSimpleList) inputs.push({ fldSimple: fs });
|
|
665
|
+
}
|
|
666
|
+
const LIVE_FIELDS = /* @__PURE__ */ new Set(["PAGE", "NUMPAGES", "SECTIONPAGES"]);
|
|
667
|
+
const fieldStack = [];
|
|
668
|
+
const runs = [];
|
|
669
|
+
for (const inp of inputs) {
|
|
670
|
+
if (inp.fldSimple) {
|
|
671
|
+
const fs = inp.fldSimple;
|
|
672
|
+
const field = String(fs.instr ?? "").trim().toUpperCase().split(/\s+/)[0];
|
|
673
|
+
const innerRuns = Array.isArray(fs.r) ? fs.r : fs.r ? [fs.r] : [];
|
|
674
|
+
const mStyle = Object.assign({}, paraStyle, extractRPr(innerRuns[0]?.rPr));
|
|
675
|
+
if (field === "PAGE") runs.push({ type: "run", text: "", style: mStyle, pageNumber: true });
|
|
676
|
+
else if (field === "NUMPAGES" || field === "SECTIONPAGES") runs.push({ type: "run", text: "", style: mStyle, totalPages: true });
|
|
677
|
+
else {
|
|
678
|
+
for (const ir of innerRuns) {
|
|
679
|
+
const run2 = await parseRun(ir, paraStyle, ctx);
|
|
680
|
+
if (run2 !== null) runs.push(run2);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const r = inp.r;
|
|
686
|
+
const { href, deleted, inserted } = inp;
|
|
687
|
+
const fldChars = "fldChar" in r ? Array.isArray(r.fldChar) ? r.fldChar : [r.fldChar] : [];
|
|
688
|
+
const hasInstr = "instrText" in r;
|
|
689
|
+
if (fldChars.length || hasInstr) {
|
|
690
|
+
for (const fc of fldChars) if (fc?.fldCharType === "begin") fieldStack.push({ name: "", inResult: false });
|
|
691
|
+
if (hasInstr) {
|
|
692
|
+
const instr = (typeof r.instrText === "object" && r.instrText !== null ? String(r.instrText["#text"] ?? "") : String(r.instrText)).trim().toUpperCase();
|
|
693
|
+
const field = instr.split(/\s+/)[0];
|
|
694
|
+
if (fieldStack.length) fieldStack[fieldStack.length - 1].name = field;
|
|
695
|
+
const mStyle = Object.assign({}, paraStyle, extractRPr(r.rPr));
|
|
696
|
+
if (field === "PAGE") runs.push({ type: "run", text: "", style: mStyle, pageNumber: true });
|
|
697
|
+
else if (field === "NUMPAGES" || field === "SECTIONPAGES") runs.push({ type: "run", text: "", style: mStyle, totalPages: true });
|
|
698
|
+
}
|
|
699
|
+
for (const fc of fldChars) if (fc?.fldCharType === "separate") {
|
|
700
|
+
if (fieldStack.length) fieldStack[fieldStack.length - 1].inResult = true;
|
|
701
|
+
}
|
|
702
|
+
for (const fc of fldChars) if (fc?.fldCharType === "end") fieldStack.pop();
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (fieldStack.some((f) => f.inResult && LIVE_FIELDS.has(f.name))) continue;
|
|
706
|
+
const run = await parseRun(r, paraStyle, ctx, href);
|
|
707
|
+
if (run !== null) {
|
|
708
|
+
if (deleted && run.type === "run") run.deleted = true;
|
|
709
|
+
if (inserted && run.type === "run") run.inserted = true;
|
|
710
|
+
runs.push(run);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const segments = [[]];
|
|
714
|
+
for (const run of runs) {
|
|
715
|
+
if (run.type === "run" && run.pageBreak) segments.push([]);
|
|
716
|
+
else segments[segments.length - 1].push(run);
|
|
717
|
+
}
|
|
718
|
+
const makeBlock = (segRuns, forceBreak) => {
|
|
719
|
+
const hasVisible = segRuns.some((r) => r.type === "image" || r.text.trim().length > 0);
|
|
720
|
+
const blockStyle = hasVisible ? paraStyle : Object.assign({}, paraStyle, extractMarkRPr(pPr));
|
|
721
|
+
return {
|
|
722
|
+
type: "paragraph",
|
|
723
|
+
style: blockStyle,
|
|
724
|
+
runs: segRuns,
|
|
725
|
+
...list ? { list } : {},
|
|
726
|
+
...forceBreak ? { pageBreakBefore: true } : {}
|
|
727
|
+
};
|
|
728
|
+
};
|
|
729
|
+
const blocks = segments.map((seg, i) => makeBlock(seg, i === 0 ? pageBreakBefore : true));
|
|
730
|
+
const sectPr = pPr?.sectPr;
|
|
731
|
+
const sectionPageSize = pageSizeFromSectPr(sectPr);
|
|
732
|
+
if (sectionPageSize) {
|
|
733
|
+
const last = blocks[blocks.length - 1];
|
|
734
|
+
last.sectionPageSize = sectionPageSize;
|
|
735
|
+
const refs = refsFromSectPr(sectPr);
|
|
736
|
+
if (refs) last.sectionRefs = refs;
|
|
737
|
+
}
|
|
738
|
+
const textBoxes = await extractTextBoxes(p, ctx);
|
|
739
|
+
return textBoxes.length ? [...blocks, ...textBoxes] : blocks;
|
|
740
|
+
}
|
|
741
|
+
async function extractTextBoxes(p, ctx) {
|
|
742
|
+
const contents = [];
|
|
743
|
+
collectTxbxContent(p, contents);
|
|
744
|
+
const blocks = [];
|
|
745
|
+
for (const tc of contents) blocks.push(...await parseBlockContainer(tc, ctx));
|
|
746
|
+
return blocks;
|
|
747
|
+
}
|
|
748
|
+
function collectTxbxContent(node, acc) {
|
|
749
|
+
if (!node || typeof node !== "object") return;
|
|
750
|
+
if (Array.isArray(node)) {
|
|
751
|
+
for (const item of node) collectTxbxContent(item, acc);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
for (const [key, value] of Object.entries(node)) {
|
|
755
|
+
if (key === "txbxContent") {
|
|
756
|
+
for (const tc of Array.isArray(value) ? value : [value]) {
|
|
757
|
+
if (tc && typeof tc === "object") acc.push(tc);
|
|
758
|
+
}
|
|
759
|
+
} else if (key === "AlternateContent") {
|
|
760
|
+
for (const ac of Array.isArray(value) ? value : [value]) {
|
|
761
|
+
const a = ac;
|
|
762
|
+
collectTxbxContent(a.Choice ?? a.Fallback, acc);
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
collectTxbxContent(value, acc);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function refsFromSectPr(sectPr) {
|
|
770
|
+
if (!sectPr) return void 0;
|
|
771
|
+
const defaultRId = (node) => {
|
|
772
|
+
const refs = Array.isArray(node) ? node : node ? [node] : [];
|
|
773
|
+
const def = refs.find((r) => r.type === "default") ?? refs[0];
|
|
774
|
+
return def?.id;
|
|
775
|
+
};
|
|
776
|
+
const headerRId = defaultRId(sectPr.headerReference);
|
|
777
|
+
const footerRId = defaultRId(sectPr.footerReference);
|
|
778
|
+
if (!headerRId && !footerRId) return void 0;
|
|
779
|
+
return { ...headerRId ? { headerRId } : {}, ...footerRId ? { footerRId } : {} };
|
|
780
|
+
}
|
|
781
|
+
async function parseTable(tbl, ctx, rawTableXml) {
|
|
782
|
+
const rows = tbl.tr ?? [];
|
|
783
|
+
const tblPrForBorders = tbl.tblPr;
|
|
784
|
+
const tblStyleId = getVal(tblPrForBorders?.tblStyle);
|
|
785
|
+
const styleBorders = tblStyleId && ctx.tableBorderMap[tblStyleId] || {};
|
|
786
|
+
const tableBorders = {
|
|
787
|
+
...styleBorders,
|
|
788
|
+
...extractBorders(tblPrForBorders?.tblBorders)
|
|
789
|
+
};
|
|
790
|
+
const definedSide = (...vals) => vals.find((v) => v && v !== "none");
|
|
791
|
+
const uniformBorder = definedSide(
|
|
792
|
+
tableBorders.insideH,
|
|
793
|
+
tableBorders.insideV,
|
|
794
|
+
tableBorders.top,
|
|
795
|
+
tableBorders.bottom,
|
|
796
|
+
tableBorders.left,
|
|
797
|
+
tableBorders.right
|
|
798
|
+
);
|
|
799
|
+
const tcXmlMap = /* @__PURE__ */ new Map();
|
|
800
|
+
if (rawTableXml) {
|
|
801
|
+
const cellInners = extractRowCellInners(rawTableXml);
|
|
802
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
803
|
+
const cells = rows[ri].tc ?? [];
|
|
804
|
+
const rawRowInners = cellInners[ri];
|
|
805
|
+
if (rawRowInners && rawRowInners.length === cells.length) {
|
|
806
|
+
for (let ci = 0; ci < cells.length; ci++) tcXmlMap.set(cells[ci], rawRowInners[ci]);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
const rawGrid = [];
|
|
811
|
+
for (const tr of rows) {
|
|
812
|
+
const cells = tr.tc ?? [];
|
|
813
|
+
const rawRow = [];
|
|
814
|
+
for (const tc of cells) {
|
|
815
|
+
const tcPr = tc.tcPr;
|
|
816
|
+
const colSpan = parseInt(getVal(tcPr?.gridSpan) ?? "1", 10) || 1;
|
|
817
|
+
const vMergeNode = tcPr?.vMerge;
|
|
818
|
+
let vMerge = "none";
|
|
819
|
+
if (vMergeNode !== void 0) {
|
|
820
|
+
const val = getVal(vMergeNode);
|
|
821
|
+
vMerge = val === "restart" ? "restart" : "continue";
|
|
822
|
+
}
|
|
823
|
+
const tcShd = tcPr?.shd;
|
|
824
|
+
const cellBg = tcShd?.fill && tcShd.fill !== "auto" ? tcShd.fill : void 0;
|
|
825
|
+
rawRow.push({ colSpan, vMerge, rawData: tc, backgroundColor: cellBg });
|
|
826
|
+
}
|
|
827
|
+
rawGrid.push(rawRow);
|
|
828
|
+
}
|
|
829
|
+
const resolved = resolveVMerge(rawGrid);
|
|
830
|
+
const irRows = [];
|
|
831
|
+
for (let ri = 0; ri < resolved.length; ri++) {
|
|
832
|
+
const resolvedRow = resolved[ri];
|
|
833
|
+
const irCells = [];
|
|
834
|
+
for (const cell of resolvedRow) {
|
|
835
|
+
const tc = cell.rawData;
|
|
836
|
+
const cellXml = tcXmlMap.get(tc);
|
|
837
|
+
const cellOrder = cellXml ? blockOrderOf(cellXml) : void 0;
|
|
838
|
+
const cellParaXmls = cellXml ? paragraphChunksOf(cellXml) : void 0;
|
|
839
|
+
const cellTableXmls = cellXml ? tableChunksOf(cellXml) : void 0;
|
|
840
|
+
const cellBlocks = await parseBlockContainer(tc, ctx, cellOrder, cellParaXmls, cellTableXmls);
|
|
841
|
+
const irCell = { rowSpan: cell.rowSpan, colSpan: cell.colSpan, blocks: cellBlocks };
|
|
842
|
+
if (cell.backgroundColor) irCell.backgroundColor = cell.backgroundColor;
|
|
843
|
+
const border = resolveCellBorder(tc.tcPr, uniformBorder);
|
|
844
|
+
if (border) irCell.border = border;
|
|
845
|
+
irCells.push(irCell);
|
|
846
|
+
}
|
|
847
|
+
const irRow = { cells: irCells };
|
|
848
|
+
const trH = rows[ri]?.trPr?.trHeight;
|
|
849
|
+
if (trH?.val != null) {
|
|
850
|
+
const px = Math.round(parseFloat(trH.val) * 96 / 1440);
|
|
851
|
+
if (px > 0) {
|
|
852
|
+
irRow.heightPx = px;
|
|
853
|
+
if (trH.hRule === "exact") irRow.heightExact = true;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
irRows.push(irRow);
|
|
857
|
+
}
|
|
858
|
+
const grid = tbl.tblGrid;
|
|
859
|
+
const colsNode = grid?.gridCol;
|
|
860
|
+
const colArr = Array.isArray(colsNode) ? colsNode : colsNode ? [colsNode] : [];
|
|
861
|
+
const columnWidths = colArr.map((c) => {
|
|
862
|
+
const w = c.w;
|
|
863
|
+
return w != null ? Math.round(parseFloat(w) * 96 / 1440) : 0;
|
|
864
|
+
});
|
|
865
|
+
const tblPr = tbl.tblPr;
|
|
866
|
+
const firstTc = (rows[0]?.tc ?? [])[0];
|
|
867
|
+
const firstTcPr = firstTc?.tcPr;
|
|
868
|
+
const marNode = tblPr?.tblCellMar ?? firstTcPr?.tcMar;
|
|
869
|
+
const cellPadding = marginToPx(marNode);
|
|
870
|
+
const jc = getVal(tblPr?.jc);
|
|
871
|
+
const align = jc === "center" ? "center" : jc === "right" || jc === "end" ? "right" : void 0;
|
|
872
|
+
return {
|
|
873
|
+
type: "table",
|
|
874
|
+
rows: irRows,
|
|
875
|
+
...columnWidths.some((w) => w > 0) ? { columnWidths } : {},
|
|
876
|
+
...cellPadding ? { cellPadding } : {},
|
|
877
|
+
...align ? { align } : {}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function resolveCellBorder(tcPr, uniform) {
|
|
881
|
+
const tcb = extractBorders(tcPr?.tcBorders);
|
|
882
|
+
const side = (s) => s === "none" ? void 0 : s ?? uniform;
|
|
883
|
+
const b = {};
|
|
884
|
+
const top = side(tcb.top), right = side(tcb.right), bottom = side(tcb.bottom), left = side(tcb.left);
|
|
885
|
+
if (top) b.top = top;
|
|
886
|
+
if (right) b.right = right;
|
|
887
|
+
if (bottom) b.bottom = bottom;
|
|
888
|
+
if (left) b.left = left;
|
|
889
|
+
return b.top || b.right || b.bottom || b.left ? b : void 0;
|
|
890
|
+
}
|
|
891
|
+
function pageSizeFromSectPr(sectPr) {
|
|
892
|
+
if (!sectPr) return void 0;
|
|
893
|
+
const twips = (v) => Math.round(parseFloat(String(v)) * 96 / 1440);
|
|
894
|
+
const pgSz = sectPr.pgSz;
|
|
895
|
+
if (!pgSz || pgSz.w == null || pgSz.h == null) return void 0;
|
|
896
|
+
const pgMar = sectPr.pgMar ?? {};
|
|
897
|
+
const m = (v, d) => v != null ? twips(v) : d;
|
|
898
|
+
const size = {
|
|
899
|
+
widthPx: twips(pgSz.w),
|
|
900
|
+
heightPx: twips(pgSz.h),
|
|
901
|
+
marginPx: {
|
|
902
|
+
top: m(pgMar.top, 96),
|
|
903
|
+
right: m(pgMar.right, 96),
|
|
904
|
+
bottom: m(pgMar.bottom, 96),
|
|
905
|
+
left: m(pgMar.left, 96)
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
if (pgMar.footer != null) size.footerPx = twips(pgMar.footer);
|
|
909
|
+
if (pgMar.header != null) size.headerPx = twips(pgMar.header);
|
|
910
|
+
return size;
|
|
911
|
+
}
|
|
912
|
+
function marginToPx(node) {
|
|
913
|
+
if (!node) return void 0;
|
|
914
|
+
const side = (s) => {
|
|
915
|
+
const w = s?.w;
|
|
916
|
+
return w != null ? Math.round(parseFloat(w) * 96 / 1440) : 0;
|
|
917
|
+
};
|
|
918
|
+
return { top: side(node.top), right: side(node.right), bottom: side(node.bottom), left: side(node.left) };
|
|
919
|
+
}
|
|
920
|
+
function bodyInner(xml) {
|
|
921
|
+
const bodyStart = xml.indexOf("<w:body>");
|
|
922
|
+
const bodyEnd = xml.lastIndexOf("</w:body>");
|
|
923
|
+
if (bodyStart === -1 || bodyEnd === -1) return "";
|
|
924
|
+
return xml.slice(bodyStart + 8, bodyEnd);
|
|
925
|
+
}
|
|
926
|
+
function paragraphChunksOf(inner) {
|
|
927
|
+
const chunks = [];
|
|
928
|
+
const re = /<(\/?)w:(p|tbl)[\s>\/]/g;
|
|
929
|
+
let tblDepth = 0;
|
|
930
|
+
let pStart = -1;
|
|
931
|
+
let m;
|
|
932
|
+
while ((m = re.exec(inner)) !== null) {
|
|
933
|
+
const isClose = m[1] === "/";
|
|
934
|
+
const tag = m[2];
|
|
935
|
+
if (tag === "tbl") {
|
|
936
|
+
if (isClose) tblDepth = Math.max(0, tblDepth - 1);
|
|
937
|
+
else tblDepth++;
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (tblDepth !== 0) continue;
|
|
941
|
+
if (!isClose) {
|
|
942
|
+
const gt = inner.indexOf(">", m.index);
|
|
943
|
+
if (gt > 0 && inner[gt - 1] === "/") chunks.push(inner.slice(m.index, gt + 1));
|
|
944
|
+
else if (pStart === -1) pStart = m.index;
|
|
945
|
+
} else if (pStart !== -1) {
|
|
946
|
+
const gt = inner.indexOf(">", m.index);
|
|
947
|
+
chunks.push(inner.slice(pStart, gt + 1));
|
|
948
|
+
pStart = -1;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return chunks;
|
|
952
|
+
}
|
|
953
|
+
function extractParagraphChunks(xml) {
|
|
954
|
+
return paragraphChunksOf(bodyInner(xml));
|
|
955
|
+
}
|
|
956
|
+
function tableChunksOf(inner) {
|
|
957
|
+
const chunks = [];
|
|
958
|
+
const re = /<(\/?)w:tbl[\s>\/]/g;
|
|
959
|
+
let tblDepth = 0;
|
|
960
|
+
let start = -1;
|
|
961
|
+
let m;
|
|
962
|
+
while ((m = re.exec(inner)) !== null) {
|
|
963
|
+
const isClose = m[1] === "/";
|
|
964
|
+
if (!isClose) {
|
|
965
|
+
if (tblDepth === 0) start = m.index;
|
|
966
|
+
tblDepth++;
|
|
967
|
+
} else {
|
|
968
|
+
tblDepth = Math.max(0, tblDepth - 1);
|
|
969
|
+
if (tblDepth === 0 && start !== -1) {
|
|
970
|
+
const gt = inner.indexOf(">", m.index);
|
|
971
|
+
chunks.push(inner.slice(start, gt + 1));
|
|
972
|
+
start = -1;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return chunks;
|
|
977
|
+
}
|
|
978
|
+
function extractTableChunks(xml) {
|
|
979
|
+
return tableChunksOf(bodyInner(xml));
|
|
980
|
+
}
|
|
981
|
+
function extractRowCellInners(tableXml) {
|
|
982
|
+
const rows = [];
|
|
983
|
+
let tblDepth = 0;
|
|
984
|
+
let curRow = null;
|
|
985
|
+
let tcStart = -1;
|
|
986
|
+
const re = /<(\/?)w:(tbl|tr|tc)[\s>\/]/g;
|
|
987
|
+
let m;
|
|
988
|
+
while ((m = re.exec(tableXml)) !== null) {
|
|
989
|
+
const isClose = m[1] === "/";
|
|
990
|
+
const tag = m[2];
|
|
991
|
+
if (tag === "tbl") {
|
|
992
|
+
if (isClose) tblDepth = Math.max(0, tblDepth - 1);
|
|
993
|
+
else tblDepth++;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (tblDepth !== 1) continue;
|
|
997
|
+
if (tag === "tr") {
|
|
998
|
+
if (isClose) {
|
|
999
|
+
if (curRow) {
|
|
1000
|
+
rows.push(curRow);
|
|
1001
|
+
curRow = null;
|
|
1002
|
+
}
|
|
1003
|
+
} else curRow = [];
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (isClose) {
|
|
1007
|
+
if (curRow && tcStart !== -1) {
|
|
1008
|
+
curRow.push(tableXml.slice(tcStart, m.index));
|
|
1009
|
+
tcStart = -1;
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
const gt = tableXml.indexOf(">", m.index);
|
|
1013
|
+
if (gt > 0 && tableXml[gt - 1] === "/") {
|
|
1014
|
+
if (curRow) curRow.push("");
|
|
1015
|
+
} else tcStart = gt + 1;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return rows;
|
|
1019
|
+
}
|
|
1020
|
+
function getRunOrder(paraXml) {
|
|
1021
|
+
let body = paraXml;
|
|
1022
|
+
const pprEnd = body.indexOf("</w:pPr>");
|
|
1023
|
+
if (pprEnd !== -1) body = body.slice(pprEnd + 8);
|
|
1024
|
+
const order = [];
|
|
1025
|
+
const re = /<(\/?)w:(r|hyperlink|ins|del|moveTo|moveFrom|fldSimple)[\s>\/]/g;
|
|
1026
|
+
let depth = 0;
|
|
1027
|
+
let inRun = false;
|
|
1028
|
+
let m;
|
|
1029
|
+
while ((m = re.exec(body)) !== null) {
|
|
1030
|
+
const isClose = m[1] === "/";
|
|
1031
|
+
const tag = m[2];
|
|
1032
|
+
if (tag === "r") {
|
|
1033
|
+
if (isClose) inRun = false;
|
|
1034
|
+
else {
|
|
1035
|
+
if (depth === 0) order.push("r");
|
|
1036
|
+
inRun = true;
|
|
1037
|
+
}
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (inRun) continue;
|
|
1041
|
+
if (isClose) {
|
|
1042
|
+
depth = Math.max(0, depth - 1);
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
const gt = body.indexOf(">", m.index);
|
|
1046
|
+
const selfClose = gt > 0 && body[gt - 1] === "/";
|
|
1047
|
+
if (depth === 0) order.push(tag);
|
|
1048
|
+
if (!selfClose) depth++;
|
|
1049
|
+
}
|
|
1050
|
+
return order;
|
|
1051
|
+
}
|
|
1052
|
+
function blockOrderOf(inner) {
|
|
1053
|
+
const order = [];
|
|
1054
|
+
const re = /<(\/?)w:(p|tbl)[\s>\/]/g;
|
|
1055
|
+
let tblDepth = 0;
|
|
1056
|
+
let m;
|
|
1057
|
+
while ((m = re.exec(inner)) !== null) {
|
|
1058
|
+
const isClose = m[1] === "/";
|
|
1059
|
+
const tag = m[2];
|
|
1060
|
+
if (isClose) {
|
|
1061
|
+
if (tag === "tbl") tblDepth = Math.max(0, tblDepth - 1);
|
|
1062
|
+
} else {
|
|
1063
|
+
if (tblDepth === 0) order.push(tag);
|
|
1064
|
+
if (tag === "tbl") tblDepth++;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return order;
|
|
1068
|
+
}
|
|
1069
|
+
function getBodyBlockOrder(xml) {
|
|
1070
|
+
return blockOrderOf(bodyInner(xml));
|
|
1071
|
+
}
|
|
1072
|
+
async function parseBlockContainer(container, ctx, order, paraXmls, tableXmls) {
|
|
1073
|
+
const ps = container.p ?? [];
|
|
1074
|
+
const tbls = container.tbl ?? [];
|
|
1075
|
+
const xmls = paraXmls && paraXmls.length === ps.length ? paraXmls : void 0;
|
|
1076
|
+
const tXmls = tableXmls && tableXmls.length === tbls.length ? tableXmls : void 0;
|
|
1077
|
+
if (!order || order.length === 0) {
|
|
1078
|
+
const blocks2 = [];
|
|
1079
|
+
for (let i = 0; i < ps.length; i++) blocks2.push(...await parseParagraph(ps[i], ctx, xmls?.[i]));
|
|
1080
|
+
for (let i = 0; i < tbls.length; i++) blocks2.push(await parseTable(tbls[i], ctx, tXmls?.[i]));
|
|
1081
|
+
return blocks2;
|
|
1082
|
+
}
|
|
1083
|
+
const blocks = [];
|
|
1084
|
+
let pIdx = 0, tblIdx = 0;
|
|
1085
|
+
for (const type of order) {
|
|
1086
|
+
if (type === "p" && pIdx < ps.length) {
|
|
1087
|
+
blocks.push(...await parseParagraph(ps[pIdx], ctx, xmls?.[pIdx]));
|
|
1088
|
+
pIdx++;
|
|
1089
|
+
} else if (type === "tbl" && tblIdx < tbls.length) {
|
|
1090
|
+
blocks.push(await parseTable(tbls[tblIdx], ctx, tXmls?.[tblIdx]));
|
|
1091
|
+
tblIdx++;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return blocks;
|
|
1095
|
+
}
|
|
1096
|
+
function inlineOmmlText(xml) {
|
|
1097
|
+
xml = xml.replace(/<m:oMath\b[\s\S]*?<\/m:oMath>/g, (frag) => {
|
|
1098
|
+
const text = (frag.match(/<m:t\b[^>]*>([\s\S]*?)<\/m:t>/g) || []).map((t) => t.replace(/<[^>]+>/g, "")).join("");
|
|
1099
|
+
return text ? `<w:r><w:t xml:space="preserve">${text}</w:t></w:r>` : "";
|
|
1100
|
+
});
|
|
1101
|
+
return xml.replace(/<m:oMathParaPr\b[^>]*\/>/g, "").replace(/<m:oMathParaPr\b[\s\S]*?<\/m:oMathParaPr>/g, "").replace(/<\/?m:oMathPara\b[^>]*>/g, "");
|
|
1102
|
+
}
|
|
1103
|
+
function stripTransparentWrappers(xml) {
|
|
1104
|
+
return xml.replace(/<w:sdtPr\b[^>]*\/>/g, "").replace(/<w:sdtPr\b[\s\S]*?<\/w:sdtPr>/g, "").replace(/<w:sdtEndPr\b[^>]*\/>/g, "").replace(/<w:sdtEndPr\b[\s\S]*?<\/w:sdtEndPr>/g, "").replace(/<w:smartTagPr\b[^>]*\/>/g, "").replace(/<w:smartTagPr\b[\s\S]*?<\/w:smartTagPr>/g, "").replace(/<w:customXmlPr\b[^>]*\/>/g, "").replace(/<w:customXmlPr\b[\s\S]*?<\/w:customXmlPr>/g, "").replace(/<\/?w:sdtContent\b[^>]*>/g, "").replace(/<\/?w:sdt\b[^>]*>/g, "").replace(/<\/?w:smartTag\b[^>]*>/g, "").replace(/<\/?w:customXml\b[^>]*>/g, "");
|
|
1105
|
+
}
|
|
1106
|
+
async function parseDocument(xml, ctx) {
|
|
1107
|
+
xml = stripTransparentWrappers(inlineOmmlText(xml));
|
|
1108
|
+
const doc = parser4.parse(xml);
|
|
1109
|
+
const body = doc?.document?.body;
|
|
1110
|
+
if (!body) return [];
|
|
1111
|
+
const order = getBodyBlockOrder(xml);
|
|
1112
|
+
const paraXmls = extractParagraphChunks(xml);
|
|
1113
|
+
const tableXmls = extractTableChunks(xml);
|
|
1114
|
+
return parseBlockContainer(body, ctx, order, paraXmls, tableXmls);
|
|
1115
|
+
}
|
|
1116
|
+
async function parseFooterXml(xml, ctx) {
|
|
1117
|
+
xml = stripTransparentWrappers(inlineOmmlText(xml));
|
|
1118
|
+
const doc = parser4.parse(xml);
|
|
1119
|
+
const ftr = doc?.ftr;
|
|
1120
|
+
if (!ftr) return [];
|
|
1121
|
+
return parseBlockContainer(ftr, ctx);
|
|
1122
|
+
}
|
|
1123
|
+
async function parseHeaderXml(xml, ctx) {
|
|
1124
|
+
xml = stripTransparentWrappers(inlineOmmlText(xml));
|
|
1125
|
+
const doc = parser4.parse(xml);
|
|
1126
|
+
const hdr = doc?.hdr;
|
|
1127
|
+
if (!hdr) return [];
|
|
1128
|
+
return parseBlockContainer(hdr, ctx);
|
|
1129
|
+
}
|
|
1130
|
+
async function parseNotesXml(xml, kind, ctx) {
|
|
1131
|
+
const map = /* @__PURE__ */ new Map();
|
|
1132
|
+
xml = stripTransparentWrappers(inlineOmmlText(xml));
|
|
1133
|
+
const doc = parser4.parse(xml);
|
|
1134
|
+
const root = doc?.[`${kind}s`];
|
|
1135
|
+
const notes = root?.[kind] ?? [];
|
|
1136
|
+
for (const note of notes) {
|
|
1137
|
+
const id = String(note.id ?? "");
|
|
1138
|
+
const type = note.type;
|
|
1139
|
+
if (type === "separator" || type === "continuationSeparator") continue;
|
|
1140
|
+
map.set(id, await parseBlockContainer(note, ctx));
|
|
1141
|
+
}
|
|
1142
|
+
return map;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/renderer/layout.ts
|
|
1146
|
+
var EMPTY_LINE_EM = 1.7;
|
|
1147
|
+
var LINE_HEIGHT = 1.15;
|
|
1148
|
+
var HEADING_MIN_PT = 24;
|
|
1149
|
+
function extractPageBackground(block) {
|
|
1150
|
+
for (const run of block.runs) {
|
|
1151
|
+
if (run.type === "image" && run.isPageBackground) return run;
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
function isWatermark(img, pw, ph) {
|
|
1156
|
+
if (img.isPageBackground) return false;
|
|
1157
|
+
return img.heightPx >= ph * 0.4 && img.widthPx < pw * 0.85;
|
|
1158
|
+
}
|
|
1159
|
+
function isPageLevelImage(run, pw, ph) {
|
|
1160
|
+
return run.type === "image" && (run.isPageBackground || isWatermark(run, pw, ph));
|
|
1161
|
+
}
|
|
1162
|
+
function flowOnly(block, pw, ph) {
|
|
1163
|
+
const hadPageImage = block.runs.some((r) => isPageLevelImage(r, pw, ph));
|
|
1164
|
+
const runs = block.runs.filter((r) => !isPageLevelImage(r, pw, ph));
|
|
1165
|
+
if (hadPageImage) {
|
|
1166
|
+
const hasContent = runs.some((r) => r.type !== "run" || r.text.length > 0);
|
|
1167
|
+
if (!hasContent) return null;
|
|
1168
|
+
}
|
|
1169
|
+
return { ...block, runs };
|
|
1170
|
+
}
|
|
1171
|
+
function watermarksOf(block, pw, ph) {
|
|
1172
|
+
if (block.type !== "paragraph") return [];
|
|
1173
|
+
return block.runs.filter(
|
|
1174
|
+
(r) => r.type === "image" && isWatermark(r, pw, ph)
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
function isBlockVisible(block) {
|
|
1178
|
+
if (block.type === "table") return true;
|
|
1179
|
+
for (const run of block.runs) {
|
|
1180
|
+
if (run.type === "image") return true;
|
|
1181
|
+
if (run.text.trim().length > 0) return true;
|
|
1182
|
+
}
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
function headingImage(block) {
|
|
1186
|
+
if (block.type !== "paragraph") return false;
|
|
1187
|
+
for (const run of block.runs) {
|
|
1188
|
+
if (run.type !== "image") continue;
|
|
1189
|
+
const img = run;
|
|
1190
|
+
if (img.isPageBackground) continue;
|
|
1191
|
+
if (img.widthPx >= 300 && img.heightPx <= 70 && img.widthPx / img.heightPx >= 5) {
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
function isHeadingBlock(block) {
|
|
1198
|
+
if (block.type !== "paragraph") return false;
|
|
1199
|
+
if (headingImage(block)) return true;
|
|
1200
|
+
for (const run of block.runs) {
|
|
1201
|
+
if (run.type === "run" && run.text.trim() && (run.style.fontSize ?? 0) >= HEADING_MIN_PT) {
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
function isIconOnly(block) {
|
|
1208
|
+
if (block.type !== "paragraph") return false;
|
|
1209
|
+
let img = null;
|
|
1210
|
+
for (const run of block.runs) {
|
|
1211
|
+
if (run.type === "image") {
|
|
1212
|
+
if (img) return false;
|
|
1213
|
+
img = run;
|
|
1214
|
+
} else if (run.text.trim().length > 0) {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return !!img && img.widthPx <= 150 && img.heightPx <= 150 && !img.isPageBackground;
|
|
1219
|
+
}
|
|
1220
|
+
function fullPageImage(blocks, pageWidthPx) {
|
|
1221
|
+
let found = null;
|
|
1222
|
+
for (const block of blocks) {
|
|
1223
|
+
if (block.type !== "paragraph") {
|
|
1224
|
+
if (isBlockVisible(block)) return null;
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
for (const run of block.runs) {
|
|
1228
|
+
if (run.type === "image") {
|
|
1229
|
+
if (found) return null;
|
|
1230
|
+
found = run;
|
|
1231
|
+
} else if (run.text.trim().length > 0) {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return found && found.widthPx >= pageWidthPx * 0.85 ? found : null;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/renderer/html.ts
|
|
1240
|
+
function styleToCss(s) {
|
|
1241
|
+
const parts = [];
|
|
1242
|
+
if (s.bold) parts.push("font-weight:bold");
|
|
1243
|
+
if (s.italic) parts.push("font-style:italic");
|
|
1244
|
+
const deco = [s.underline ? "underline" : "", s.strike ? "line-through" : ""].filter(Boolean);
|
|
1245
|
+
if (deco.length) parts.push(`text-decoration:${deco.join(" ")}`);
|
|
1246
|
+
if (s.vertAlign) {
|
|
1247
|
+
parts.push(`vertical-align:${s.vertAlign}`);
|
|
1248
|
+
parts.push("font-size:0.83em");
|
|
1249
|
+
} else if (s.fontSize != null) {
|
|
1250
|
+
parts.push(`font-size:${s.fontSize}pt`);
|
|
1251
|
+
}
|
|
1252
|
+
if (s.fontFamily) parts.push(`font-family:${s.fontFamily},sans-serif`);
|
|
1253
|
+
if (s.color) parts.push(`color:#${s.color}`);
|
|
1254
|
+
if (s.alignment) parts.push(`text-align:${s.alignment}`);
|
|
1255
|
+
if (s.backgroundColor) parts.push(`background-color:#${s.backgroundColor}`);
|
|
1256
|
+
if (s.highlight) parts.push(`background-color:${s.highlight}`);
|
|
1257
|
+
return parts.join(";");
|
|
1258
|
+
}
|
|
1259
|
+
function renderRun(run, parent, skipLeadingTabs = false) {
|
|
1260
|
+
const href = run.href;
|
|
1261
|
+
let target = parent;
|
|
1262
|
+
if (href) {
|
|
1263
|
+
const a = document.createElement("a");
|
|
1264
|
+
a.setAttribute("href", sanitizeHref(href));
|
|
1265
|
+
a.target = "_blank";
|
|
1266
|
+
a.rel = "noopener noreferrer";
|
|
1267
|
+
parent.appendChild(a);
|
|
1268
|
+
target = a;
|
|
1269
|
+
}
|
|
1270
|
+
if (run.type === "image") {
|
|
1271
|
+
const img = document.createElement("img");
|
|
1272
|
+
setImageSrc(img, run.src);
|
|
1273
|
+
img.width = run.widthPx;
|
|
1274
|
+
img.height = run.heightPx;
|
|
1275
|
+
img.style.display = "inline-block";
|
|
1276
|
+
img.style.maxWidth = "100%";
|
|
1277
|
+
target.appendChild(img);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const textRun = run;
|
|
1281
|
+
if (textRun.deleted) {
|
|
1282
|
+
if (!showRevisions) return;
|
|
1283
|
+
const del = document.createElement("del");
|
|
1284
|
+
del.style.cssText = "color:#c0392b";
|
|
1285
|
+
target.appendChild(del);
|
|
1286
|
+
target = del;
|
|
1287
|
+
} else if (textRun.inserted && showRevisions) {
|
|
1288
|
+
const ins = document.createElement("ins");
|
|
1289
|
+
ins.style.cssText = "color:#1d6f42;text-decoration:underline";
|
|
1290
|
+
target.appendChild(ins);
|
|
1291
|
+
target = ins;
|
|
1292
|
+
}
|
|
1293
|
+
if (textRun.pageNumber) {
|
|
1294
|
+
target.appendChild(document.createTextNode(String(currentPageNumber)));
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (textRun.totalPages) {
|
|
1298
|
+
const span = document.createElement("span");
|
|
1299
|
+
span.dataset.ssdNumpages = "1";
|
|
1300
|
+
span.textContent = "1";
|
|
1301
|
+
target.appendChild(span);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (textRun.noteRef) {
|
|
1305
|
+
const { type, number } = textRun.noteRef;
|
|
1306
|
+
const prefix = type === "footnote" ? "fn" : "en";
|
|
1307
|
+
const sup = document.createElement("sup");
|
|
1308
|
+
sup.id = `${prefix}ref-${number}`;
|
|
1309
|
+
const a = document.createElement("a");
|
|
1310
|
+
a.setAttribute("href", `#${prefix}-${number}`);
|
|
1311
|
+
a.textContent = String(number);
|
|
1312
|
+
sup.appendChild(a);
|
|
1313
|
+
target.appendChild(sup);
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
for (let t = 0; !skipLeadingTabs && t < (textRun.tabs ?? 0); t++) {
|
|
1317
|
+
const sp = document.createElement("span");
|
|
1318
|
+
sp.style.cssText = "display:inline-block;min-width:2.5em";
|
|
1319
|
+
target.appendChild(sp);
|
|
1320
|
+
}
|
|
1321
|
+
const css = styleToCss(textRun.style);
|
|
1322
|
+
if (textRun.text) {
|
|
1323
|
+
if (css || textRun.style.rtl) {
|
|
1324
|
+
const span = document.createElement("span");
|
|
1325
|
+
if (css) span.style.cssText = css;
|
|
1326
|
+
if (textRun.style.rtl) span.dir = "rtl";
|
|
1327
|
+
span.textContent = textRun.text;
|
|
1328
|
+
target.appendChild(span);
|
|
1329
|
+
} else {
|
|
1330
|
+
target.appendChild(document.createTextNode(textRun.text));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (textRun.lineBreak) target.appendChild(document.createElement("br"));
|
|
1334
|
+
}
|
|
1335
|
+
function renderNotes(doc, container, opts = { footnotes: true, endnotes: true }) {
|
|
1336
|
+
const all = [];
|
|
1337
|
+
if (opts.footnotes) all.push({ prefix: "fn", label: "Footnotes", notes: doc.footnotes });
|
|
1338
|
+
if (opts.endnotes) all.push({ prefix: "en", label: "Endnotes", notes: doc.endnotes });
|
|
1339
|
+
for (const { prefix, label, notes } of all) {
|
|
1340
|
+
if (!notes || notes.length === 0) continue;
|
|
1341
|
+
const section = document.createElement("section");
|
|
1342
|
+
section.className = `ssd-${prefix === "fn" ? "footnotes" : "endnotes"}`;
|
|
1343
|
+
const hr = document.createElement("hr");
|
|
1344
|
+
section.appendChild(hr);
|
|
1345
|
+
const heading = document.createElement("h2");
|
|
1346
|
+
heading.textContent = label;
|
|
1347
|
+
heading.style.cssText = "font-size:1em";
|
|
1348
|
+
section.appendChild(heading);
|
|
1349
|
+
const ol = document.createElement("ol");
|
|
1350
|
+
for (const note of notes) {
|
|
1351
|
+
const li = document.createElement("li");
|
|
1352
|
+
li.id = `${prefix}-${note.number}`;
|
|
1353
|
+
renderBlocks(note.blocks, li);
|
|
1354
|
+
const back = document.createElement("a");
|
|
1355
|
+
back.setAttribute("href", `#${prefix}ref-${note.number}`);
|
|
1356
|
+
back.textContent = " \u21A9";
|
|
1357
|
+
li.appendChild(back);
|
|
1358
|
+
ol.appendChild(li);
|
|
1359
|
+
}
|
|
1360
|
+
section.appendChild(ol);
|
|
1361
|
+
container.appendChild(section);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
function sanitizeHref(href) {
|
|
1365
|
+
const trimmed = href.trim();
|
|
1366
|
+
if (/^(#|\/|\.|[^:]*$)/.test(trimmed)) return trimmed;
|
|
1367
|
+
if (/^(https?:|mailto:|tel:)/i.test(trimmed)) return trimmed;
|
|
1368
|
+
return "#";
|
|
1369
|
+
}
|
|
1370
|
+
function sanitizeImageSrc(src) {
|
|
1371
|
+
const trimmed = src.trim();
|
|
1372
|
+
if (/^data:image\//i.test(trimmed)) return trimmed;
|
|
1373
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
1374
|
+
if (/^(\/|\.|[^:]*$)/.test(trimmed)) return trimmed;
|
|
1375
|
+
return "";
|
|
1376
|
+
}
|
|
1377
|
+
function setImageSrc(img, src) {
|
|
1378
|
+
const safe = sanitizeImageSrc(src);
|
|
1379
|
+
if (safe) img.src = safe;
|
|
1380
|
+
}
|
|
1381
|
+
var emptyLineEm = LINE_HEIGHT;
|
|
1382
|
+
var showRevisions = false;
|
|
1383
|
+
function ensureLineBox(el) {
|
|
1384
|
+
if (!el.textContent && !el.querySelector("img")) {
|
|
1385
|
+
el.style.minHeight = `${emptyLineEm}em`;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
var inTableCell = false;
|
|
1389
|
+
var currentPageNumber = 0;
|
|
1390
|
+
var FOOTNOTE_SEPARATOR_H = 12;
|
|
1391
|
+
function footnoteNumbersIn(el) {
|
|
1392
|
+
return Array.from(el.querySelectorAll('sup[id^="fnref-"]')).map((s) => parseInt(s.id.slice("fnref-".length), 10)).filter((n) => !Number.isNaN(n));
|
|
1393
|
+
}
|
|
1394
|
+
function buildFootnoteItem(fn) {
|
|
1395
|
+
const item = document.createElement("div");
|
|
1396
|
+
item.style.cssText = "display:flex;gap:6px;align-items:baseline";
|
|
1397
|
+
const num = document.createElement("span");
|
|
1398
|
+
num.id = `fn-${fn.number}`;
|
|
1399
|
+
num.textContent = String(fn.number);
|
|
1400
|
+
num.style.flex = "0 0 auto";
|
|
1401
|
+
const content = document.createElement("div");
|
|
1402
|
+
renderBlocks(fn.blocks, content);
|
|
1403
|
+
const back = document.createElement("a");
|
|
1404
|
+
back.setAttribute("href", `#fnref-${fn.number}`);
|
|
1405
|
+
back.textContent = " \u21A9";
|
|
1406
|
+
content.appendChild(back);
|
|
1407
|
+
item.append(num, content);
|
|
1408
|
+
return item;
|
|
1409
|
+
}
|
|
1410
|
+
function renderPageFootnotes(footnotes, pageDiv, numbers, pm) {
|
|
1411
|
+
if (numbers.length === 0) return;
|
|
1412
|
+
const box = document.createElement("div");
|
|
1413
|
+
box.className = "ssd-footnotes";
|
|
1414
|
+
box.style.cssText = `position:absolute;left:${pm.left}px;right:${pm.right}px;bottom:${pm.bottom}px;font-size:9pt;line-height:1.3`;
|
|
1415
|
+
const sep = document.createElement("div");
|
|
1416
|
+
sep.style.cssText = "border-top:1px solid #999;width:33%;margin-bottom:4px";
|
|
1417
|
+
box.appendChild(sep);
|
|
1418
|
+
for (const n of numbers) {
|
|
1419
|
+
const fn = footnotes.find((f) => f.number === n);
|
|
1420
|
+
if (fn) box.appendChild(buildFootnoteItem(fn));
|
|
1421
|
+
}
|
|
1422
|
+
pageDiv.appendChild(box);
|
|
1423
|
+
}
|
|
1424
|
+
function pickPart(doc, pageNum, def, first, even) {
|
|
1425
|
+
if (doc.titlePg && pageNum === 1 && first) return first;
|
|
1426
|
+
if (doc.evenAndOddHeaders && pageNum % 2 === 0 && even) return even;
|
|
1427
|
+
return def;
|
|
1428
|
+
}
|
|
1429
|
+
function renderFooter(doc, pageDiv, pageNum, pm, footerPx) {
|
|
1430
|
+
const footer = pickPart(doc, pageNum, doc.footer, doc.footerFirst, doc.footerEven);
|
|
1431
|
+
if (!footer || footer.length === 0) return;
|
|
1432
|
+
const prev = currentPageNumber;
|
|
1433
|
+
currentPageNumber = pageNum;
|
|
1434
|
+
const el = document.createElement("div");
|
|
1435
|
+
el.className = "ssd-footer";
|
|
1436
|
+
el.style.cssText = `position:absolute;left:${pm.left}px;right:${pm.right}px;bottom:${footerPx}px`;
|
|
1437
|
+
renderBlocks(footer, el);
|
|
1438
|
+
pageDiv.appendChild(el);
|
|
1439
|
+
currentPageNumber = prev;
|
|
1440
|
+
}
|
|
1441
|
+
function fillTotalPages(container, total) {
|
|
1442
|
+
container.querySelectorAll("[data-ssd-numpages]").forEach((el) => {
|
|
1443
|
+
el.textContent = String(total);
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
function renderHeader(doc, pageDiv, pageNum, pm, headerPx) {
|
|
1447
|
+
const header = pickPart(doc, pageNum, doc.header, doc.headerFirst, doc.headerEven);
|
|
1448
|
+
if (!header || header.length === 0) return;
|
|
1449
|
+
const prev = currentPageNumber;
|
|
1450
|
+
currentPageNumber = pageNum;
|
|
1451
|
+
const el = document.createElement("div");
|
|
1452
|
+
el.className = "ssd-header";
|
|
1453
|
+
el.style.cssText = `position:absolute;left:${pm.left}px;right:${pm.right}px;top:${headerPx}px`;
|
|
1454
|
+
renderBlocks(header, el);
|
|
1455
|
+
pageDiv.appendChild(el);
|
|
1456
|
+
currentPageNumber = prev;
|
|
1457
|
+
}
|
|
1458
|
+
function paragraphCss(style, skipIndent = false) {
|
|
1459
|
+
const mt = inTableCell ? 0 : style.spaceBefore ?? 0;
|
|
1460
|
+
const mb = inTableCell ? 0 : style.spaceAfter ?? 0;
|
|
1461
|
+
const lh = style.lineHeightPx != null ? `${style.lineHeightPx}px` : `${style.lineHeight ?? LINE_HEIGHT}`;
|
|
1462
|
+
let css = `margin:${mt}px 0 ${mb}px;line-height:${lh}`;
|
|
1463
|
+
if (!skipIndent) {
|
|
1464
|
+
if (style.indentLeft) css += `;padding-left:${style.indentLeft}px`;
|
|
1465
|
+
if (style.indentRight) css += `;padding-right:${style.indentRight}px`;
|
|
1466
|
+
if (style.indentFirstLine) css += `;text-indent:${style.indentFirstLine}px`;
|
|
1467
|
+
else if (style.indentHanging) css += `;text-indent:${-style.indentHanging}px`;
|
|
1468
|
+
}
|
|
1469
|
+
if (style.borderTop) css += `;border-top:${style.borderTop}`;
|
|
1470
|
+
if (style.borderBottom) css += `;border-bottom:${style.borderBottom}`;
|
|
1471
|
+
if (style.borderLeft) css += `;border-left:${style.borderLeft}`;
|
|
1472
|
+
if (style.borderRight) css += `;border-right:${style.borderRight}`;
|
|
1473
|
+
const inline = styleToCss(style);
|
|
1474
|
+
if (inline) css += ";" + inline;
|
|
1475
|
+
return css;
|
|
1476
|
+
}
|
|
1477
|
+
function renderParagraph(block) {
|
|
1478
|
+
const p = document.createElement("p");
|
|
1479
|
+
p.style.cssText = paragraphCss(block.style);
|
|
1480
|
+
if (block.style.rtl) p.dir = "rtl";
|
|
1481
|
+
const stops = block.style.tabStops;
|
|
1482
|
+
const hasTabRun = block.runs.some((r) => r.type === "run" && (r.tabs ?? 0) > 0);
|
|
1483
|
+
const alignStop = stops?.some((s) => s.val === "right" || s.val === "center" || s.val === "decimal");
|
|
1484
|
+
if (hasTabRun && stops && alignStop) {
|
|
1485
|
+
renderTabbedParagraph(block, p, stops);
|
|
1486
|
+
} else {
|
|
1487
|
+
for (const run of block.runs) renderRun(run, p);
|
|
1488
|
+
}
|
|
1489
|
+
ensureLineBox(p);
|
|
1490
|
+
if (block.pageBreakBefore) p.dataset.ssdBreak = "1";
|
|
1491
|
+
return p;
|
|
1492
|
+
}
|
|
1493
|
+
function renderTabbedParagraph(block, p, stops) {
|
|
1494
|
+
p.style.display = "flex";
|
|
1495
|
+
p.style.alignItems = "baseline";
|
|
1496
|
+
p.style.width = "100%";
|
|
1497
|
+
const segments = [];
|
|
1498
|
+
const seps = [];
|
|
1499
|
+
let stopIdx = 0;
|
|
1500
|
+
let current = document.createElement("span");
|
|
1501
|
+
current.style.cssText = "flex:0 1 auto;min-width:0";
|
|
1502
|
+
segments.push(current);
|
|
1503
|
+
for (const run of block.runs) {
|
|
1504
|
+
let leadTabs = run.type === "run" ? run.tabs ?? 0 : 0;
|
|
1505
|
+
while (leadTabs-- > 0) {
|
|
1506
|
+
seps.push(stops[Math.min(stopIdx, stops.length - 1)]);
|
|
1507
|
+
stopIdx++;
|
|
1508
|
+
current = document.createElement("span");
|
|
1509
|
+
current.style.cssText = "flex:0 1 auto;min-width:0";
|
|
1510
|
+
segments.push(current);
|
|
1511
|
+
}
|
|
1512
|
+
renderRun(
|
|
1513
|
+
run,
|
|
1514
|
+
current,
|
|
1515
|
+
/* skipLeadingTabs */
|
|
1516
|
+
true
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
const last = segments[segments.length - 1];
|
|
1520
|
+
if (segments.length > 1) last.style.cssText = "flex:0 0 auto;white-space:nowrap";
|
|
1521
|
+
p.appendChild(segments[0]);
|
|
1522
|
+
for (let i = 1; i < segments.length; i++) {
|
|
1523
|
+
p.appendChild(makeLeader(seps[i - 1]));
|
|
1524
|
+
p.appendChild(segments[i]);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function makeLeader(stop) {
|
|
1528
|
+
const el = document.createElement("span");
|
|
1529
|
+
const grows = stop.val === "right" || stop.val === "center" || stop.val === "decimal";
|
|
1530
|
+
const border = stop.leader === "dot" ? "border-bottom:2px dotted currentColor" : stop.leader === "hyphen" ? "border-bottom:1px dashed currentColor" : stop.leader === "underscore" ? "border-bottom:1px solid currentColor" : "";
|
|
1531
|
+
if (grows) {
|
|
1532
|
+
el.style.cssText = `flex:1 1 0;margin:0 4px;align-self:flex-end;transform:translateY(-0.35em);${border}`;
|
|
1533
|
+
} else {
|
|
1534
|
+
el.style.cssText = "display:inline-block;flex:0 0 auto;min-width:2.5em";
|
|
1535
|
+
}
|
|
1536
|
+
return el;
|
|
1537
|
+
}
|
|
1538
|
+
function renderTable(block, container) {
|
|
1539
|
+
const table = document.createElement("table");
|
|
1540
|
+
table.style.borderCollapse = "collapse";
|
|
1541
|
+
table.style.maxWidth = "100%";
|
|
1542
|
+
if (block.columnWidths && block.columnWidths.length > 0) {
|
|
1543
|
+
const total = block.columnWidths.reduce((a, b) => a + b, 0);
|
|
1544
|
+
if (total > 0) {
|
|
1545
|
+
table.style.tableLayout = "fixed";
|
|
1546
|
+
table.style.width = `${total}px`;
|
|
1547
|
+
if (block.align === "center") table.style.margin = "0 auto";
|
|
1548
|
+
else if (block.align === "right") table.style.marginLeft = "auto";
|
|
1549
|
+
const colgroup = document.createElement("colgroup");
|
|
1550
|
+
for (const w of block.columnWidths) {
|
|
1551
|
+
const col = document.createElement("col");
|
|
1552
|
+
col.style.width = `${w}px`;
|
|
1553
|
+
colgroup.appendChild(col);
|
|
1554
|
+
}
|
|
1555
|
+
table.appendChild(colgroup);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
const pad = block.cellPadding;
|
|
1559
|
+
for (const row of block.rows) {
|
|
1560
|
+
const tr = document.createElement("tr");
|
|
1561
|
+
if (row.heightPx) tr.style.height = `${row.heightPx}px`;
|
|
1562
|
+
for (const cell of row.cells) {
|
|
1563
|
+
const td = document.createElement("td");
|
|
1564
|
+
if (cell.colSpan > 1) td.colSpan = cell.colSpan;
|
|
1565
|
+
if (cell.rowSpan > 1) td.rowSpan = cell.rowSpan;
|
|
1566
|
+
if (cell.backgroundColor) td.style.backgroundColor = `#${cell.backgroundColor}`;
|
|
1567
|
+
if (cell.border) {
|
|
1568
|
+
if (cell.border.top) td.style.borderTop = cell.border.top;
|
|
1569
|
+
if (cell.border.right) td.style.borderRight = cell.border.right;
|
|
1570
|
+
if (cell.border.bottom) td.style.borderBottom = cell.border.bottom;
|
|
1571
|
+
if (cell.border.left) td.style.borderLeft = cell.border.left;
|
|
1572
|
+
}
|
|
1573
|
+
if (pad) td.style.padding = `${pad.top}px ${pad.right}px ${pad.bottom}px ${pad.left}px`;
|
|
1574
|
+
td.style.verticalAlign = "top";
|
|
1575
|
+
td.style.overflowWrap = "break-word";
|
|
1576
|
+
td.style.wordBreak = "break-word";
|
|
1577
|
+
const prev = inTableCell;
|
|
1578
|
+
inTableCell = true;
|
|
1579
|
+
renderBlocks(cell.blocks, td);
|
|
1580
|
+
inTableCell = prev;
|
|
1581
|
+
tr.appendChild(td);
|
|
1582
|
+
}
|
|
1583
|
+
table.appendChild(tr);
|
|
1584
|
+
}
|
|
1585
|
+
container.appendChild(table);
|
|
1586
|
+
}
|
|
1587
|
+
var LIST_STYLE = {
|
|
1588
|
+
decimal: "decimal",
|
|
1589
|
+
decimalZero: "decimal-leading-zero",
|
|
1590
|
+
lowerLetter: "lower-alpha",
|
|
1591
|
+
upperLetter: "upper-alpha",
|
|
1592
|
+
lowerRoman: "lower-roman",
|
|
1593
|
+
upperRoman: "upper-roman",
|
|
1594
|
+
bullet: "disc",
|
|
1595
|
+
none: "none"
|
|
1596
|
+
};
|
|
1597
|
+
function renderBlocks(blocks, container) {
|
|
1598
|
+
let stack = [];
|
|
1599
|
+
const closeLists = () => {
|
|
1600
|
+
if (stack.length > 0) container.appendChild(stack[0].el);
|
|
1601
|
+
stack = [];
|
|
1602
|
+
};
|
|
1603
|
+
for (const block of blocks) {
|
|
1604
|
+
if (block.type === "paragraph" && block.list) {
|
|
1605
|
+
const { numId, ordered, start, ilvl, format } = block.list;
|
|
1606
|
+
if (stack.length > 0 && stack[0].numId !== numId) closeLists();
|
|
1607
|
+
while (stack.length > ilvl + 1) stack.pop();
|
|
1608
|
+
while (stack.length < ilvl + 1) {
|
|
1609
|
+
const listEl = document.createElement(ordered ? "ol" : "ul");
|
|
1610
|
+
if (ordered) listEl.start = start;
|
|
1611
|
+
const styleType = LIST_STYLE[format];
|
|
1612
|
+
if (styleType) listEl.style.listStyleType = styleType;
|
|
1613
|
+
const indent = block.style.indentLeft ?? 24;
|
|
1614
|
+
listEl.style.margin = "0";
|
|
1615
|
+
listEl.style.paddingLeft = `${indent}px`;
|
|
1616
|
+
const parent = stack[stack.length - 1];
|
|
1617
|
+
if (parent) (parent.lastLi ?? parent.el).appendChild(listEl);
|
|
1618
|
+
stack.push({ el: listEl, numId, lastLi: null });
|
|
1619
|
+
}
|
|
1620
|
+
const frame = stack[stack.length - 1];
|
|
1621
|
+
const li = document.createElement("li");
|
|
1622
|
+
li.style.cssText = paragraphCss(block.style, true);
|
|
1623
|
+
for (const run of block.runs) renderRun(run, li);
|
|
1624
|
+
frame.el.appendChild(li);
|
|
1625
|
+
frame.lastLi = li;
|
|
1626
|
+
} else {
|
|
1627
|
+
closeLists();
|
|
1628
|
+
if (block.type === "paragraph") {
|
|
1629
|
+
container.appendChild(renderParagraph(block));
|
|
1630
|
+
} else if (block.type === "table") {
|
|
1631
|
+
renderTable(block, container);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
closeLists();
|
|
1636
|
+
}
|
|
1637
|
+
function splitTableRows(table, availH) {
|
|
1638
|
+
const rows = Array.from(table.rows);
|
|
1639
|
+
let used = 0;
|
|
1640
|
+
let splitAt = 0;
|
|
1641
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1642
|
+
const rh = rows[i].offsetHeight;
|
|
1643
|
+
if (used > 0 && used + rh > availH) break;
|
|
1644
|
+
used += rh;
|
|
1645
|
+
splitAt = i + 1;
|
|
1646
|
+
}
|
|
1647
|
+
if (splitAt === 0 || splitAt >= rows.length) return null;
|
|
1648
|
+
const rest = table.cloneNode(false);
|
|
1649
|
+
for (let i = splitAt; i < rows.length; i++) rest.appendChild(rows[i]);
|
|
1650
|
+
return rest;
|
|
1651
|
+
}
|
|
1652
|
+
function pageBoxStyle(pw, ph, pm, extra = []) {
|
|
1653
|
+
return [
|
|
1654
|
+
"position:relative",
|
|
1655
|
+
"box-sizing:border-box",
|
|
1656
|
+
"background-color:#fff",
|
|
1657
|
+
"margin:0 auto 16px",
|
|
1658
|
+
"box-shadow:0 2px 12px rgba(0,0,0,.25)",
|
|
1659
|
+
"flex-shrink:0",
|
|
1660
|
+
`width:${pw}px`,
|
|
1661
|
+
`min-height:${ph}px`,
|
|
1662
|
+
`padding:${pm.top}px ${pm.right}px ${pm.bottom}px ${pm.left}px`,
|
|
1663
|
+
...extra
|
|
1664
|
+
].join(";");
|
|
1665
|
+
}
|
|
1666
|
+
function renderPlainPaginated(doc, container, pw, ph, pm, pageOffset = 0) {
|
|
1667
|
+
const contentW = pw - pm.left - pm.right;
|
|
1668
|
+
const contentH = ph - pm.top - pm.bottom;
|
|
1669
|
+
const stage = document.createElement("div");
|
|
1670
|
+
stage.className = "ssd-page";
|
|
1671
|
+
stage.style.cssText = `position:absolute;left:-9999px;top:0;visibility:hidden;width:${contentW}px;padding:0;margin:0;min-height:0;box-shadow:none;background:none`;
|
|
1672
|
+
container.appendChild(stage);
|
|
1673
|
+
renderBlocks(doc.blocks, stage);
|
|
1674
|
+
const children = Array.from(stage.children);
|
|
1675
|
+
const tops = children.map((c) => c.offsetTop);
|
|
1676
|
+
const stageH = stage.scrollHeight;
|
|
1677
|
+
const heights = children.map((_, i) => (i + 1 < children.length ? tops[i + 1] : stageH) - tops[i]);
|
|
1678
|
+
const footnotes = doc.footnotes ?? [];
|
|
1679
|
+
const footnoteH = {};
|
|
1680
|
+
for (const fn of footnotes) {
|
|
1681
|
+
const item = buildFootnoteItem(fn);
|
|
1682
|
+
item.style.position = "absolute";
|
|
1683
|
+
item.style.visibility = "hidden";
|
|
1684
|
+
item.style.width = `${contentW}px`;
|
|
1685
|
+
stage.appendChild(item);
|
|
1686
|
+
footnoteH[fn.number] = item.offsetHeight;
|
|
1687
|
+
stage.removeChild(item);
|
|
1688
|
+
}
|
|
1689
|
+
const pages = [];
|
|
1690
|
+
const pageFootnotes = [];
|
|
1691
|
+
let pageDiv;
|
|
1692
|
+
let used = 0;
|
|
1693
|
+
let reserve = 0;
|
|
1694
|
+
let pageHasFn = false;
|
|
1695
|
+
const newPage = () => {
|
|
1696
|
+
const div = document.createElement("div");
|
|
1697
|
+
div.className = "ssd-page";
|
|
1698
|
+
div.style.cssText = pageBoxStyle(pw, ph, pm);
|
|
1699
|
+
container.appendChild(div);
|
|
1700
|
+
pages.push(div);
|
|
1701
|
+
pageFootnotes.push([]);
|
|
1702
|
+
used = 0;
|
|
1703
|
+
reserve = 0;
|
|
1704
|
+
pageHasFn = false;
|
|
1705
|
+
return div;
|
|
1706
|
+
};
|
|
1707
|
+
const fnReserve = (nums, hasFn) => nums.length === 0 ? 0 : (hasFn ? 0 : FOOTNOTE_SEPARATOR_H) + nums.reduce((a, n) => a + (footnoteH[n] ?? 0), 0);
|
|
1708
|
+
const recordFootnotes = (nums, add) => {
|
|
1709
|
+
if (nums.length === 0) return;
|
|
1710
|
+
pageFootnotes[pages.length - 1].push(...nums);
|
|
1711
|
+
reserve += add;
|
|
1712
|
+
pageHasFn = true;
|
|
1713
|
+
};
|
|
1714
|
+
pageDiv = newPage();
|
|
1715
|
+
for (let i = 0; i < children.length; i++) {
|
|
1716
|
+
const child = children[i];
|
|
1717
|
+
const forced = child.dataset.ssdBreak === "1";
|
|
1718
|
+
if (child.tagName === "TABLE") {
|
|
1719
|
+
if (forced && used > 0) pageDiv = newPage();
|
|
1720
|
+
let table = child;
|
|
1721
|
+
for (; ; ) {
|
|
1722
|
+
pageDiv.appendChild(table);
|
|
1723
|
+
const th = table.offsetHeight;
|
|
1724
|
+
if (used + th <= contentH - reserve) {
|
|
1725
|
+
used += th;
|
|
1726
|
+
break;
|
|
1727
|
+
}
|
|
1728
|
+
const rest = splitTableRows(table, contentH - reserve - used);
|
|
1729
|
+
if (!rest) {
|
|
1730
|
+
if (used === 0) {
|
|
1731
|
+
used += th;
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
pageDiv.removeChild(table);
|
|
1735
|
+
pageDiv = newPage();
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
pageDiv = newPage();
|
|
1739
|
+
table = rest;
|
|
1740
|
+
}
|
|
1741
|
+
const refs2 = footnoteNumbersIn(table);
|
|
1742
|
+
recordFootnotes(refs2, fnReserve(refs2, pageHasFn));
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
const h = heights[i];
|
|
1746
|
+
const refs = footnoteNumbersIn(child);
|
|
1747
|
+
let add = fnReserve(refs, pageHasFn);
|
|
1748
|
+
if (forced && used > 0 || used > 0 && used + h > contentH - reserve - add) {
|
|
1749
|
+
pageDiv = newPage();
|
|
1750
|
+
add = fnReserve(refs, false);
|
|
1751
|
+
}
|
|
1752
|
+
pageDiv.appendChild(child);
|
|
1753
|
+
used += h;
|
|
1754
|
+
recordFootnotes(refs, add);
|
|
1755
|
+
}
|
|
1756
|
+
stage.remove();
|
|
1757
|
+
for (let i = pages.length - 1; i >= 0; i--) {
|
|
1758
|
+
const hasFn = pageFootnotes[i].length > 0;
|
|
1759
|
+
const hasContent = (pages[i].textContent ?? "").trim().length > 0 || !!pages[i].querySelector("img,table");
|
|
1760
|
+
if (!hasFn && !hasContent) {
|
|
1761
|
+
pages[i].remove();
|
|
1762
|
+
pages.splice(i, 1);
|
|
1763
|
+
pageFootnotes.splice(i, 1);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
pages.forEach((page, i) => renderPageFootnotes(footnotes, page, pageFootnotes[i], pm));
|
|
1767
|
+
if (doc.endnotes && doc.endnotes.length > 0) {
|
|
1768
|
+
const endHost = document.createElement("div");
|
|
1769
|
+
renderNotes(doc, endHost, { endnotes: true });
|
|
1770
|
+
if (endHost.childNodes.length > 0) {
|
|
1771
|
+
const div = newPage();
|
|
1772
|
+
while (endHost.firstChild) div.appendChild(endHost.firstChild);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
const footerPx = doc.pageSize?.footerPx ?? Math.round(pm.bottom / 2);
|
|
1776
|
+
pages.forEach((page, i) => renderFooter(doc, page, pageOffset + i + 1, pm, footerPx));
|
|
1777
|
+
const headerPx = doc.pageSize?.headerPx ?? Math.round(pm.top / 2);
|
|
1778
|
+
pages.forEach((page, i) => renderHeader(doc, page, pageOffset + i + 1, pm, headerPx));
|
|
1779
|
+
fillTotalPages(container, container.querySelectorAll(".ssd-page").length);
|
|
1780
|
+
}
|
|
1781
|
+
function render(doc, container, options = {}) {
|
|
1782
|
+
showRevisions = options.showRevisions ?? false;
|
|
1783
|
+
const hasPageBg = doc.blocks.some(
|
|
1784
|
+
(b) => b.type === "paragraph" && extractPageBackground(b) !== null
|
|
1785
|
+
);
|
|
1786
|
+
const ps = doc.pageSize;
|
|
1787
|
+
if (!ps || ps.widthPx === 0 || ps.heightPx === 0) {
|
|
1788
|
+
emptyLineEm = hasPageBg ? EMPTY_LINE_EM : LINE_HEIGHT;
|
|
1789
|
+
renderBlocks(doc.blocks, container);
|
|
1790
|
+
renderNotes(doc, container);
|
|
1791
|
+
fillTotalPages(container, 1);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
if (doc.sections && doc.sections.length > 1) {
|
|
1795
|
+
renderSections(doc, container, hasPageBg);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
emptyLineEm = hasPageBg ? EMPTY_LINE_EM : LINE_HEIGHT;
|
|
1799
|
+
if (!hasPageBg) {
|
|
1800
|
+
renderPlainPaginated(doc, container, ps.widthPx, ps.heightPx, ps.marginPx);
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
renderPageBgPaginated(doc, container);
|
|
1804
|
+
}
|
|
1805
|
+
function renderSections(doc, container, docHasPageBg) {
|
|
1806
|
+
const sections = doc.sections;
|
|
1807
|
+
const lastIdx = sections.length - 1;
|
|
1808
|
+
if (!docHasPageBg) {
|
|
1809
|
+
emptyLineEm = LINE_HEIGHT;
|
|
1810
|
+
sections.forEach((section, i) => {
|
|
1811
|
+
const sp = section.pageSize;
|
|
1812
|
+
const subDoc = {
|
|
1813
|
+
blocks: section.blocks,
|
|
1814
|
+
pageSize: sp,
|
|
1815
|
+
footnotes: doc.footnotes,
|
|
1816
|
+
endnotes: i === lastIdx ? doc.endnotes : void 0,
|
|
1817
|
+
footer: section.footer ?? doc.footer,
|
|
1818
|
+
header: section.header ?? doc.header
|
|
1819
|
+
};
|
|
1820
|
+
const pageOffset = container.querySelectorAll(".ssd-page").length;
|
|
1821
|
+
renderPlainPaginated(subDoc, container, sp.widthPx, sp.heightPx, sp.marginPx, pageOffset);
|
|
1822
|
+
});
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
for (const section of sections) {
|
|
1826
|
+
const sp = section.pageSize;
|
|
1827
|
+
const sectionHasBg = section.blocks.some(
|
|
1828
|
+
(b) => b.type === "paragraph" && extractPageBackground(b) !== null
|
|
1829
|
+
);
|
|
1830
|
+
emptyLineEm = sectionHasBg ? EMPTY_LINE_EM : LINE_HEIGHT;
|
|
1831
|
+
const subDoc = {
|
|
1832
|
+
blocks: section.blocks,
|
|
1833
|
+
pageSize: sp,
|
|
1834
|
+
footer: section.footer ?? doc.footer,
|
|
1835
|
+
header: section.header ?? doc.header
|
|
1836
|
+
};
|
|
1837
|
+
if (sectionHasBg) {
|
|
1838
|
+
renderPageBgPaginated(subDoc, container);
|
|
1839
|
+
} else {
|
|
1840
|
+
const pageOffset = container.querySelectorAll(".ssd-page").length;
|
|
1841
|
+
renderPlainPaginated(subDoc, container, sp.widthPx, sp.heightPx, sp.marginPx, pageOffset);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
appendNotesPage(doc, container);
|
|
1845
|
+
fillTotalPages(container, container.querySelectorAll(".ssd-page").length);
|
|
1846
|
+
}
|
|
1847
|
+
function appendNotesPage(doc, container) {
|
|
1848
|
+
const host = document.createElement("div");
|
|
1849
|
+
renderNotes(doc, host);
|
|
1850
|
+
if (host.childNodes.length === 0) return;
|
|
1851
|
+
const ps = doc.pageSize;
|
|
1852
|
+
const pm = ps.marginPx;
|
|
1853
|
+
const div = document.createElement("div");
|
|
1854
|
+
div.className = "ssd-page";
|
|
1855
|
+
div.style.cssText = [
|
|
1856
|
+
"position:relative",
|
|
1857
|
+
"box-sizing:border-box",
|
|
1858
|
+
"background-color:#fff",
|
|
1859
|
+
"margin:0 auto 16px",
|
|
1860
|
+
"box-shadow:0 2px 12px rgba(0,0,0,.25)",
|
|
1861
|
+
`width:${ps.widthPx}px`,
|
|
1862
|
+
`min-height:${ps.heightPx}px`,
|
|
1863
|
+
`padding:${pm.top}px ${pm.right}px ${pm.bottom}px ${pm.left}px`
|
|
1864
|
+
].join(";");
|
|
1865
|
+
while (host.firstChild) div.appendChild(host.firstChild);
|
|
1866
|
+
container.appendChild(div);
|
|
1867
|
+
}
|
|
1868
|
+
function renderPageBgPaginated(doc, container) {
|
|
1869
|
+
const hasPageBg = true;
|
|
1870
|
+
const ps = doc.pageSize;
|
|
1871
|
+
const pw = ps.widthPx;
|
|
1872
|
+
const ph = ps.heightPx;
|
|
1873
|
+
const pm = ps.marginPx;
|
|
1874
|
+
const contentW = pw - pm.left - pm.right;
|
|
1875
|
+
const contentH = ph - pm.top - pm.bottom;
|
|
1876
|
+
const toFlow = (block) => hasPageBg && block.type === "paragraph" ? flowOnly(block, pw, ph) : block;
|
|
1877
|
+
const measureDiv = document.createElement("div");
|
|
1878
|
+
measureDiv.className = "ssd-page";
|
|
1879
|
+
measureDiv.style.cssText = `position:fixed;top:-9999px;left:-9999px;visibility:hidden;pointer-events:none;width:${contentW}px;padding:0;margin:0;min-height:0;box-shadow:none;background:none`;
|
|
1880
|
+
container.appendChild(measureDiv);
|
|
1881
|
+
const blockHeight = new Array(doc.blocks.length).fill(0);
|
|
1882
|
+
for (let i = 0; i < doc.blocks.length; i++) {
|
|
1883
|
+
const toMeasure = toFlow(doc.blocks[i]);
|
|
1884
|
+
if (!toMeasure) continue;
|
|
1885
|
+
const el = document.createElement("div");
|
|
1886
|
+
renderBlocks([toMeasure], el);
|
|
1887
|
+
measureDiv.appendChild(el);
|
|
1888
|
+
blockHeight[i] = el.offsetHeight;
|
|
1889
|
+
measureDiv.removeChild(el);
|
|
1890
|
+
}
|
|
1891
|
+
container.removeChild(measureDiv);
|
|
1892
|
+
const n = doc.blocks.length;
|
|
1893
|
+
const sectionStart = new Array(n).fill(false);
|
|
1894
|
+
if (hasPageBg) {
|
|
1895
|
+
for (let i = 0; i < n; i++) {
|
|
1896
|
+
let heading = isHeadingBlock(doc.blocks[i]);
|
|
1897
|
+
if (heading && i > 0 && sectionStart[i - 1] && isIconOnly(doc.blocks[i - 1])) {
|
|
1898
|
+
heading = false;
|
|
1899
|
+
}
|
|
1900
|
+
const iconLead = isIconOnly(doc.blocks[i]) && i + 1 < n && isHeadingBlock(doc.blocks[i + 1]);
|
|
1901
|
+
sectionStart[i] = heading || iconLead;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
const forcesBreak = (i) => sectionStart[i] || doc.blocks[i].type === "paragraph" && !!doc.blocks[i].pageBreakBefore;
|
|
1905
|
+
const blockPage = new Array(n).fill(0);
|
|
1906
|
+
let page = 0;
|
|
1907
|
+
let usedOnPage = 0;
|
|
1908
|
+
for (let i = 0; i < n; i++) {
|
|
1909
|
+
const h = blockHeight[i];
|
|
1910
|
+
if (forcesBreak(i) && usedOnPage > 0 || usedOnPage > 0 && usedOnPage + h > contentH) {
|
|
1911
|
+
page++;
|
|
1912
|
+
usedOnPage = 0;
|
|
1913
|
+
}
|
|
1914
|
+
blockPage[i] = page;
|
|
1915
|
+
usedOnPage += h;
|
|
1916
|
+
}
|
|
1917
|
+
let totalPages = page + 1;
|
|
1918
|
+
const distinctBgs = [];
|
|
1919
|
+
for (let i = 0; i < doc.blocks.length; i++) {
|
|
1920
|
+
const block = doc.blocks[i];
|
|
1921
|
+
if (block.type !== "paragraph") continue;
|
|
1922
|
+
const bg = extractPageBackground(block);
|
|
1923
|
+
if (!bg) continue;
|
|
1924
|
+
const last = distinctBgs[distinctBgs.length - 1];
|
|
1925
|
+
if (!last || last.src !== bg.src) distinctBgs.push(bg);
|
|
1926
|
+
}
|
|
1927
|
+
const pageBg = new Array(totalPages).fill(null);
|
|
1928
|
+
const keepIfEmpty = new Array(totalPages).fill(false);
|
|
1929
|
+
for (let k = 0; k < distinctBgs.length; k++) {
|
|
1930
|
+
const startPage = Math.min(k, totalPages - 1);
|
|
1931
|
+
const end = k + 1 < distinctBgs.length ? Math.min(k + 1, totalPages) : totalPages;
|
|
1932
|
+
if (k === 0) keepIfEmpty[startPage] = true;
|
|
1933
|
+
for (let p = startPage; p < end; p++) pageBg[p] = distinctBgs[k];
|
|
1934
|
+
}
|
|
1935
|
+
const pageBlocks = Array.from({ length: totalPages }, () => []);
|
|
1936
|
+
const pageWatermarks = Array.from({ length: totalPages }, () => []);
|
|
1937
|
+
for (let i = 0; i < doc.blocks.length; i++) {
|
|
1938
|
+
const block = doc.blocks[i];
|
|
1939
|
+
const renderable = toFlow(block);
|
|
1940
|
+
if (renderable) pageBlocks[blockPage[i]].push(renderable);
|
|
1941
|
+
if (hasPageBg) for (const wm of watermarksOf(block, pw, ph)) pageWatermarks[blockPage[i]].push(wm);
|
|
1942
|
+
}
|
|
1943
|
+
const wmForBg = /* @__PURE__ */ new Map();
|
|
1944
|
+
for (let p = 0; p < totalPages; p++) {
|
|
1945
|
+
const bgSrc = pageBg[p]?.src ?? "";
|
|
1946
|
+
for (const wm of pageWatermarks[p]) if (!wmForBg.has(bgSrc)) wmForBg.set(bgSrc, wm);
|
|
1947
|
+
}
|
|
1948
|
+
for (let p = 0; p < totalPages; p++) {
|
|
1949
|
+
const wm = wmForBg.get(pageBg[p]?.src ?? "");
|
|
1950
|
+
pageWatermarks[p] = wm ? [wm] : [];
|
|
1951
|
+
if (wm) pageBg[p] = null;
|
|
1952
|
+
}
|
|
1953
|
+
const notePage = document.createElement("div");
|
|
1954
|
+
renderNotes(doc, notePage);
|
|
1955
|
+
if (notePage.childNodes.length > 0) {
|
|
1956
|
+
pageBlocks.push([]);
|
|
1957
|
+
pageWatermarks.push([]);
|
|
1958
|
+
pageBg.push(null);
|
|
1959
|
+
totalPages++;
|
|
1960
|
+
}
|
|
1961
|
+
const pageBoxCss = (extra = []) => [
|
|
1962
|
+
"position:relative",
|
|
1963
|
+
"box-sizing:border-box",
|
|
1964
|
+
"background-color:#fff",
|
|
1965
|
+
"margin:0 auto 16px",
|
|
1966
|
+
"box-shadow:0 2px 12px rgba(0,0,0,.25)",
|
|
1967
|
+
`width:${pw}px`,
|
|
1968
|
+
`min-height:${ph}px`,
|
|
1969
|
+
...extra
|
|
1970
|
+
].join(";");
|
|
1971
|
+
const bgH = `${ph}px`;
|
|
1972
|
+
for (let p = 0; p < totalPages; p++) {
|
|
1973
|
+
const blocks = pageBlocks[p];
|
|
1974
|
+
const isNotePage = p === totalPages - 1 && notePage.childNodes.length > 0 && blocks.length === 0;
|
|
1975
|
+
const keepEmptyCover = keepIfEmpty[p] && (pageBg[p] !== null || pageWatermarks[p].length > 0);
|
|
1976
|
+
if (!isNotePage && !blocks.some(isBlockVisible) && !keepEmptyCover) continue;
|
|
1977
|
+
const div = document.createElement("div");
|
|
1978
|
+
div.className = "ssd-page";
|
|
1979
|
+
if (isNotePage) {
|
|
1980
|
+
div.style.cssText = pageBoxCss([`padding:${pm.top}px ${pm.right}px ${pm.bottom}px ${pm.left}px`]);
|
|
1981
|
+
while (notePage.firstChild) div.appendChild(notePage.firstChild);
|
|
1982
|
+
container.appendChild(div);
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
const fullImg = hasPageBg ? fullPageImage(blocks, pw) : null;
|
|
1986
|
+
if (fullImg) {
|
|
1987
|
+
div.style.cssText = [
|
|
1988
|
+
"position:relative",
|
|
1989
|
+
"box-sizing:border-box",
|
|
1990
|
+
`width:${pw}px`,
|
|
1991
|
+
`height:${ph}px`,
|
|
1992
|
+
`min-height:${ph}px`,
|
|
1993
|
+
"flex-shrink:0",
|
|
1994
|
+
"margin:0 auto 16px",
|
|
1995
|
+
"overflow:hidden",
|
|
1996
|
+
"box-shadow:0 2px 12px rgba(0,0,0,.25)"
|
|
1997
|
+
].join(";");
|
|
1998
|
+
const img = document.createElement("img");
|
|
1999
|
+
setImageSrc(img, fullImg.src);
|
|
2000
|
+
img.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover";
|
|
2001
|
+
div.appendChild(img);
|
|
2002
|
+
container.appendChild(div);
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
const bgImg = pageBg[p];
|
|
2006
|
+
const extra = [
|
|
2007
|
+
"background-repeat:no-repeat",
|
|
2008
|
+
"background-position:top center",
|
|
2009
|
+
`background-size:100% ${bgH}`,
|
|
2010
|
+
`padding:${pm.top}px ${pm.right}px ${pm.bottom}px ${pm.left}px`
|
|
2011
|
+
];
|
|
2012
|
+
const bgSafe = bgImg ? sanitizeImageSrc(bgImg.src) : "";
|
|
2013
|
+
if (bgSafe) extra.push(`background-image:url('${bgSafe}')`);
|
|
2014
|
+
div.style.cssText = pageBoxCss(extra);
|
|
2015
|
+
for (const wm of pageWatermarks[p]) {
|
|
2016
|
+
const img = document.createElement("img");
|
|
2017
|
+
setImageSrc(img, wm.src);
|
|
2018
|
+
img.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;object-fit:fill;z-index:0;pointer-events:none";
|
|
2019
|
+
div.appendChild(img);
|
|
2020
|
+
}
|
|
2021
|
+
const content = document.createElement("div");
|
|
2022
|
+
content.style.cssText = "position:relative;z-index:1";
|
|
2023
|
+
renderBlocks(blocks, content);
|
|
2024
|
+
div.appendChild(content);
|
|
2025
|
+
container.appendChild(div);
|
|
2026
|
+
}
|
|
2027
|
+
fillTotalPages(container, container.querySelectorAll(".ssd-page").length);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// src/index.ts
|
|
2031
|
+
function parsePageSize(xml) {
|
|
2032
|
+
const twipsToPx2 = (s) => Math.round(parseFloat(s) * 96 / 1440);
|
|
2033
|
+
const lastSect = xml.lastIndexOf("<w:sectPr");
|
|
2034
|
+
const scope = lastSect !== -1 ? xml.slice(lastSect) : xml;
|
|
2035
|
+
const pgSz = /<w:pgSz\b([^/]*)\/?>/.exec(scope) ?? /<w:pgSz\b([^/]*)\/?>/.exec(xml);
|
|
2036
|
+
if (!pgSz) return void 0;
|
|
2037
|
+
const attrs = pgSz[1];
|
|
2038
|
+
const w = /\bw:w="([\d.]+)"/.exec(attrs)?.[1];
|
|
2039
|
+
const h = /\bw:h="([\d.]+)"/.exec(attrs)?.[1];
|
|
2040
|
+
if (!w || !h) return void 0;
|
|
2041
|
+
const pgMar = /<w:pgMar\b([^/]*)\/?>/.exec(scope) ?? /<w:pgMar\b([^/]*)\/?>/.exec(xml);
|
|
2042
|
+
const ma = pgMar?.[1] ?? "";
|
|
2043
|
+
const top = /\bw:top="([\d.]+)"/.exec(ma)?.[1] ?? "1440";
|
|
2044
|
+
const right = /\bw:right="([\d.]+)"/.exec(ma)?.[1] ?? "1440";
|
|
2045
|
+
const bottom = /\bw:bottom="([\d.]+)"/.exec(ma)?.[1] ?? "1440";
|
|
2046
|
+
const left = /\bw:left="([\d.]+)"/.exec(ma)?.[1] ?? "1440";
|
|
2047
|
+
const footer = /\bw:footer="([\d.]+)"/.exec(ma)?.[1];
|
|
2048
|
+
const header = /\bw:header="([\d.]+)"/.exec(ma)?.[1];
|
|
2049
|
+
return {
|
|
2050
|
+
widthPx: twipsToPx2(w),
|
|
2051
|
+
heightPx: twipsToPx2(h),
|
|
2052
|
+
marginPx: {
|
|
2053
|
+
top: twipsToPx2(top),
|
|
2054
|
+
right: twipsToPx2(right),
|
|
2055
|
+
bottom: twipsToPx2(bottom),
|
|
2056
|
+
left: twipsToPx2(left)
|
|
2057
|
+
},
|
|
2058
|
+
...footer ? { footerPx: twipsToPx2(footer) } : {},
|
|
2059
|
+
...header ? { headerPx: twipsToPx2(header) } : {}
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
async function readEntry(zip, path, required) {
|
|
2063
|
+
const entry = zip.file(path);
|
|
2064
|
+
if (!entry) {
|
|
2065
|
+
if (required) throw new DocxParseError(`Missing required entry: ${path}`, "MISSING_ENTRY");
|
|
2066
|
+
return null;
|
|
2067
|
+
}
|
|
2068
|
+
return entry.async("string");
|
|
2069
|
+
}
|
|
2070
|
+
async function parse(buffer) {
|
|
2071
|
+
let zip;
|
|
2072
|
+
try {
|
|
2073
|
+
zip = await JSZip.loadAsync(buffer);
|
|
2074
|
+
} catch {
|
|
2075
|
+
throw new DocxParseError("Failed to load zip archive", "INVALID_ZIP");
|
|
2076
|
+
}
|
|
2077
|
+
const [documentXml, stylesXml, relsXml, numberingXml, footnotesXml, endnotesXml, settingsXml] = await Promise.all([
|
|
2078
|
+
readEntry(zip, "word/document.xml", true),
|
|
2079
|
+
readEntry(zip, "word/styles.xml", true),
|
|
2080
|
+
readEntry(zip, "word/_rels/document.xml.rels", false),
|
|
2081
|
+
readEntry(zip, "word/numbering.xml", false),
|
|
2082
|
+
readEntry(zip, "word/footnotes.xml", false),
|
|
2083
|
+
readEntry(zip, "word/endnotes.xml", false),
|
|
2084
|
+
readEntry(zip, "word/settings.xml", false)
|
|
2085
|
+
]);
|
|
2086
|
+
const relationshipMap = relsXml ? parseRelationships(relsXml) : {};
|
|
2087
|
+
const { styleMap, docDefaults, tableBorderMap } = parseStyles(stylesXml);
|
|
2088
|
+
const { abstractNumMap, numMap } = numberingXml ? parseNumbering(numberingXml) : emptyNumbering();
|
|
2089
|
+
const ctx = {
|
|
2090
|
+
styleMap,
|
|
2091
|
+
docDefaults,
|
|
2092
|
+
tableBorderMap,
|
|
2093
|
+
abstractNumMap,
|
|
2094
|
+
numMap,
|
|
2095
|
+
relationshipMap,
|
|
2096
|
+
zip,
|
|
2097
|
+
footnoteRefs: [],
|
|
2098
|
+
endnoteRefs: []
|
|
2099
|
+
};
|
|
2100
|
+
const blocks = await parseDocument(documentXml, ctx);
|
|
2101
|
+
const footnotes = await resolveNotes(footnotesXml, "footnote", ctx.footnoteRefs, ctx);
|
|
2102
|
+
const endnotes = await resolveNotes(endnotesXml, "endnote", ctx.endnoteRefs, ctx);
|
|
2103
|
+
const footer = await resolveFooter(documentXml, relationshipMap, zip, ctx);
|
|
2104
|
+
const header = await resolveHeader(documentXml, relationshipMap, zip, ctx);
|
|
2105
|
+
const headerFirst = await resolveHeader(documentXml, relationshipMap, zip, ctx, "first");
|
|
2106
|
+
const headerEven = await resolveHeader(documentXml, relationshipMap, zip, ctx, "even");
|
|
2107
|
+
const footerFirst = await resolveFooter(documentXml, relationshipMap, zip, ctx, "first");
|
|
2108
|
+
const footerEven = await resolveFooter(documentXml, relationshipMap, zip, ctx, "even");
|
|
2109
|
+
const bodySectScope = documentXml.slice(documentXml.lastIndexOf("<w:sectPr"));
|
|
2110
|
+
const titlePg = /<w:titlePg\b[^>]*\/?>/.test(bodySectScope) && !/<w:titlePg\b[^>]*w:val="(?:0|false|off)"/.test(bodySectScope);
|
|
2111
|
+
const evenAndOddHeaders = !!settingsXml && /<w:evenAndOddHeaders\b[^>]*\/?>/.test(settingsXml) && !/<w:evenAndOddHeaders\b[^>]*w:val="(?:0|false|off)"/.test(settingsXml);
|
|
2112
|
+
const pageSize = parsePageSize(documentXml);
|
|
2113
|
+
const resolveRef = (rId, kind) => resolveRid(rId, relationshipMap, zip, ctx, kind === "header" ? parseHeaderXml : parseFooterXml);
|
|
2114
|
+
const sections = pageSize ? await buildSections(blocks, pageSize, bodySectPrRefs(documentXml), resolveRef, header, footer) : [];
|
|
2115
|
+
return {
|
|
2116
|
+
blocks,
|
|
2117
|
+
pageSize,
|
|
2118
|
+
...footnotes.length ? { footnotes } : {},
|
|
2119
|
+
...endnotes.length ? { endnotes } : {},
|
|
2120
|
+
...footer && footer.length ? { footer } : {},
|
|
2121
|
+
...header && header.length ? { header } : {},
|
|
2122
|
+
...headerFirst && headerFirst.length ? { headerFirst } : {},
|
|
2123
|
+
...headerEven && headerEven.length ? { headerEven } : {},
|
|
2124
|
+
...footerFirst && footerFirst.length ? { footerFirst } : {},
|
|
2125
|
+
...footerEven && footerEven.length ? { footerEven } : {},
|
|
2126
|
+
...titlePg ? { titlePg } : {},
|
|
2127
|
+
...evenAndOddHeaders ? { evenAndOddHeaders } : {},
|
|
2128
|
+
...sections.length > 1 ? { sections } : {}
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
async function buildSections(blocks, bodyPageSize, bodyRefs, resolveRef, docHeader, docFooter) {
|
|
2132
|
+
const raw = [];
|
|
2133
|
+
let cur = [];
|
|
2134
|
+
for (const b of blocks) {
|
|
2135
|
+
cur.push(b);
|
|
2136
|
+
const pb = b.type === "paragraph" ? b : void 0;
|
|
2137
|
+
const tagged = pb?.sectionPageSize;
|
|
2138
|
+
if (tagged) {
|
|
2139
|
+
raw.push({ blocks: cur, pageSize: tagged, refs: pb.sectionRefs ?? {} });
|
|
2140
|
+
delete pb.sectionPageSize;
|
|
2141
|
+
delete pb.sectionRefs;
|
|
2142
|
+
cur = [];
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (cur.length) raw.push({ blocks: cur, pageSize: bodyPageSize, refs: bodyRefs });
|
|
2146
|
+
const sections = [];
|
|
2147
|
+
let carriedHeader = docHeader;
|
|
2148
|
+
let carriedFooter = docFooter;
|
|
2149
|
+
for (const s of raw) {
|
|
2150
|
+
if (s.refs.headerRId) carriedHeader = await resolveRef(s.refs.headerRId, "header") ?? carriedHeader;
|
|
2151
|
+
if (s.refs.footerRId) carriedFooter = await resolveRef(s.refs.footerRId, "footer") ?? carriedFooter;
|
|
2152
|
+
const header = carriedHeader;
|
|
2153
|
+
const footer = carriedFooter;
|
|
2154
|
+
sections.push({
|
|
2155
|
+
blocks: s.blocks,
|
|
2156
|
+
pageSize: s.pageSize,
|
|
2157
|
+
...header && header.length ? { header } : {},
|
|
2158
|
+
...footer && footer.length ? { footer } : {}
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
return sections;
|
|
2162
|
+
}
|
|
2163
|
+
function bodySectPrRefs(documentXml) {
|
|
2164
|
+
const lastSect = documentXml.lastIndexOf("<w:sectPr");
|
|
2165
|
+
if (lastSect === -1) return {};
|
|
2166
|
+
const end = documentXml.indexOf("</w:sectPr>", lastSect);
|
|
2167
|
+
const scope = end !== -1 ? documentXml.slice(lastSect, end) : documentXml.slice(lastSect);
|
|
2168
|
+
return {
|
|
2169
|
+
...defaultRefRId(scope, "header") ? { headerRId: defaultRefRId(scope, "header") } : {},
|
|
2170
|
+
...defaultRefRId(scope, "footer") ? { footerRId: defaultRefRId(scope, "footer") } : {}
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
function defaultRefRId(scope, kind) {
|
|
2174
|
+
const re = new RegExp(`<w:${kind}Reference\\b[^>]*>`, "g");
|
|
2175
|
+
let m;
|
|
2176
|
+
while ((m = re.exec(scope)) !== null) {
|
|
2177
|
+
if (/w:type="default"/.test(m[0])) return /r:id="([^"]+)"/.exec(m[0])?.[1];
|
|
2178
|
+
}
|
|
2179
|
+
return void 0;
|
|
2180
|
+
}
|
|
2181
|
+
async function resolveFooter(documentXml, relationshipMap, zip, ctx, type = "default") {
|
|
2182
|
+
return resolvePart(documentXml, relationshipMap, zip, ctx, "footer", parseFooterXml, type);
|
|
2183
|
+
}
|
|
2184
|
+
async function resolveHeader(documentXml, relationshipMap, zip, ctx, type = "default") {
|
|
2185
|
+
return resolvePart(documentXml, relationshipMap, zip, ctx, "header", parseHeaderXml, type);
|
|
2186
|
+
}
|
|
2187
|
+
async function resolvePart(documentXml, relationshipMap, zip, ctx, kind, parseXml, type = "default") {
|
|
2188
|
+
const ref = new RegExp(`<w:${kind}Reference\\b[^>]*>`, "g");
|
|
2189
|
+
let m;
|
|
2190
|
+
let rId;
|
|
2191
|
+
while ((m = ref.exec(documentXml)) !== null) {
|
|
2192
|
+
const tag = m[0];
|
|
2193
|
+
if (!new RegExp(`w:type="${type}"`).test(tag)) continue;
|
|
2194
|
+
rId = /r:id="([^"]+)"/.exec(tag)?.[1];
|
|
2195
|
+
break;
|
|
2196
|
+
}
|
|
2197
|
+
return resolveRid(rId, relationshipMap, zip, ctx, parseXml);
|
|
2198
|
+
}
|
|
2199
|
+
async function resolveRid(rId, relationshipMap, zip, ctx, parseXml) {
|
|
2200
|
+
if (!rId || !relationshipMap[rId]) return void 0;
|
|
2201
|
+
const xml = await readEntry(zip, `word/${relationshipMap[rId].target}`, false);
|
|
2202
|
+
return xml ? parseXml(xml, ctx) : void 0;
|
|
2203
|
+
}
|
|
2204
|
+
async function resolveNotes(xml, kind, refIds, ctx) {
|
|
2205
|
+
if (!xml || refIds.length === 0) return [];
|
|
2206
|
+
const map = await parseNotesXml(xml, kind, ctx);
|
|
2207
|
+
const entries = [];
|
|
2208
|
+
refIds.forEach((id, i) => {
|
|
2209
|
+
const noteBlocks = map.get(id);
|
|
2210
|
+
if (noteBlocks) entries.push({ number: i + 1, blocks: noteBlocks });
|
|
2211
|
+
});
|
|
2212
|
+
return entries;
|
|
2213
|
+
}
|
|
2214
|
+
function render2(doc, container, options) {
|
|
2215
|
+
render(doc, container, options);
|
|
2216
|
+
}
|
|
2217
|
+
export {
|
|
2218
|
+
DocxParseError,
|
|
2219
|
+
parse,
|
|
2220
|
+
render2 as render
|
|
2221
|
+
};
|