@beyondwork/docx-react-component 1.0.18 → 1.0.20
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/README.md +8 -2
- package/package.json +24 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +710 -4
- package/src/api/session-state.ts +60 -0
- package/src/core/commands/formatting-commands.ts +2 -1
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +19 -3
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +357 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +4 -1
- package/src/index.ts +51 -0
- package/src/io/docx-session.ts +623 -56
- package/src/io/export/serialize-comments.ts +104 -34
- package/src/io/export/serialize-footnotes.ts +198 -1
- package/src/io/export/serialize-headers-footers.ts +203 -10
- package/src/io/export/serialize-main-document.ts +285 -8
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +144 -32
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-comments.ts +85 -19
- package/src/io/ooxml/parse-fields.ts +396 -0
- package/src/io/ooxml/parse-footnotes.ts +452 -22
- package/src/io/ooxml/parse-headers-footers.ts +657 -29
- package/src/io/ooxml/parse-inline-media.ts +30 -0
- package/src/io/ooxml/parse-main-document.ts +807 -20
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +250 -4
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +87 -2
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +603 -0
- package/src/runtime/document-runtime.ts +1754 -78
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
- package/src/runtime/session-capabilities.ts +35 -3
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +324 -36
- package/src/runtime/table-schema.ts +89 -7
- package/src/runtime/view-state.ts +477 -0
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +2469 -1344
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1422 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +51 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +127 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
- package/src/validation/compatibility-engine.ts +119 -24
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +707 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
BorderSpec,
|
|
2
3
|
TextMark,
|
|
3
4
|
ParagraphBorders,
|
|
4
5
|
ParagraphShading,
|
|
@@ -6,6 +7,16 @@ import type {
|
|
|
6
7
|
ParagraphIndentation,
|
|
7
8
|
TabStop,
|
|
8
9
|
TableLook,
|
|
10
|
+
SectionProperties,
|
|
11
|
+
PageSize,
|
|
12
|
+
PageMargins,
|
|
13
|
+
ColumnProperties,
|
|
14
|
+
PageNumbering,
|
|
15
|
+
HeaderFooterReference,
|
|
16
|
+
HeaderFooterVariant,
|
|
17
|
+
SectionDocumentGrid,
|
|
18
|
+
SectionLineNumbering,
|
|
19
|
+
SectionPageBorders,
|
|
9
20
|
} from "../../model/canonical-document.ts";
|
|
10
21
|
import type { OpcRelationship } from "./part-manifest.ts";
|
|
11
22
|
import {
|
|
@@ -15,9 +26,12 @@ import {
|
|
|
15
26
|
import { toCanonicalNumberingInstanceId } from "./parse-numbering.ts";
|
|
16
27
|
import { parseComplexContentXml } from "./parse-complex-content.ts";
|
|
17
28
|
import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
|
|
29
|
+
import { classifyFieldInstruction } from "./parse-fields.ts";
|
|
30
|
+
import { resolveHighlightColor } from "./highlight-colors.ts";
|
|
18
31
|
|
|
19
32
|
export interface ParsedMainDocument {
|
|
20
33
|
blocks: ParsedBlockNode[];
|
|
34
|
+
finalSectionProperties?: SectionProperties;
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
export type ParsedBlockNode =
|
|
@@ -26,8 +40,15 @@ export type ParsedBlockNode =
|
|
|
26
40
|
| ParsedSdtNode
|
|
27
41
|
| ParsedCustomXmlNode
|
|
28
42
|
| ParsedAltChunkNode
|
|
43
|
+
| ParsedSectionBreakNode
|
|
29
44
|
| ParsedOpaqueBlockNode;
|
|
30
45
|
|
|
46
|
+
export interface ParsedSectionBreakNode {
|
|
47
|
+
type: "section_break";
|
|
48
|
+
sectionPropertiesXml: string;
|
|
49
|
+
sectionProperties: SectionProperties;
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
export interface ParsedParagraphNode {
|
|
32
53
|
type: "paragraph";
|
|
33
54
|
styleId?: string;
|
|
@@ -37,6 +58,7 @@ export interface ParsedParagraphNode {
|
|
|
37
58
|
};
|
|
38
59
|
alignment?: "left" | "center" | "right" | "both" | "distribute";
|
|
39
60
|
spacing?: ParagraphSpacing;
|
|
61
|
+
contextualSpacing?: boolean;
|
|
40
62
|
indentation?: ParagraphIndentation;
|
|
41
63
|
tabStops?: TabStop[];
|
|
42
64
|
keepNext?: boolean;
|
|
@@ -49,6 +71,8 @@ export interface ParsedParagraphNode {
|
|
|
49
71
|
bidi?: boolean;
|
|
50
72
|
suppressLineNumbers?: boolean;
|
|
51
73
|
cnfStyle?: string;
|
|
74
|
+
sectionProperties?: SectionProperties;
|
|
75
|
+
sectionPropertiesXml?: string;
|
|
52
76
|
children: ParsedInlineNode[];
|
|
53
77
|
rawXml: string;
|
|
54
78
|
}
|
|
@@ -69,7 +93,10 @@ export type ParsedInlineNode =
|
|
|
69
93
|
| ParsedVmlShapeInlineNode
|
|
70
94
|
| ParsedBookmarkStartInlineNode
|
|
71
95
|
| ParsedBookmarkEndInlineNode
|
|
72
|
-
|
|
|
96
|
+
| ParsedFootnoteRefInlineNode
|
|
97
|
+
| ParsedFieldInlineNode
|
|
98
|
+
| ParsedPermStartInlineNode
|
|
99
|
+
| ParsedPermEndInlineNode;
|
|
73
100
|
|
|
74
101
|
export interface ParsedTextNode {
|
|
75
102
|
type: "text";
|
|
@@ -104,6 +131,8 @@ export interface ParsedImageNode {
|
|
|
104
131
|
contentType?: string;
|
|
105
132
|
filename?: string;
|
|
106
133
|
altText?: string;
|
|
134
|
+
widthEmu?: number;
|
|
135
|
+
heightEmu?: number;
|
|
107
136
|
placementXml?: string;
|
|
108
137
|
display?: "inline" | "floating";
|
|
109
138
|
floating?: {
|
|
@@ -152,6 +181,7 @@ export interface ParsedShapeInlineNode {
|
|
|
152
181
|
type: "shape";
|
|
153
182
|
text?: string;
|
|
154
183
|
geometry?: string;
|
|
184
|
+
isTextBox?: boolean;
|
|
155
185
|
rawXml: string;
|
|
156
186
|
}
|
|
157
187
|
|
|
@@ -182,11 +212,32 @@ export interface ParsedBookmarkEndInlineNode {
|
|
|
182
212
|
rawXml: string;
|
|
183
213
|
}
|
|
184
214
|
|
|
215
|
+
export interface ParsedFootnoteRefInlineNode {
|
|
216
|
+
type: "footnote_ref";
|
|
217
|
+
noteId: string;
|
|
218
|
+
noteKind: "footnote" | "endnote";
|
|
219
|
+
}
|
|
220
|
+
|
|
185
221
|
export interface ParsedFieldInlineNode {
|
|
186
222
|
type: "field";
|
|
187
|
-
fieldType: "simple";
|
|
223
|
+
fieldType: "simple" | "complex";
|
|
188
224
|
instruction: string;
|
|
189
|
-
contentXml
|
|
225
|
+
contentXml?: string;
|
|
226
|
+
children?: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
|
|
227
|
+
rawXml: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface ParsedPermStartInlineNode {
|
|
231
|
+
type: "perm_start";
|
|
232
|
+
rangeId: string;
|
|
233
|
+
editorGroup?: string;
|
|
234
|
+
editor?: string;
|
|
235
|
+
rawXml: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface ParsedPermEndInlineNode {
|
|
239
|
+
type: "perm_end";
|
|
240
|
+
rangeId: string;
|
|
190
241
|
rawXml: string;
|
|
191
242
|
}
|
|
192
243
|
|
|
@@ -195,6 +246,23 @@ export interface ParsedOpaqueBlockNode {
|
|
|
195
246
|
rawXml: string;
|
|
196
247
|
}
|
|
197
248
|
|
|
249
|
+
export interface ParsedSdtCheckboxState {
|
|
250
|
+
checked: boolean;
|
|
251
|
+
checkedChar?: string;
|
|
252
|
+
uncheckedChar?: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface ParsedSdtDatePickerState {
|
|
256
|
+
fullDate?: string;
|
|
257
|
+
dateFormat?: string;
|
|
258
|
+
lid?: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface ParsedSdtDropdownListItem {
|
|
262
|
+
displayText?: string;
|
|
263
|
+
value: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
198
266
|
export interface ParsedSdtNode {
|
|
199
267
|
type: "sdt";
|
|
200
268
|
properties: {
|
|
@@ -203,6 +271,11 @@ export interface ParsedSdtNode {
|
|
|
203
271
|
tag?: string;
|
|
204
272
|
lock?: string;
|
|
205
273
|
propertiesXml?: string;
|
|
274
|
+
checkbox?: ParsedSdtCheckboxState;
|
|
275
|
+
datePicker?: ParsedSdtDatePickerState;
|
|
276
|
+
dropdownList?: ParsedSdtDropdownListItem[];
|
|
277
|
+
comboBox?: ParsedSdtDropdownListItem[];
|
|
278
|
+
showingPlcHdr?: boolean;
|
|
206
279
|
};
|
|
207
280
|
children: ParsedBlockNode[];
|
|
208
281
|
rawXml: string;
|
|
@@ -290,11 +363,24 @@ export function parseMainDocumentXml(
|
|
|
290
363
|
const bodyElement = findChildElement(documentElement, "body");
|
|
291
364
|
const relationshipMap = new Map(relationships.map((relationship) => [relationship.id, relationship]));
|
|
292
365
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
366
|
+
const allBlocks = bodyElement.children
|
|
367
|
+
.filter((node): node is XmlElementNode => node.type === "element")
|
|
368
|
+
.map((node) => parseBodyChild(node, xml, relationshipMap, relationships, mediaParts, sourcePartPath));
|
|
369
|
+
|
|
370
|
+
// The last body-level sectPr is the final section properties (not an intermediate section break).
|
|
371
|
+
// Extract it from the blocks list and store it separately.
|
|
372
|
+
let finalSectionProperties: SectionProperties | undefined;
|
|
373
|
+
const blocks: ParsedBlockNode[] = [];
|
|
374
|
+
for (let i = 0; i < allBlocks.length; i++) {
|
|
375
|
+
const block = allBlocks[i];
|
|
376
|
+
if (block.type === "section_break" && i === allBlocks.length - 1) {
|
|
377
|
+
finalSectionProperties = block.sectionProperties;
|
|
378
|
+
} else {
|
|
379
|
+
blocks.push(block);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { blocks, finalSectionProperties };
|
|
298
384
|
}
|
|
299
385
|
|
|
300
386
|
function parseBodyChild(
|
|
@@ -340,6 +426,10 @@ function parseBodyChild(
|
|
|
340
426
|
return parseAltChunkElement(node, sourceXml);
|
|
341
427
|
}
|
|
342
428
|
|
|
429
|
+
if (nodeType === "sectPr") {
|
|
430
|
+
return parseSectionBreakElement(node, sourceXml);
|
|
431
|
+
}
|
|
432
|
+
|
|
343
433
|
if (nodeType !== "p") {
|
|
344
434
|
return {
|
|
345
435
|
type: "opaque_block",
|
|
@@ -351,6 +441,7 @@ function parseBodyChild(
|
|
|
351
441
|
let numbering: ParsedParagraphNode["numbering"];
|
|
352
442
|
let alignment: ParsedParagraphNode["alignment"];
|
|
353
443
|
let spacing: ParsedParagraphNode["spacing"];
|
|
444
|
+
let contextualSpacing: ParsedParagraphNode["contextualSpacing"];
|
|
354
445
|
let indentation: ParsedParagraphNode["indentation"];
|
|
355
446
|
let tabStops: ParsedParagraphNode["tabStops"];
|
|
356
447
|
let keepNext: ParsedParagraphNode["keepNext"];
|
|
@@ -363,8 +454,15 @@ function parseBodyChild(
|
|
|
363
454
|
let bidi: ParsedParagraphNode["bidi"];
|
|
364
455
|
let suppressLineNumbers: ParsedParagraphNode["suppressLineNumbers"];
|
|
365
456
|
let cnfStyle: ParsedParagraphNode["cnfStyle"];
|
|
457
|
+
let sectionProperties: SectionProperties | undefined;
|
|
458
|
+
let sectionPropertiesXml: string | undefined;
|
|
366
459
|
let paragraphSupported = true;
|
|
367
460
|
const children: ParsedInlineNode[] = [];
|
|
461
|
+
let activeComplexField: {
|
|
462
|
+
instruction: string;
|
|
463
|
+
children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
|
|
464
|
+
mode: "instruction" | "result";
|
|
465
|
+
} | null = null;
|
|
368
466
|
|
|
369
467
|
for (const child of node.children) {
|
|
370
468
|
if (child.type !== "element") {
|
|
@@ -377,6 +475,7 @@ function parseBodyChild(
|
|
|
377
475
|
numbering = readParagraphNumbering(child);
|
|
378
476
|
alignment = readParagraphAlignment(child);
|
|
379
477
|
spacing = readParagraphSpacing(child);
|
|
478
|
+
contextualSpacing = readOptionalOnOffParagraphProperty(child, "contextualSpacing");
|
|
380
479
|
indentation = readParagraphIndentation(child);
|
|
381
480
|
tabStops = readParagraphTabStops(child);
|
|
382
481
|
keepNext = readOnOffParagraphProperty(child, "keepNext");
|
|
@@ -389,36 +488,153 @@ function parseBodyChild(
|
|
|
389
488
|
bidi = readOnOffParagraphProperty(child, "bidi");
|
|
390
489
|
suppressLineNumbers = readOnOffParagraphProperty(child, "suppressLineNumbers");
|
|
391
490
|
cnfStyle = readParagraphCnfStyle(child);
|
|
491
|
+
sectionProperties = readSectionPropertiesFromPPr(child);
|
|
492
|
+
sectionPropertiesXml = readSectionPropertiesXmlFromPPr(child, sourceXml);
|
|
392
493
|
paragraphSupported = paragraphSupported && supportsParagraphProperties(child);
|
|
393
494
|
break;
|
|
394
495
|
case "r":
|
|
395
|
-
|
|
496
|
+
activeComplexField = appendParagraphRunNodes(
|
|
497
|
+
child,
|
|
498
|
+
children,
|
|
499
|
+
activeComplexField,
|
|
500
|
+
sourceXml,
|
|
501
|
+
relationships,
|
|
502
|
+
mediaParts,
|
|
503
|
+
sourcePartPath,
|
|
504
|
+
);
|
|
396
505
|
break;
|
|
397
506
|
case "hyperlink": {
|
|
507
|
+
flushActiveComplexField(children, () => {
|
|
508
|
+
activeComplexField = null;
|
|
509
|
+
}, activeComplexField);
|
|
398
510
|
const hyperlink = parseHyperlink(child, sourceXml, relationshipMap);
|
|
399
511
|
children.push(hyperlink);
|
|
400
512
|
break;
|
|
401
513
|
}
|
|
402
514
|
case "ins":
|
|
403
515
|
case "del": {
|
|
516
|
+
flushActiveComplexField(children, () => {
|
|
517
|
+
activeComplexField = null;
|
|
518
|
+
}, activeComplexField);
|
|
404
519
|
children.push(...parseRevisionContainer(child, sourceXml, relationshipMap));
|
|
405
520
|
break;
|
|
406
521
|
}
|
|
407
522
|
case "commentRangeStart":
|
|
408
523
|
case "commentRangeEnd":
|
|
409
524
|
break;
|
|
410
|
-
case "bookmarkStart":
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
525
|
+
case "bookmarkStart": {
|
|
526
|
+
const bkId = child.attributes["w:id"] ?? child.attributes.id ?? "";
|
|
527
|
+
const bkName = child.attributes["w:name"] ?? child.attributes.name ?? "";
|
|
528
|
+
if (bkId) {
|
|
529
|
+
const bookmarkNode = {
|
|
530
|
+
type: "bookmark_start",
|
|
531
|
+
bookmarkId: bkId,
|
|
532
|
+
name: bkName,
|
|
533
|
+
rawXml: sourceXml.slice(child.start, child.end),
|
|
534
|
+
} as ParsedBookmarkStartInlineNode;
|
|
535
|
+
flushActiveComplexField(children, () => {
|
|
536
|
+
activeComplexField = null;
|
|
537
|
+
}, activeComplexField);
|
|
538
|
+
children.push(bookmarkNode);
|
|
539
|
+
} else {
|
|
540
|
+
flushActiveComplexField(children, () => {
|
|
541
|
+
activeComplexField = null;
|
|
542
|
+
}, activeComplexField);
|
|
543
|
+
children.push({ type: "opaque_inline", rawXml: sourceXml.slice(child.start, child.end) });
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case "bookmarkEnd": {
|
|
548
|
+
const bkEndId = child.attributes["w:id"] ?? child.attributes.id ?? "";
|
|
549
|
+
if (bkEndId) {
|
|
550
|
+
const bookmarkNode = {
|
|
551
|
+
type: "bookmark_end",
|
|
552
|
+
bookmarkId: bkEndId,
|
|
553
|
+
rawXml: sourceXml.slice(child.start, child.end),
|
|
554
|
+
} as ParsedBookmarkEndInlineNode;
|
|
555
|
+
flushActiveComplexField(children, () => {
|
|
556
|
+
activeComplexField = null;
|
|
557
|
+
}, activeComplexField);
|
|
558
|
+
children.push(bookmarkNode);
|
|
559
|
+
} else {
|
|
560
|
+
flushActiveComplexField(children, () => {
|
|
561
|
+
activeComplexField = null;
|
|
562
|
+
}, activeComplexField);
|
|
563
|
+
children.push({ type: "opaque_inline", rawXml: sourceXml.slice(child.start, child.end) });
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
case "fldSimple": {
|
|
568
|
+
flushActiveComplexField(children, () => {
|
|
569
|
+
activeComplexField = null;
|
|
570
|
+
}, activeComplexField);
|
|
571
|
+
const fieldInstr = child.attributes["w:instr"] ?? child.attributes.instr ?? "";
|
|
572
|
+
const fieldContentXml = child.children
|
|
573
|
+
.filter((c): c is XmlElementNode => c.type === "element")
|
|
574
|
+
.map((c) => sourceXml.slice(c.start, c.end))
|
|
575
|
+
.join("");
|
|
576
|
+
children.push({
|
|
577
|
+
type: "field",
|
|
578
|
+
fieldType: "simple",
|
|
579
|
+
instruction: fieldInstr,
|
|
580
|
+
contentXml: fieldContentXml,
|
|
581
|
+
rawXml: sourceXml.slice(child.start, child.end),
|
|
582
|
+
} as ParsedFieldInlineNode);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
415
585
|
case "proofErr":
|
|
586
|
+
flushActiveComplexField(children, () => {
|
|
587
|
+
activeComplexField = null;
|
|
588
|
+
}, activeComplexField);
|
|
416
589
|
children.push({
|
|
417
590
|
type: "opaque_inline",
|
|
418
591
|
rawXml: sourceXml.slice(child.start, child.end),
|
|
419
592
|
});
|
|
420
593
|
break;
|
|
594
|
+
case "permStart":
|
|
595
|
+
flushActiveComplexField(children, () => {
|
|
596
|
+
activeComplexField = null;
|
|
597
|
+
}, activeComplexField);
|
|
598
|
+
children.push(parsePermStartNode(child, sourceXml));
|
|
599
|
+
break;
|
|
600
|
+
case "permEnd":
|
|
601
|
+
flushActiveComplexField(children, () => {
|
|
602
|
+
activeComplexField = null;
|
|
603
|
+
}, activeComplexField);
|
|
604
|
+
children.push(parsePermEndNode(child, sourceXml));
|
|
605
|
+
break;
|
|
606
|
+
case "footnoteReference": {
|
|
607
|
+
flushActiveComplexField(children, () => {
|
|
608
|
+
activeComplexField = null;
|
|
609
|
+
}, activeComplexField);
|
|
610
|
+
const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
|
|
611
|
+
if (noteId) {
|
|
612
|
+
children.push({
|
|
613
|
+
type: "footnote_ref",
|
|
614
|
+
noteId,
|
|
615
|
+
noteKind: "footnote",
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
case "endnoteReference": {
|
|
621
|
+
flushActiveComplexField(children, () => {
|
|
622
|
+
activeComplexField = null;
|
|
623
|
+
}, activeComplexField);
|
|
624
|
+
const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
|
|
625
|
+
if (noteId) {
|
|
626
|
+
children.push({
|
|
627
|
+
type: "footnote_ref",
|
|
628
|
+
noteId,
|
|
629
|
+
noteKind: "endnote",
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
421
634
|
default:
|
|
635
|
+
flushActiveComplexField(children, () => {
|
|
636
|
+
activeComplexField = null;
|
|
637
|
+
}, activeComplexField);
|
|
422
638
|
children.push({
|
|
423
639
|
type: "opaque_inline",
|
|
424
640
|
rawXml: sourceXml.slice(child.start, child.end),
|
|
@@ -427,6 +643,10 @@ function parseBodyChild(
|
|
|
427
643
|
}
|
|
428
644
|
}
|
|
429
645
|
|
|
646
|
+
flushActiveComplexField(children, () => {
|
|
647
|
+
activeComplexField = null;
|
|
648
|
+
}, activeComplexField);
|
|
649
|
+
|
|
430
650
|
if (!paragraphSupported) {
|
|
431
651
|
return {
|
|
432
652
|
type: "opaque_block",
|
|
@@ -440,6 +660,7 @@ function parseBodyChild(
|
|
|
440
660
|
...(numbering ? { numbering } : {}),
|
|
441
661
|
...(alignment ? { alignment } : {}),
|
|
442
662
|
...(spacing ? { spacing } : {}),
|
|
663
|
+
...(contextualSpacing !== undefined ? { contextualSpacing } : {}),
|
|
443
664
|
...(indentation ? { indentation } : {}),
|
|
444
665
|
...(tabStops && tabStops.length > 0 ? { tabStops } : {}),
|
|
445
666
|
...(keepNext ? { keepNext } : {}),
|
|
@@ -452,11 +673,121 @@ function parseBodyChild(
|
|
|
452
673
|
...(bidi ? { bidi } : {}),
|
|
453
674
|
...(suppressLineNumbers ? { suppressLineNumbers } : {}),
|
|
454
675
|
...(cnfStyle ? { cnfStyle } : {}),
|
|
676
|
+
...(sectionProperties ? { sectionProperties } : {}),
|
|
677
|
+
...(sectionPropertiesXml ? { sectionPropertiesXml } : {}),
|
|
455
678
|
children,
|
|
456
679
|
rawXml: sourceXml.slice(node.start, node.end),
|
|
457
680
|
};
|
|
458
681
|
}
|
|
459
682
|
|
|
683
|
+
function appendParagraphRunNodes(
|
|
684
|
+
node: XmlElementNode,
|
|
685
|
+
children: ParsedInlineNode[],
|
|
686
|
+
activeComplexField: {
|
|
687
|
+
instruction: string;
|
|
688
|
+
children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
|
|
689
|
+
mode: "instruction" | "result";
|
|
690
|
+
} | null,
|
|
691
|
+
sourceXml: string,
|
|
692
|
+
relationships: readonly OpcRelationship[],
|
|
693
|
+
mediaParts: ReadonlyMap<string, InlineMediaPart>,
|
|
694
|
+
sourcePartPath: string,
|
|
695
|
+
): {
|
|
696
|
+
instruction: string;
|
|
697
|
+
children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
|
|
698
|
+
mode: "instruction" | "result";
|
|
699
|
+
} | null {
|
|
700
|
+
const hasFieldMarkers = node.children.some(
|
|
701
|
+
(child) =>
|
|
702
|
+
child.type === "element" &&
|
|
703
|
+
(localName(child.name) === "fldChar" || localName(child.name) === "instrText"),
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
if (activeComplexField?.mode === "result" && !hasFieldMarkers) {
|
|
707
|
+
const run = parseRunContentOnly(node, sourceXml);
|
|
708
|
+
if (
|
|
709
|
+
run.supported &&
|
|
710
|
+
run.nodes.every(
|
|
711
|
+
(child) =>
|
|
712
|
+
child.type === "text" || child.type === "hard_break" || child.type === "tab",
|
|
713
|
+
)
|
|
714
|
+
) {
|
|
715
|
+
activeComplexField.children.push(...run.nodes);
|
|
716
|
+
return activeComplexField;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (!hasFieldMarkers) {
|
|
721
|
+
children.push(...parseRun(node, sourceXml, relationships, mediaParts, sourcePartPath));
|
|
722
|
+
return activeComplexField;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
for (const child of node.children) {
|
|
726
|
+
if (child.type !== "element") {
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const name = localName(child.name);
|
|
731
|
+
if (name === "fldChar") {
|
|
732
|
+
const fldType = child.attributes["w:fldCharType"] ?? child.attributes.fldCharType;
|
|
733
|
+
if (fldType === "begin") {
|
|
734
|
+
activeComplexField = { instruction: "", children: [], mode: "instruction" };
|
|
735
|
+
} else if (fldType === "separate" && activeComplexField) {
|
|
736
|
+
activeComplexField.mode = "result";
|
|
737
|
+
} else if (fldType === "end" && activeComplexField) {
|
|
738
|
+
flushActiveComplexField(children, () => {
|
|
739
|
+
activeComplexField = null;
|
|
740
|
+
}, activeComplexField);
|
|
741
|
+
}
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (name === "instrText") {
|
|
746
|
+
const instruction = child.children
|
|
747
|
+
.filter((entry): entry is XmlTextNode => entry.type === "text")
|
|
748
|
+
.map((entry) => entry.text)
|
|
749
|
+
.join("");
|
|
750
|
+
if (activeComplexField) {
|
|
751
|
+
activeComplexField.instruction += instruction;
|
|
752
|
+
} else if (instruction.trim().length > 0) {
|
|
753
|
+
children.push({
|
|
754
|
+
type: "field",
|
|
755
|
+
fieldType: "complex",
|
|
756
|
+
instruction,
|
|
757
|
+
children: [],
|
|
758
|
+
rawXml: sourceXml.slice(node.start, node.end),
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return activeComplexField;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function flushActiveComplexField(
|
|
769
|
+
children: ParsedInlineNode[],
|
|
770
|
+
reset: () => void,
|
|
771
|
+
activeComplexField: {
|
|
772
|
+
instruction: string;
|
|
773
|
+
children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
|
|
774
|
+
mode: "instruction" | "result";
|
|
775
|
+
} | null,
|
|
776
|
+
): void {
|
|
777
|
+
if (!activeComplexField || activeComplexField.instruction.trim().length === 0) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
children.push({
|
|
782
|
+
type: "field",
|
|
783
|
+
fieldType: "complex",
|
|
784
|
+
instruction: activeComplexField.instruction,
|
|
785
|
+
children: activeComplexField.children,
|
|
786
|
+
rawXml: "",
|
|
787
|
+
});
|
|
788
|
+
reset();
|
|
789
|
+
}
|
|
790
|
+
|
|
460
791
|
function parseTableElement(
|
|
461
792
|
node: XmlElementNode,
|
|
462
793
|
sourceXml: string,
|
|
@@ -692,7 +1023,68 @@ function readSdtProperties(
|
|
|
692
1023
|
properties.lock = readOptionalAttribute(child, "val");
|
|
693
1024
|
continue;
|
|
694
1025
|
}
|
|
695
|
-
if (
|
|
1026
|
+
if (name === "showingPlcHdr") {
|
|
1027
|
+
const val = readOptionalAttribute(child, "val");
|
|
1028
|
+
properties.showingPlcHdr = val !== "false" && val !== "0";
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Checkbox (w14:checkbox)
|
|
1033
|
+
if (name === "checkbox") {
|
|
1034
|
+
properties.sdtType = "checkbox";
|
|
1035
|
+
const checkedNode = findFirstDescendant(child, "checked");
|
|
1036
|
+
const checkedVal = checkedNode ? (readOptionalAttribute(checkedNode, "val") ?? "0") : "0";
|
|
1037
|
+
const checkedCharNode = findFirstDescendant(child, "checkedState");
|
|
1038
|
+
const uncheckedCharNode = findFirstDescendant(child, "uncheckedState");
|
|
1039
|
+
properties.checkbox = {
|
|
1040
|
+
checked: checkedVal === "1" || checkedVal === "true",
|
|
1041
|
+
...(checkedCharNode ? { checkedChar: readOptionalAttribute(checkedCharNode, "val") } : {}),
|
|
1042
|
+
...(uncheckedCharNode ? { uncheckedChar: readOptionalAttribute(uncheckedCharNode, "val") } : {}),
|
|
1043
|
+
};
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Date picker
|
|
1048
|
+
if (name === "date") {
|
|
1049
|
+
properties.sdtType = "date";
|
|
1050
|
+
const fullDate = readOptionalAttribute(child, "fullDate");
|
|
1051
|
+
const dateFormatNode = findFirstChild(child, "dateFormat");
|
|
1052
|
+
const lidNode = findFirstChild(child, "lid");
|
|
1053
|
+
properties.datePicker = {
|
|
1054
|
+
...(fullDate ? { fullDate } : {}),
|
|
1055
|
+
...(dateFormatNode ? { dateFormat: readOptionalAttribute(dateFormatNode, "val") } : {}),
|
|
1056
|
+
...(lidNode ? { lid: readOptionalAttribute(lidNode, "val") } : {}),
|
|
1057
|
+
};
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Dropdown list
|
|
1062
|
+
if (name === "dropDownList") {
|
|
1063
|
+
properties.sdtType = "dropDownList";
|
|
1064
|
+
properties.dropdownList = readSdtListItems(child);
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Combo box
|
|
1069
|
+
if (name === "comboBox") {
|
|
1070
|
+
properties.sdtType = "comboBox";
|
|
1071
|
+
properties.comboBox = readSdtListItems(child);
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Plain text
|
|
1076
|
+
if (name === "text") {
|
|
1077
|
+
properties.sdtType = "plainText";
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Rich text (richText element is the default, but if explicitly present, tag it)
|
|
1082
|
+
if (name === "richText") {
|
|
1083
|
+
properties.sdtType = "richText";
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (!properties.sdtType && name !== "id" && name !== "placeholder" && name !== "showingPlcHdr" && name !== "rPr") {
|
|
696
1088
|
properties.sdtType = name;
|
|
697
1089
|
}
|
|
698
1090
|
}
|
|
@@ -700,6 +1092,34 @@ function readSdtProperties(
|
|
|
700
1092
|
return properties;
|
|
701
1093
|
}
|
|
702
1094
|
|
|
1095
|
+
function readSdtListItems(node: XmlElementNode): ParsedSdtDropdownListItem[] {
|
|
1096
|
+
const items: ParsedSdtDropdownListItem[] = [];
|
|
1097
|
+
for (const child of node.children) {
|
|
1098
|
+
if (child.type !== "element" || localName(child.name) !== "listItem") continue;
|
|
1099
|
+
const value = readOptionalAttribute(child, "value") ?? "";
|
|
1100
|
+
const displayText = readOptionalAttribute(child, "displayText");
|
|
1101
|
+
items.push({ value, ...(displayText ? { displayText } : {}) });
|
|
1102
|
+
}
|
|
1103
|
+
return items;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function findFirstChild(node: XmlElementNode, local: string): XmlElementNode | undefined {
|
|
1107
|
+
for (const child of node.children) {
|
|
1108
|
+
if (child.type === "element" && localName(child.name) === local) return child;
|
|
1109
|
+
}
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function findFirstDescendant(node: XmlElementNode, local: string): XmlElementNode | undefined {
|
|
1114
|
+
for (const child of node.children) {
|
|
1115
|
+
if (child.type !== "element") continue;
|
|
1116
|
+
if (localName(child.name) === local) return child;
|
|
1117
|
+
const nested = findFirstDescendant(child, local);
|
|
1118
|
+
if (nested) return nested;
|
|
1119
|
+
}
|
|
1120
|
+
return undefined;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
703
1123
|
function readTableStyleId(node: XmlElementNode): string | undefined {
|
|
704
1124
|
for (const child of node.children) {
|
|
705
1125
|
if (child.type !== "element" || localName(child.name) !== "tblStyle") continue;
|
|
@@ -762,9 +1182,35 @@ function readTableGridColumns(node: XmlElementNode): number[] {
|
|
|
762
1182
|
*/
|
|
763
1183
|
function tableRequiresOpaquePreservation(rawXml: string): boolean {
|
|
764
1184
|
// Safe table-local content now includes hyperlinks, bookmarks, comments,
|
|
765
|
-
// nested tables, floating images,
|
|
766
|
-
//
|
|
767
|
-
|
|
1185
|
+
// nested tables, floating images, VML preview atoms, and bounded field
|
|
1186
|
+
// families already owned by the current field slice. Risky table-local
|
|
1187
|
+
// semantics still fail closed to preserve-only.
|
|
1188
|
+
if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag|gridAfter|gridBefore|tblCellSpacing)\b/.test(rawXml)) {
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const fldSimpleInstructions = [...rawXml.matchAll(/\bw:instr="([^"]*)"/g)].map((match) => match[1] ?? "");
|
|
1193
|
+
const complexInstructions = [...rawXml.matchAll(/<(?:\w+:)?instrText\b[^>]*>([\s\S]*?)<\/(?:\w+:)?instrText>/gu)]
|
|
1194
|
+
.map((match) => decodeXmlEntities(match[1] ?? ""));
|
|
1195
|
+
for (const instruction of [...fldSimpleInstructions, ...complexInstructions]) {
|
|
1196
|
+
const classification = classifyFieldInstruction(instruction);
|
|
1197
|
+
if (!isSafeMainStoryTableFieldFamily(classification.family)) {
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function isSafeMainStoryTableFieldFamily(family: string): boolean {
|
|
1206
|
+
return (
|
|
1207
|
+
family === "REF" ||
|
|
1208
|
+
family === "PAGEREF" ||
|
|
1209
|
+
family === "NOTEREF" ||
|
|
1210
|
+
family === "TOC" ||
|
|
1211
|
+
family === "PAGE" ||
|
|
1212
|
+
family === "NUMPAGES"
|
|
1213
|
+
);
|
|
768
1214
|
}
|
|
769
1215
|
|
|
770
1216
|
function readCellGridSpan(node: XmlElementNode): number | undefined {
|
|
@@ -935,6 +1381,18 @@ function readOnOffParagraphProperty(node: XmlElementNode, name: string): boolean
|
|
|
935
1381
|
return val !== "false" && val !== "0" && val !== "off" ? true : undefined;
|
|
936
1382
|
}
|
|
937
1383
|
|
|
1384
|
+
function readOptionalOnOffParagraphProperty(
|
|
1385
|
+
node: XmlElementNode,
|
|
1386
|
+
name: string,
|
|
1387
|
+
): boolean | undefined {
|
|
1388
|
+
const propNode = node.children.find(
|
|
1389
|
+
(child): child is XmlElementNode => child.type === "element" && localName(child.name) === name,
|
|
1390
|
+
);
|
|
1391
|
+
if (!propNode) return undefined;
|
|
1392
|
+
const val = (propNode.attributes["w:val"] ?? propNode.attributes.val ?? "true").toLowerCase();
|
|
1393
|
+
return val !== "false" && val !== "0" && val !== "off";
|
|
1394
|
+
}
|
|
1395
|
+
|
|
938
1396
|
function readParagraphOutlineLevel(node: XmlElementNode): number | undefined {
|
|
939
1397
|
const propNode = node.children.find(
|
|
940
1398
|
(child): child is XmlElementNode => child.type === "element" && localName(child.name) === "outlineLvl",
|
|
@@ -1199,6 +1657,8 @@ function parseRun(
|
|
|
1199
1657
|
contentType: media.contentType,
|
|
1200
1658
|
filename: media.filename,
|
|
1201
1659
|
...(media.altText ? { altText: media.altText } : {}),
|
|
1660
|
+
...(media.widthEmu !== undefined ? { widthEmu: media.widthEmu } : {}),
|
|
1661
|
+
...(media.heightEmu !== undefined ? { heightEmu: media.heightEmu } : {}),
|
|
1202
1662
|
placementXml,
|
|
1203
1663
|
...(media.display ? { display: media.display } : {}),
|
|
1204
1664
|
...(media.floating ? { floating: media.floating } : {}),
|
|
@@ -1231,6 +1691,28 @@ function parseRun(
|
|
|
1231
1691
|
}
|
|
1232
1692
|
case "commentReference":
|
|
1233
1693
|
break;
|
|
1694
|
+
case "footnoteReference": {
|
|
1695
|
+
const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
|
|
1696
|
+
if (noteId) {
|
|
1697
|
+
result.push({
|
|
1698
|
+
type: "footnote_ref",
|
|
1699
|
+
noteId,
|
|
1700
|
+
noteKind: "footnote",
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1705
|
+
case "endnoteReference": {
|
|
1706
|
+
const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
|
|
1707
|
+
if (noteId) {
|
|
1708
|
+
result.push({
|
|
1709
|
+
type: "footnote_ref",
|
|
1710
|
+
noteId,
|
|
1711
|
+
noteKind: "endnote",
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1234
1716
|
case "lastRenderedPageBreak":
|
|
1235
1717
|
case "proofErr":
|
|
1236
1718
|
result.push({
|
|
@@ -1310,8 +1792,6 @@ function parseRevisionContainer(
|
|
|
1310
1792
|
case "commentRangeEnd":
|
|
1311
1793
|
case "bookmarkStart":
|
|
1312
1794
|
case "bookmarkEnd":
|
|
1313
|
-
case "permStart":
|
|
1314
|
-
case "permEnd":
|
|
1315
1795
|
case "proofErr":
|
|
1316
1796
|
case "lastRenderedPageBreak":
|
|
1317
1797
|
return [
|
|
@@ -1320,6 +1800,10 @@ function parseRevisionContainer(
|
|
|
1320
1800
|
rawXml: sourceXml.slice(node.start, node.end),
|
|
1321
1801
|
},
|
|
1322
1802
|
];
|
|
1803
|
+
case "permStart":
|
|
1804
|
+
return [parsePermStartNode(node, sourceXml)];
|
|
1805
|
+
case "permEnd":
|
|
1806
|
+
return [parsePermEndNode(node, sourceXml)];
|
|
1323
1807
|
default:
|
|
1324
1808
|
return [
|
|
1325
1809
|
{
|
|
@@ -1541,6 +2025,11 @@ function readRunMarks(node: XmlElementNode, sourceXml: string): MarksParseResult
|
|
|
1541
2025
|
marks.push(backgroundColorMark);
|
|
1542
2026
|
}
|
|
1543
2027
|
|
|
2028
|
+
const highlightMark = readRunHighlight(properties);
|
|
2029
|
+
if (highlightMark) {
|
|
2030
|
+
marks.push(highlightMark);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
1544
2033
|
const charSpacingMark = readNumericRunMark(properties, "spacing", "charSpacing");
|
|
1545
2034
|
if (charSpacingMark) {
|
|
1546
2035
|
marks.push(charSpacingMark);
|
|
@@ -1653,6 +2142,28 @@ function readRunBackgroundColor(properties: XmlElementNode): TextMark | undefine
|
|
|
1653
2142
|
return { type: "backgroundColor", color: fill };
|
|
1654
2143
|
}
|
|
1655
2144
|
|
|
2145
|
+
function readRunHighlight(properties: XmlElementNode): TextMark | undefined {
|
|
2146
|
+
const highlightNode = properties.children.find(
|
|
2147
|
+
(child): child is XmlElementNode =>
|
|
2148
|
+
child.type === "element" && localName(child.name) === "highlight",
|
|
2149
|
+
);
|
|
2150
|
+
if (!highlightNode) {
|
|
2151
|
+
return undefined;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
const highlightValue = highlightNode.attributes["w:val"] ?? highlightNode.attributes.val;
|
|
2155
|
+
const resolvedHighlight = resolveHighlightColor(highlightValue);
|
|
2156
|
+
if (!resolvedHighlight) {
|
|
2157
|
+
return undefined;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
return {
|
|
2161
|
+
type: "highlight",
|
|
2162
|
+
color: resolvedHighlight.color,
|
|
2163
|
+
val: resolvedHighlight.val,
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
|
|
1656
2167
|
function readNumericRunMark(
|
|
1657
2168
|
properties: XmlElementNode,
|
|
1658
2169
|
elementName: "spacing" | "kern" | "position",
|
|
@@ -2007,3 +2518,279 @@ function decodeXmlEntities(value: string): string {
|
|
|
2007
2518
|
}
|
|
2008
2519
|
});
|
|
2009
2520
|
}
|
|
2521
|
+
|
|
2522
|
+
// ---- Section properties parsing ----
|
|
2523
|
+
|
|
2524
|
+
function parseSectionBreakElement(
|
|
2525
|
+
node: XmlElementNode,
|
|
2526
|
+
sourceXml: string,
|
|
2527
|
+
): ParsedSectionBreakNode {
|
|
2528
|
+
const props = parseSectionPropertiesFromElement(node);
|
|
2529
|
+
return {
|
|
2530
|
+
type: "section_break",
|
|
2531
|
+
sectionPropertiesXml: sourceXml.slice(node.start, node.end),
|
|
2532
|
+
sectionProperties: props,
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
function readSectionPropertiesFromPPr(
|
|
2537
|
+
pPrNode: XmlElementNode,
|
|
2538
|
+
): SectionProperties | undefined {
|
|
2539
|
+
for (const child of pPrNode.children) {
|
|
2540
|
+
if (child.type === "element" && localName(child.name) === "sectPr") {
|
|
2541
|
+
return parseSectionPropertiesFromElement(child);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
return undefined;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function readSectionPropertiesXmlFromPPr(
|
|
2548
|
+
pPrNode: XmlElementNode,
|
|
2549
|
+
sourceXml: string,
|
|
2550
|
+
): string | undefined {
|
|
2551
|
+
for (const child of pPrNode.children) {
|
|
2552
|
+
if (child.type === "element" && localName(child.name) === "sectPr") {
|
|
2553
|
+
return sourceXml.slice(child.start, child.end);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
return undefined;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
export function parseSectionPropertiesFromElement(
|
|
2560
|
+
node: XmlElementNode,
|
|
2561
|
+
): SectionProperties {
|
|
2562
|
+
const props: SectionProperties = {};
|
|
2563
|
+
|
|
2564
|
+
for (const child of node.children) {
|
|
2565
|
+
if (child.type !== "element") continue;
|
|
2566
|
+
const name = localName(child.name);
|
|
2567
|
+
|
|
2568
|
+
switch (name) {
|
|
2569
|
+
case "pgSz": {
|
|
2570
|
+
const w = safeParseInt(child.attributes["w:w"]);
|
|
2571
|
+
const h = safeParseInt(child.attributes["w:h"]);
|
|
2572
|
+
if (w !== undefined && h !== undefined) {
|
|
2573
|
+
const pageSize: PageSize = { width: w, height: h };
|
|
2574
|
+
const orient = child.attributes["w:orient"];
|
|
2575
|
+
if (orient === "landscape" || orient === "portrait") {
|
|
2576
|
+
pageSize.orientation = orient;
|
|
2577
|
+
}
|
|
2578
|
+
props.pageSize = pageSize;
|
|
2579
|
+
}
|
|
2580
|
+
break;
|
|
2581
|
+
}
|
|
2582
|
+
case "pgMar": {
|
|
2583
|
+
const top = safeParseInt(child.attributes["w:top"]);
|
|
2584
|
+
const right = safeParseInt(child.attributes["w:right"]);
|
|
2585
|
+
const bottom = safeParseInt(child.attributes["w:bottom"]);
|
|
2586
|
+
const left = safeParseInt(child.attributes["w:left"]);
|
|
2587
|
+
if (top !== undefined && right !== undefined && bottom !== undefined && left !== undefined) {
|
|
2588
|
+
const margins: PageMargins = { top, right, bottom, left };
|
|
2589
|
+
const header = safeParseInt(child.attributes["w:header"]);
|
|
2590
|
+
const footer = safeParseInt(child.attributes["w:footer"]);
|
|
2591
|
+
const gutter = safeParseInt(child.attributes["w:gutter"]);
|
|
2592
|
+
if (header !== undefined) margins.header = header;
|
|
2593
|
+
if (footer !== undefined) margins.footer = footer;
|
|
2594
|
+
if (gutter !== undefined) margins.gutter = gutter;
|
|
2595
|
+
props.pageMargins = margins;
|
|
2596
|
+
}
|
|
2597
|
+
break;
|
|
2598
|
+
}
|
|
2599
|
+
case "cols": {
|
|
2600
|
+
const columns: ColumnProperties = {};
|
|
2601
|
+
const num = safeParseInt(child.attributes["w:num"]);
|
|
2602
|
+
const space = safeParseInt(child.attributes["w:space"]);
|
|
2603
|
+
const equalWidth = child.attributes["w:equalWidth"];
|
|
2604
|
+
const sep = child.attributes["w:sep"];
|
|
2605
|
+
if (num !== undefined) columns.count = num;
|
|
2606
|
+
if (space !== undefined) columns.space = space;
|
|
2607
|
+
if (equalWidth !== undefined) columns.equalWidth = equalWidth !== "0" && equalWidth !== "false";
|
|
2608
|
+
if (sep === "1" || sep === "true") columns.separator = true;
|
|
2609
|
+
const colDefs: Array<{ width: number; space?: number }> = [];
|
|
2610
|
+
for (const colChild of child.children) {
|
|
2611
|
+
if (colChild.type === "element" && localName(colChild.name) === "col") {
|
|
2612
|
+
const colW = safeParseInt(colChild.attributes["w:w"]);
|
|
2613
|
+
const colSpace = safeParseInt(colChild.attributes["w:space"]);
|
|
2614
|
+
if (colW !== undefined) {
|
|
2615
|
+
colDefs.push(colSpace !== undefined ? { width: colW, space: colSpace } : { width: colW });
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
if (colDefs.length > 0) columns.columns = colDefs;
|
|
2620
|
+
if (Object.keys(columns).length > 0) props.columns = columns;
|
|
2621
|
+
break;
|
|
2622
|
+
}
|
|
2623
|
+
case "pgNumType": {
|
|
2624
|
+
const numbering: PageNumbering = {};
|
|
2625
|
+
const fmt = child.attributes["w:fmt"];
|
|
2626
|
+
const start = safeParseInt(child.attributes["w:start"]);
|
|
2627
|
+
const chapStyle = child.attributes["w:chapStyle"];
|
|
2628
|
+
const chapSep = child.attributes["w:chapSep"];
|
|
2629
|
+
if (fmt) numbering.format = fmt;
|
|
2630
|
+
if (start !== undefined) numbering.start = start;
|
|
2631
|
+
if (chapStyle) numbering.chapStyle = chapStyle;
|
|
2632
|
+
if (chapSep) numbering.chapSep = chapSep;
|
|
2633
|
+
if (Object.keys(numbering).length > 0) props.pageNumbering = numbering;
|
|
2634
|
+
break;
|
|
2635
|
+
}
|
|
2636
|
+
case "lnNumType": {
|
|
2637
|
+
const lineNumbering: SectionLineNumbering = {};
|
|
2638
|
+
const countBy = safeParseInt(child.attributes["w:countBy"]);
|
|
2639
|
+
const start = safeParseInt(child.attributes["w:start"]);
|
|
2640
|
+
const distance = safeParseInt(child.attributes["w:distance"]);
|
|
2641
|
+
const restart = child.attributes["w:restart"];
|
|
2642
|
+
if (countBy !== undefined) lineNumbering.countBy = countBy;
|
|
2643
|
+
if (start !== undefined) lineNumbering.start = start;
|
|
2644
|
+
if (distance !== undefined) lineNumbering.distance = distance;
|
|
2645
|
+
if (
|
|
2646
|
+
restart === "newPage" ||
|
|
2647
|
+
restart === "newSection" ||
|
|
2648
|
+
restart === "continuous"
|
|
2649
|
+
) {
|
|
2650
|
+
lineNumbering.restart = restart;
|
|
2651
|
+
}
|
|
2652
|
+
if (Object.keys(lineNumbering).length > 0) {
|
|
2653
|
+
props.lineNumbering = lineNumbering;
|
|
2654
|
+
}
|
|
2655
|
+
break;
|
|
2656
|
+
}
|
|
2657
|
+
case "pgBorders": {
|
|
2658
|
+
const pageBorders: SectionPageBorders = {};
|
|
2659
|
+
const offsetFrom = child.attributes["w:offsetFrom"];
|
|
2660
|
+
const display = child.attributes["w:display"];
|
|
2661
|
+
const zOrder = child.attributes["w:zOrder"];
|
|
2662
|
+
if (offsetFrom === "page" || offsetFrom === "text") {
|
|
2663
|
+
pageBorders.offsetFrom = offsetFrom;
|
|
2664
|
+
}
|
|
2665
|
+
if (
|
|
2666
|
+
display === "allPages" ||
|
|
2667
|
+
display === "firstPage" ||
|
|
2668
|
+
display === "notFirstPage"
|
|
2669
|
+
) {
|
|
2670
|
+
pageBorders.display = display;
|
|
2671
|
+
}
|
|
2672
|
+
if (zOrder === "front" || zOrder === "back") {
|
|
2673
|
+
pageBorders.zOrder = zOrder;
|
|
2674
|
+
}
|
|
2675
|
+
for (const borderChild of child.children) {
|
|
2676
|
+
if (borderChild.type !== "element") continue;
|
|
2677
|
+
const borderName = localName(borderChild.name);
|
|
2678
|
+
const border = parseBorderSpec(borderChild);
|
|
2679
|
+
if (!border) continue;
|
|
2680
|
+
if (
|
|
2681
|
+
borderName === "top" ||
|
|
2682
|
+
borderName === "left" ||
|
|
2683
|
+
borderName === "bottom" ||
|
|
2684
|
+
borderName === "right"
|
|
2685
|
+
) {
|
|
2686
|
+
pageBorders[borderName] = border;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
if (Object.keys(pageBorders).length > 0) {
|
|
2690
|
+
props.pageBorders = pageBorders;
|
|
2691
|
+
}
|
|
2692
|
+
break;
|
|
2693
|
+
}
|
|
2694
|
+
case "docGrid": {
|
|
2695
|
+
const documentGrid: SectionDocumentGrid = {};
|
|
2696
|
+
const type = child.attributes["w:type"];
|
|
2697
|
+
const linePitch = safeParseInt(child.attributes["w:linePitch"]);
|
|
2698
|
+
const charSpace = safeParseInt(child.attributes["w:charSpace"]);
|
|
2699
|
+
if (
|
|
2700
|
+
type === "default" ||
|
|
2701
|
+
type === "lines" ||
|
|
2702
|
+
type === "linesAndChars" ||
|
|
2703
|
+
type === "snapToChars"
|
|
2704
|
+
) {
|
|
2705
|
+
documentGrid.type = type;
|
|
2706
|
+
}
|
|
2707
|
+
if (linePitch !== undefined) documentGrid.linePitch = linePitch;
|
|
2708
|
+
if (charSpace !== undefined) documentGrid.charSpace = charSpace;
|
|
2709
|
+
if (Object.keys(documentGrid).length > 0) {
|
|
2710
|
+
props.documentGrid = documentGrid;
|
|
2711
|
+
}
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
case "headerReference": {
|
|
2715
|
+
const variant = child.attributes["w:type"] as HeaderFooterVariant | undefined;
|
|
2716
|
+
const rId = child.attributes["r:id"];
|
|
2717
|
+
if (variant && rId) {
|
|
2718
|
+
if (!props.headerReferences) props.headerReferences = [];
|
|
2719
|
+
props.headerReferences.push({ variant, relationshipId: rId });
|
|
2720
|
+
}
|
|
2721
|
+
break;
|
|
2722
|
+
}
|
|
2723
|
+
case "footerReference": {
|
|
2724
|
+
const variant = child.attributes["w:type"] as HeaderFooterVariant | undefined;
|
|
2725
|
+
const rId = child.attributes["r:id"];
|
|
2726
|
+
if (variant && rId) {
|
|
2727
|
+
if (!props.footerReferences) props.footerReferences = [];
|
|
2728
|
+
props.footerReferences.push({ variant, relationshipId: rId });
|
|
2729
|
+
}
|
|
2730
|
+
break;
|
|
2731
|
+
}
|
|
2732
|
+
case "type": {
|
|
2733
|
+
const val = child.attributes["w:val"];
|
|
2734
|
+
if (val === "continuous" || val === "nextPage" || val === "evenPage" || val === "oddPage" || val === "nextColumn") {
|
|
2735
|
+
props.sectionType = val;
|
|
2736
|
+
}
|
|
2737
|
+
break;
|
|
2738
|
+
}
|
|
2739
|
+
case "titlePg": {
|
|
2740
|
+
const val = child.attributes["w:val"];
|
|
2741
|
+
props.titlePage = val !== "false" && val !== "0";
|
|
2742
|
+
break;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
return props;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
function safeParseInt(value: string | undefined): number | undefined {
|
|
2751
|
+
if (value === undefined) return undefined;
|
|
2752
|
+
const n = Number.parseInt(value, 10);
|
|
2753
|
+
return Number.isFinite(n) ? n : undefined;
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
function parseBorderSpec(node: XmlElementNode): BorderSpec | undefined {
|
|
2757
|
+
const border: BorderSpec = {};
|
|
2758
|
+
const value = node.attributes["w:val"];
|
|
2759
|
+
const size = safeParseInt(node.attributes["w:sz"]);
|
|
2760
|
+
const space = safeParseInt(node.attributes["w:space"]);
|
|
2761
|
+
const color = node.attributes["w:color"];
|
|
2762
|
+
|
|
2763
|
+
if (value) border.value = value;
|
|
2764
|
+
if (size !== undefined) border.size = size;
|
|
2765
|
+
if (space !== undefined) border.space = space;
|
|
2766
|
+
if (color) border.color = color;
|
|
2767
|
+
|
|
2768
|
+
return Object.keys(border).length > 0 ? border : undefined;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
function parsePermStartNode(
|
|
2772
|
+
node: XmlElementNode,
|
|
2773
|
+
sourceXml: string,
|
|
2774
|
+
): ParsedPermStartInlineNode {
|
|
2775
|
+
const rangeId = node.attributes["w:id"] ?? node.attributes.id ?? "";
|
|
2776
|
+
const edGrp = node.attributes["w:edGrp"] ?? node.attributes.edGrp;
|
|
2777
|
+
const ed = node.attributes["w:ed"] ?? node.attributes.ed;
|
|
2778
|
+
return {
|
|
2779
|
+
type: "perm_start",
|
|
2780
|
+
rangeId,
|
|
2781
|
+
...(edGrp ? { editorGroup: edGrp } : {}),
|
|
2782
|
+
...(ed ? { editor: ed } : {}),
|
|
2783
|
+
rawXml: sourceXml.slice(node.start, node.end),
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
function parsePermEndNode(
|
|
2788
|
+
node: XmlElementNode,
|
|
2789
|
+
sourceXml: string,
|
|
2790
|
+
): ParsedPermEndInlineNode {
|
|
2791
|
+
return {
|
|
2792
|
+
type: "perm_end",
|
|
2793
|
+
rangeId: node.attributes["w:id"] ?? node.attributes.id ?? "",
|
|
2794
|
+
rawXml: sourceXml.slice(node.start, node.end),
|
|
2795
|
+
};
|
|
2796
|
+
}
|