@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
|
@@ -3,7 +3,12 @@ import type {
|
|
|
3
3
|
FootnoteCollection,
|
|
4
4
|
FootnoteDefinition,
|
|
5
5
|
InlineNode,
|
|
6
|
+
ParagraphIndentation,
|
|
6
7
|
ParagraphNode,
|
|
8
|
+
ParagraphSpacing,
|
|
9
|
+
TableCellNode,
|
|
10
|
+
TableNode,
|
|
11
|
+
TableRowNode,
|
|
7
12
|
TextMark,
|
|
8
13
|
} from "../../model/canonical-document.ts";
|
|
9
14
|
|
|
@@ -137,16 +142,24 @@ function parseNoteElement(
|
|
|
137
142
|
if (name === "p") {
|
|
138
143
|
blocks.push(parseParagraphElement(child));
|
|
139
144
|
} else if (name === "tbl") {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
+
// Simple tables (no revisions, fields, or nested tables) are promoted
|
|
146
|
+
// to supported-roundtrip; structurally risky tables stay opaque.
|
|
147
|
+
if (isSimpleSecondaryStoryTable(child)) {
|
|
148
|
+
blocks.push(parseSimpleTableElement(child));
|
|
149
|
+
} else {
|
|
150
|
+
blocks.push({
|
|
151
|
+
type: "opaque_block",
|
|
152
|
+
fragmentId: `fragment:note-tbl-${noteId}`,
|
|
153
|
+
warningId: `warning:note-opaque-table`,
|
|
154
|
+
rawXml: serializeElementToXml(child),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
145
157
|
} else {
|
|
146
158
|
blocks.push({
|
|
147
159
|
type: "opaque_block",
|
|
148
160
|
fragmentId: `fragment:note-opaque-${noteId}`,
|
|
149
161
|
warningId: `warning:note-opaque-block`,
|
|
162
|
+
rawXml: serializeElementToXml(child),
|
|
150
163
|
});
|
|
151
164
|
}
|
|
152
165
|
}
|
|
@@ -156,7 +169,15 @@ function parseNoteElement(
|
|
|
156
169
|
|
|
157
170
|
function parseParagraphElement(pElement: XmlElementNode): ParagraphNode {
|
|
158
171
|
let styleId: string | undefined;
|
|
172
|
+
let alignment: ParagraphNode["alignment"];
|
|
173
|
+
let spacing: ParagraphNode["spacing"];
|
|
174
|
+
let indentation: ParagraphNode["indentation"];
|
|
159
175
|
const children: InlineNode[] = [];
|
|
176
|
+
let activeComplexField: {
|
|
177
|
+
instruction: string;
|
|
178
|
+
children: InlineNode[];
|
|
179
|
+
mode: "instruction" | "result";
|
|
180
|
+
} | null = null;
|
|
160
181
|
|
|
161
182
|
for (const child of pElement.children) {
|
|
162
183
|
if (child.type !== "element") {
|
|
@@ -168,24 +189,141 @@ function parseParagraphElement(pElement: XmlElementNode): ParagraphNode {
|
|
|
168
189
|
if (name === "pPr") {
|
|
169
190
|
const pStyle = findChildElementOptional(child, "pStyle");
|
|
170
191
|
styleId = pStyle?.attributes["w:val"] ?? pStyle?.attributes.val;
|
|
192
|
+
const jc = findChildElementOptional(child, "jc");
|
|
193
|
+
const jcVal = jc?.attributes["w:val"] ?? jc?.attributes.val;
|
|
194
|
+
if (jcVal === "left" || jcVal === "center" || jcVal === "right" || jcVal === "both" || jcVal === "distribute") {
|
|
195
|
+
alignment = jcVal;
|
|
196
|
+
}
|
|
197
|
+
spacing = readParagraphSpacing(child);
|
|
198
|
+
indentation = readParagraphIndentation(child);
|
|
171
199
|
} else if (name === "r") {
|
|
172
|
-
|
|
200
|
+
activeComplexField = appendRunNodes(child, children, activeComplexField);
|
|
173
201
|
} else if (name === "hyperlink") {
|
|
202
|
+
if (activeComplexField && activeComplexField.instruction.trim().length > 0) {
|
|
203
|
+
children.push({
|
|
204
|
+
type: "field",
|
|
205
|
+
fieldType: "complex",
|
|
206
|
+
instruction: activeComplexField.instruction,
|
|
207
|
+
children: activeComplexField.children,
|
|
208
|
+
});
|
|
209
|
+
activeComplexField = null;
|
|
210
|
+
}
|
|
174
211
|
children.push(parseHyperlinkElement(child));
|
|
175
212
|
} else if (name === "bookmarkStart" || name === "bookmarkEnd") {
|
|
176
|
-
|
|
213
|
+
const bookmarkNode = parseBookmarkElement(child);
|
|
214
|
+
if (activeComplexField?.mode === "result") {
|
|
215
|
+
activeComplexField.children.push(bookmarkNode);
|
|
216
|
+
} else {
|
|
217
|
+
if (activeComplexField && activeComplexField.instruction.trim().length > 0) {
|
|
218
|
+
children.push({
|
|
219
|
+
type: "field",
|
|
220
|
+
fieldType: "complex",
|
|
221
|
+
instruction: activeComplexField.instruction,
|
|
222
|
+
children: activeComplexField.children,
|
|
223
|
+
});
|
|
224
|
+
activeComplexField = null;
|
|
225
|
+
}
|
|
226
|
+
children.push(bookmarkNode);
|
|
227
|
+
}
|
|
177
228
|
} else if (name === "fldSimple") {
|
|
178
|
-
|
|
229
|
+
if (activeComplexField && activeComplexField.instruction.trim().length > 0) {
|
|
230
|
+
children.push({
|
|
231
|
+
type: "field",
|
|
232
|
+
fieldType: "complex",
|
|
233
|
+
instruction: activeComplexField.instruction,
|
|
234
|
+
children: activeComplexField.children,
|
|
235
|
+
});
|
|
236
|
+
activeComplexField = null;
|
|
237
|
+
}
|
|
238
|
+
pushFieldNode(children, child, "simple");
|
|
179
239
|
}
|
|
180
240
|
}
|
|
181
241
|
|
|
242
|
+
if (activeComplexField && activeComplexField.instruction.trim().length > 0) {
|
|
243
|
+
children.push({
|
|
244
|
+
type: "field",
|
|
245
|
+
fieldType: "complex",
|
|
246
|
+
instruction: activeComplexField.instruction,
|
|
247
|
+
children: activeComplexField.children,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
182
251
|
return {
|
|
183
252
|
type: "paragraph",
|
|
184
253
|
...(styleId ? { styleId } : {}),
|
|
254
|
+
...(alignment ? { alignment } : {}),
|
|
255
|
+
...(spacing ? { spacing } : {}),
|
|
256
|
+
...(indentation ? { indentation } : {}),
|
|
185
257
|
children,
|
|
186
258
|
};
|
|
187
259
|
}
|
|
188
260
|
|
|
261
|
+
function appendRunNodes(
|
|
262
|
+
rElement: XmlElementNode,
|
|
263
|
+
nodes: InlineNode[],
|
|
264
|
+
activeComplexField: {
|
|
265
|
+
instruction: string;
|
|
266
|
+
children: InlineNode[];
|
|
267
|
+
mode: "instruction" | "result";
|
|
268
|
+
} | null,
|
|
269
|
+
): {
|
|
270
|
+
instruction: string;
|
|
271
|
+
children: InlineNode[];
|
|
272
|
+
mode: "instruction" | "result";
|
|
273
|
+
} | null {
|
|
274
|
+
const marks: TextMark[] = parseRunProperties(rElement);
|
|
275
|
+
|
|
276
|
+
for (const child of rElement.children) {
|
|
277
|
+
if (child.type !== "element") {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const name = localName(child.name);
|
|
282
|
+
if (name === "fldChar") {
|
|
283
|
+
const fldType = child.attributes["w:fldCharType"] ?? child.attributes.fldCharType;
|
|
284
|
+
if (fldType === "begin") {
|
|
285
|
+
activeComplexField = { instruction: "", children: [], mode: "instruction" };
|
|
286
|
+
} else if (fldType === "separate" && activeComplexField) {
|
|
287
|
+
activeComplexField.mode = "result";
|
|
288
|
+
} else if (fldType === "end" && activeComplexField) {
|
|
289
|
+
if (activeComplexField.instruction.trim().length > 0) {
|
|
290
|
+
nodes.push({
|
|
291
|
+
type: "field",
|
|
292
|
+
fieldType: "complex",
|
|
293
|
+
instruction: activeComplexField.instruction,
|
|
294
|
+
children: activeComplexField.children,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
activeComplexField = null;
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (name === "instrText") {
|
|
303
|
+
if (activeComplexField) {
|
|
304
|
+
activeComplexField.instruction += extractTextContent(child);
|
|
305
|
+
} else {
|
|
306
|
+
pushFieldNode(nodes, child, "complex");
|
|
307
|
+
}
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const inlineNode = parseRunChildNode(child, marks);
|
|
312
|
+
if (!inlineNode) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (activeComplexField?.mode === "result") {
|
|
317
|
+
activeComplexField.children.push(inlineNode);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
nodes.push(inlineNode);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return activeComplexField;
|
|
325
|
+
}
|
|
326
|
+
|
|
189
327
|
function parseRunElement(rElement: XmlElementNode): InlineNode[] {
|
|
190
328
|
const nodes: InlineNode[] = [];
|
|
191
329
|
const marks: TextMark[] = parseRunProperties(rElement);
|
|
@@ -226,14 +364,60 @@ function parseRunElement(rElement: XmlElementNode): InlineNode[] {
|
|
|
226
364
|
}
|
|
227
365
|
} else if (name === "bookmarkStart" || name === "bookmarkEnd") {
|
|
228
366
|
nodes.push(parseBookmarkElement(child));
|
|
229
|
-
} else if (name === "
|
|
230
|
-
nodes
|
|
367
|
+
} else if (name === "instrText") {
|
|
368
|
+
pushFieldNode(nodes, child, "complex");
|
|
231
369
|
}
|
|
232
370
|
}
|
|
233
371
|
|
|
234
372
|
return nodes;
|
|
235
373
|
}
|
|
236
374
|
|
|
375
|
+
function parseRunChildNode(
|
|
376
|
+
child: XmlElementNode,
|
|
377
|
+
marks: TextMark[],
|
|
378
|
+
): InlineNode | null {
|
|
379
|
+
const name = localName(child.name);
|
|
380
|
+
|
|
381
|
+
if (name === "t") {
|
|
382
|
+
const text = extractTextContent(child);
|
|
383
|
+
if (text.length > 0) {
|
|
384
|
+
return {
|
|
385
|
+
type: "text",
|
|
386
|
+
text,
|
|
387
|
+
...(marks.length > 0 ? { marks } : {}),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
if (name === "br") {
|
|
393
|
+
return { type: "hard_break" };
|
|
394
|
+
}
|
|
395
|
+
if (name === "tab") {
|
|
396
|
+
return { type: "tab" };
|
|
397
|
+
}
|
|
398
|
+
if (name === "footnoteReference") {
|
|
399
|
+
const noteId =
|
|
400
|
+
child.attributes["w:id"] ?? child.attributes.id ?? "";
|
|
401
|
+
if (noteId) {
|
|
402
|
+
return { type: "footnote_ref", noteId, noteKind: "footnote" };
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
if (name === "endnoteReference") {
|
|
407
|
+
const noteId =
|
|
408
|
+
child.attributes["w:id"] ?? child.attributes.id ?? "";
|
|
409
|
+
if (noteId) {
|
|
410
|
+
return { type: "footnote_ref", noteId, noteKind: "endnote" };
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
if (name === "bookmarkStart" || name === "bookmarkEnd") {
|
|
415
|
+
return parseBookmarkElement(child);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
237
421
|
function parseHyperlinkElement(element: XmlElementNode): Extract<InlineNode, { type: "hyperlink" }> {
|
|
238
422
|
const href = element.attributes["w:anchor"]
|
|
239
423
|
? `#${element.attributes["w:anchor"]}`
|
|
@@ -275,22 +459,30 @@ function parseBookmarkElement(
|
|
|
275
459
|
};
|
|
276
460
|
}
|
|
277
461
|
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
462
|
+
function pushFieldNode(
|
|
463
|
+
nodes: InlineNode[],
|
|
464
|
+
element: XmlElementNode,
|
|
465
|
+
fieldType: "simple" | "complex",
|
|
466
|
+
): void {
|
|
467
|
+
const instruction = readFieldInstruction(element);
|
|
468
|
+
if (!instruction) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
nodes.push({
|
|
289
473
|
type: "field",
|
|
290
474
|
fieldType,
|
|
291
475
|
instruction,
|
|
292
476
|
children: [],
|
|
293
|
-
};
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function readFieldInstruction(element: XmlElementNode): string | undefined {
|
|
481
|
+
const instruction =
|
|
482
|
+
element.attributes["w:instr"] ??
|
|
483
|
+
element.attributes.instr ??
|
|
484
|
+
extractTextContent(element);
|
|
485
|
+
return instruction.trim().length > 0 ? instruction : undefined;
|
|
294
486
|
}
|
|
295
487
|
|
|
296
488
|
function parseRunProperties(rElement: XmlElementNode): TextMark[] {
|
|
@@ -322,12 +514,75 @@ function parseRunProperties(rElement: XmlElementNode): TextMark[] {
|
|
|
322
514
|
case "strike":
|
|
323
515
|
if (val !== "0" && val !== "false") marks.push({ type: "strikethrough" });
|
|
324
516
|
break;
|
|
517
|
+
case "dstrike":
|
|
518
|
+
if (val !== "0" && val !== "false") marks.push({ type: "doubleStrikethrough" });
|
|
519
|
+
break;
|
|
520
|
+
case "rFonts": {
|
|
521
|
+
const family =
|
|
522
|
+
child.attributes["w:ascii"] ??
|
|
523
|
+
child.attributes["w:hAnsi"] ??
|
|
524
|
+
child.attributes.ascii ??
|
|
525
|
+
child.attributes.hAnsi;
|
|
526
|
+
if (family) marks.push({ type: "fontFamily", val: family });
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case "sz": {
|
|
530
|
+
const szVal = child.attributes["w:val"] ?? child.attributes.val;
|
|
531
|
+
if (szVal) {
|
|
532
|
+
const size = Number.parseInt(szVal, 10);
|
|
533
|
+
if (Number.isFinite(size) && size > 0) marks.push({ type: "fontSize", val: size });
|
|
534
|
+
}
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case "color": {
|
|
538
|
+
const colorVal = child.attributes["w:val"] ?? child.attributes.val;
|
|
539
|
+
if (colorVal && colorVal !== "auto") marks.push({ type: "textColor", color: colorVal });
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case "smallCaps":
|
|
543
|
+
if (val !== "0" && val !== "false") marks.push({ type: "smallCaps" });
|
|
544
|
+
break;
|
|
545
|
+
case "caps":
|
|
546
|
+
if (val !== "0" && val !== "false") marks.push({ type: "allCaps" });
|
|
547
|
+
break;
|
|
325
548
|
}
|
|
326
549
|
}
|
|
327
550
|
|
|
328
551
|
return marks;
|
|
329
552
|
}
|
|
330
553
|
|
|
554
|
+
function readParagraphSpacing(pPr: XmlElementNode): ParagraphSpacing | undefined {
|
|
555
|
+
const spacingNode = findChildElementOptional(pPr, "spacing");
|
|
556
|
+
if (!spacingNode) return undefined;
|
|
557
|
+
const result: ParagraphSpacing = {};
|
|
558
|
+
const before = spacingNode.attributes["w:before"] ?? spacingNode.attributes.before;
|
|
559
|
+
if (before) result.before = Number.parseInt(before, 10);
|
|
560
|
+
const after = spacingNode.attributes["w:after"] ?? spacingNode.attributes.after;
|
|
561
|
+
if (after) result.after = Number.parseInt(after, 10);
|
|
562
|
+
const line = spacingNode.attributes["w:line"] ?? spacingNode.attributes.line;
|
|
563
|
+
if (line) result.line = Number.parseInt(line, 10);
|
|
564
|
+
const lineRule = spacingNode.attributes["w:lineRule"] ?? spacingNode.attributes.lineRule;
|
|
565
|
+
if (lineRule === "auto" || lineRule === "exact" || lineRule === "atLeast") {
|
|
566
|
+
result.lineRule = lineRule;
|
|
567
|
+
}
|
|
568
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function readParagraphIndentation(pPr: XmlElementNode): ParagraphIndentation | undefined {
|
|
572
|
+
const indNode = findChildElementOptional(pPr, "ind");
|
|
573
|
+
if (!indNode) return undefined;
|
|
574
|
+
const result: ParagraphIndentation = {};
|
|
575
|
+
const left = indNode.attributes["w:left"] ?? indNode.attributes.left;
|
|
576
|
+
if (left) result.left = Number.parseInt(left, 10);
|
|
577
|
+
const right = indNode.attributes["w:right"] ?? indNode.attributes.right;
|
|
578
|
+
if (right) result.right = Number.parseInt(right, 10);
|
|
579
|
+
const firstLine = indNode.attributes["w:firstLine"] ?? indNode.attributes.firstLine;
|
|
580
|
+
if (firstLine) result.firstLine = Number.parseInt(firstLine, 10);
|
|
581
|
+
const hanging = indNode.attributes["w:hanging"] ?? indNode.attributes.hanging;
|
|
582
|
+
if (hanging) result.hanging = Number.parseInt(hanging, 10);
|
|
583
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
584
|
+
}
|
|
585
|
+
|
|
331
586
|
function extractTextContent(tElement: XmlElementNode): string {
|
|
332
587
|
let text = "";
|
|
333
588
|
for (const child of tElement.children) {
|
|
@@ -353,6 +608,181 @@ function localName(name: string): string {
|
|
|
353
608
|
return idx >= 0 ? name.slice(idx + 1) : name;
|
|
354
609
|
}
|
|
355
610
|
|
|
611
|
+
// ---- Simple secondary-story table support ----
|
|
612
|
+
|
|
613
|
+
const RISKY_TABLE_ELEMENT_NAMES = new Set([
|
|
614
|
+
"ins",
|
|
615
|
+
"del",
|
|
616
|
+
"moveFrom",
|
|
617
|
+
"moveTo",
|
|
618
|
+
"tblPrChange",
|
|
619
|
+
"trPrChange",
|
|
620
|
+
"tcPrChange",
|
|
621
|
+
"rPrChange",
|
|
622
|
+
"pPrChange",
|
|
623
|
+
"sectPrChange",
|
|
624
|
+
"fldSimple",
|
|
625
|
+
"fldChar",
|
|
626
|
+
"instrText",
|
|
627
|
+
"sdt",
|
|
628
|
+
"customXml",
|
|
629
|
+
]);
|
|
630
|
+
|
|
631
|
+
function isSimpleSecondaryStoryTable(tblElement: XmlElementNode): boolean {
|
|
632
|
+
return !containsRiskyElement(tblElement);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function containsRiskyElement(element: XmlElementNode): boolean {
|
|
636
|
+
for (const child of element.children) {
|
|
637
|
+
if (child.type !== "element") {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const name = localName(child.name);
|
|
641
|
+
if (RISKY_TABLE_ELEMENT_NAMES.has(name)) {
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
if (name === "tbl") {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
if (containsRiskyElement(child)) {
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function parseSimpleTableElement(tblElement: XmlElementNode): TableNode {
|
|
655
|
+
let gridColumns: number[] = [];
|
|
656
|
+
const rows: TableRowNode[] = [];
|
|
657
|
+
let propertiesXml: string | undefined;
|
|
658
|
+
let styleId: string | undefined;
|
|
659
|
+
|
|
660
|
+
for (const child of tblElement.children) {
|
|
661
|
+
if (child.type !== "element") continue;
|
|
662
|
+
const name = localName(child.name);
|
|
663
|
+
|
|
664
|
+
if (name === "tblPr") {
|
|
665
|
+
propertiesXml = serializeElementToXml(child);
|
|
666
|
+
const pStyle = findChildElementOptional(child, "tblStyle");
|
|
667
|
+
styleId = pStyle?.attributes["w:val"] ?? pStyle?.attributes.val;
|
|
668
|
+
} else if (name === "tblGrid") {
|
|
669
|
+
gridColumns = readGridColumns(child);
|
|
670
|
+
} else if (name === "tr") {
|
|
671
|
+
rows.push(parseSimpleTableRow(child));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
type: "table",
|
|
677
|
+
...(styleId ? { styleId } : {}),
|
|
678
|
+
...(propertiesXml ? { propertiesXml } : {}),
|
|
679
|
+
gridColumns,
|
|
680
|
+
rows,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function readGridColumns(tblGrid: XmlElementNode): number[] {
|
|
685
|
+
const columns: number[] = [];
|
|
686
|
+
for (const child of tblGrid.children) {
|
|
687
|
+
if (child.type !== "element") continue;
|
|
688
|
+
if (localName(child.name) === "gridCol") {
|
|
689
|
+
const w = child.attributes["w:w"] ?? child.attributes.w ?? "0";
|
|
690
|
+
columns.push(Number.parseInt(w, 10) || 0);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return columns;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function parseSimpleTableRow(trElement: XmlElementNode): TableRowNode {
|
|
697
|
+
const cells: TableCellNode[] = [];
|
|
698
|
+
let propertiesXml: string | undefined;
|
|
699
|
+
|
|
700
|
+
for (const child of trElement.children) {
|
|
701
|
+
if (child.type !== "element") continue;
|
|
702
|
+
const name = localName(child.name);
|
|
703
|
+
|
|
704
|
+
if (name === "trPr") {
|
|
705
|
+
propertiesXml = serializeElementToXml(child);
|
|
706
|
+
} else if (name === "tc") {
|
|
707
|
+
cells.push(parseSimpleTableCell(child));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
type: "table_row",
|
|
713
|
+
...(propertiesXml ? { propertiesXml } : {}),
|
|
714
|
+
cells,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function parseSimpleTableCell(tcElement: XmlElementNode): TableCellNode {
|
|
719
|
+
const children: BlockNode[] = [];
|
|
720
|
+
let propertiesXml: string | undefined;
|
|
721
|
+
let gridSpan: number | undefined;
|
|
722
|
+
let verticalMerge: "restart" | "continue" | undefined;
|
|
723
|
+
|
|
724
|
+
for (const child of tcElement.children) {
|
|
725
|
+
if (child.type !== "element") continue;
|
|
726
|
+
const name = localName(child.name);
|
|
727
|
+
|
|
728
|
+
if (name === "tcPr") {
|
|
729
|
+
propertiesXml = serializeElementToXml(child);
|
|
730
|
+
const gsEl = findChildElementOptional(child, "gridSpan");
|
|
731
|
+
const gsVal = gsEl?.attributes["w:val"] ?? gsEl?.attributes.val;
|
|
732
|
+
if (gsVal) gridSpan = Number.parseInt(gsVal, 10) || undefined;
|
|
733
|
+
|
|
734
|
+
const vmEl = findChildElementOptional(child, "vMerge");
|
|
735
|
+
if (vmEl) {
|
|
736
|
+
const vmVal = vmEl.attributes["w:val"] ?? vmEl.attributes.val ?? "continue";
|
|
737
|
+
verticalMerge = vmVal === "restart" ? "restart" : "continue";
|
|
738
|
+
}
|
|
739
|
+
} else if (name === "p") {
|
|
740
|
+
children.push(parseParagraphElement(child));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
type: "table_cell",
|
|
746
|
+
...(propertiesXml ? { propertiesXml } : {}),
|
|
747
|
+
...(gridSpan ? { gridSpan } : {}),
|
|
748
|
+
...(verticalMerge ? { verticalMerge } : {}),
|
|
749
|
+
children: children.length > 0 ? children : [{ type: "paragraph", children: [] }],
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function serializeElementToXml(element: XmlElementNode): string {
|
|
754
|
+
const attrs = Object.entries(element.attributes)
|
|
755
|
+
.map(([key, value]) => ` ${key}="${escapeXmlAttribute(value)}"`)
|
|
756
|
+
.join("");
|
|
757
|
+
const children = element.children
|
|
758
|
+
.map((child) => {
|
|
759
|
+
if (child.type === "text") {
|
|
760
|
+
return escapeXmlText(child.text);
|
|
761
|
+
}
|
|
762
|
+
return serializeElementToXml(child);
|
|
763
|
+
})
|
|
764
|
+
.join("");
|
|
765
|
+
if (children.length === 0) {
|
|
766
|
+
return `<${element.name}${attrs}/>`;
|
|
767
|
+
}
|
|
768
|
+
return `<${element.name}${attrs}>${children}</${element.name}>`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function escapeXmlAttribute(text: string): string {
|
|
772
|
+
return text
|
|
773
|
+
.replace(/&/g, "&")
|
|
774
|
+
.replace(/"/g, """)
|
|
775
|
+
.replace(/</g, "<")
|
|
776
|
+
.replace(/>/g, ">");
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function escapeXmlText(text: string): string {
|
|
780
|
+
return text
|
|
781
|
+
.replace(/&/g, "&")
|
|
782
|
+
.replace(/</g, "<")
|
|
783
|
+
.replace(/>/g, ">");
|
|
784
|
+
}
|
|
785
|
+
|
|
356
786
|
// ---- Minimal XML parser (same pattern as parse-numbering.ts) ----
|
|
357
787
|
|
|
358
788
|
function parseXml(xml: string): XmlElementNode {
|