@beyondwork/docx-react-component 1.0.28 → 1.0.30

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.
Files changed (92) hide show
  1. package/package.json +26 -37
  2. package/src/api/public-types.ts +531 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/index.ts +201 -79
  5. package/src/core/commands/table-structure-commands.ts +138 -5
  6. package/src/core/state/text-transaction.ts +370 -3
  7. package/src/index.ts +41 -0
  8. package/src/io/docx-session.ts +318 -25
  9. package/src/io/export/serialize-footnotes.ts +41 -46
  10. package/src/io/export/serialize-headers-footers.ts +36 -40
  11. package/src/io/export/serialize-main-document.ts +55 -89
  12. package/src/io/export/serialize-numbering.ts +104 -4
  13. package/src/io/export/serialize-runtime-revisions.ts +196 -2
  14. package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
  15. package/src/io/export/table-properties-xml.ts +318 -0
  16. package/src/io/normalize/normalize-text.ts +34 -3
  17. package/src/io/ooxml/parse-comments.ts +6 -0
  18. package/src/io/ooxml/parse-footnotes.ts +69 -13
  19. package/src/io/ooxml/parse-headers-footers.ts +54 -11
  20. package/src/io/ooxml/parse-main-document.ts +112 -42
  21. package/src/io/ooxml/parse-numbering.ts +341 -26
  22. package/src/io/ooxml/parse-revisions.ts +118 -4
  23. package/src/io/ooxml/parse-styles.ts +176 -0
  24. package/src/io/ooxml/parse-tables.ts +34 -25
  25. package/src/io/ooxml/revision-boundaries.ts +127 -3
  26. package/src/io/ooxml/workflow-payload.ts +544 -0
  27. package/src/model/canonical-document.ts +91 -1
  28. package/src/model/snapshot.ts +112 -1
  29. package/src/preservation/store.ts +73 -3
  30. package/src/review/store/comment-store.ts +19 -1
  31. package/src/review/store/revision-actions.ts +29 -0
  32. package/src/review/store/revision-store.ts +12 -1
  33. package/src/review/store/revision-types.ts +11 -0
  34. package/src/runtime/context-analytics.ts +824 -0
  35. package/src/runtime/document-locations.ts +521 -0
  36. package/src/runtime/document-navigation.ts +14 -1
  37. package/src/runtime/document-outline.ts +440 -0
  38. package/src/runtime/document-runtime.ts +941 -45
  39. package/src/runtime/event-refresh-hints.ts +137 -0
  40. package/src/runtime/numbering-prefix.ts +67 -39
  41. package/src/runtime/page-layout-estimation.ts +100 -7
  42. package/src/runtime/resolved-numbering-geometry.ts +293 -0
  43. package/src/runtime/session-capabilities.ts +2 -2
  44. package/src/runtime/suggestions-snapshot.ts +137 -0
  45. package/src/runtime/surface-projection.ts +223 -27
  46. package/src/runtime/table-style-resolver.ts +409 -0
  47. package/src/runtime/view-state.ts +17 -1
  48. package/src/runtime/workflow-markup.ts +54 -14
  49. package/src/ui/WordReviewEditor.tsx +1269 -87
  50. package/src/ui/editor-command-bag.ts +7 -0
  51. package/src/ui/editor-runtime-boundary.ts +111 -10
  52. package/src/ui/editor-shell-view.tsx +17 -15
  53. package/src/ui/editor-surface-controller.tsx +5 -0
  54. package/src/ui/headless/selection-tool-context.ts +19 -0
  55. package/src/ui/headless/selection-tool-resolver.ts +752 -0
  56. package/src/ui/headless/selection-tool-types.ts +129 -0
  57. package/src/ui/headless/selection-toolbar-model.ts +10 -33
  58. package/src/ui/runtime-shortcut-dispatch.ts +365 -0
  59. package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
  60. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
  61. package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
  62. package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
  63. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
  64. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
  65. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
  66. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
  67. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
  69. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
  70. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
  72. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
  73. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
  76. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
  77. package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
  78. package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
  79. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
  80. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  81. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
  82. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
  83. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
  84. package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
  85. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
  87. package/src/ui-tailwind/theme/editor-theme.css +58 -40
  88. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
  89. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
  90. package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
  91. package/src/validation/compatibility-engine.ts +246 -2
  92. package/src/validation/docx-comment-proof.ts +24 -11
@@ -8,6 +8,14 @@ import type {
8
8
  TableRowNode,
9
9
  TextMark,
10
10
  } from "../../model/canonical-document.ts";
11
+ import type { RevisionRecord as ReviewRevisionRecord } from "../../review/store/revision-types.ts";
12
+ import { serializeRuntimeRevisionsIntoStoryXml } from "./serialize-runtime-revisions.ts";
13
+ import { splitStoryBlocksForRuntimeRevisions } from "./split-story-blocks-for-runtime-revisions.ts";
14
+ import {
15
+ serializeTableCellPropertiesXml,
16
+ serializeTablePropertiesXml,
17
+ serializeTableRowPropertiesXml,
18
+ } from "./table-properties-xml.ts";
11
19
 
12
20
  export const WORD_FOOTNOTES_CONTENT_TYPE =
13
21
  "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml";
@@ -21,12 +29,16 @@ const R_NS = `xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/rel
21
29
  * Serialize the footnotes portion of a FootnoteCollection to footnotes.xml.
22
30
  * Includes the required separator and continuation-separator stubs.
23
31
  */
24
- export function serializeFootnotesXml(collection: FootnoteCollection): string {
32
+ export function serializeFootnotesXml(
33
+ collection: FootnoteCollection,
34
+ revisionsByNoteId: Record<string, readonly ReviewRevisionRecord[]> = {},
35
+ ): string {
25
36
  const entries = Object.values(collection.footnotes).sort(compareNoteIds);
26
37
  const body = [
27
38
  serializeSeparatorStub("footnote", "-1", "separator"),
28
39
  serializeSeparatorStub("footnote", "0", "continuationSeparator"),
29
- ...entries.map((entry) => serializeNoteDefinition("footnote", entry)),
40
+ ...entries.map((entry) =>
41
+ serializeNoteDefinition("footnote", entry, revisionsByNoteId[entry.noteId] ?? [])),
30
42
  ].join("");
31
43
 
32
44
  return [
@@ -39,12 +51,16 @@ export function serializeFootnotesXml(collection: FootnoteCollection): string {
39
51
  * Serialize the endnotes portion of a FootnoteCollection to endnotes.xml.
40
52
  * Includes the required separator and continuation-separator stubs.
41
53
  */
42
- export function serializeEndnotesXml(collection: FootnoteCollection): string {
54
+ export function serializeEndnotesXml(
55
+ collection: FootnoteCollection,
56
+ revisionsByNoteId: Record<string, readonly ReviewRevisionRecord[]> = {},
57
+ ): string {
43
58
  const entries = Object.values(collection.endnotes).sort(compareNoteIds);
44
59
  const body = [
45
60
  serializeSeparatorStub("endnote", "-1", "separator"),
46
61
  serializeSeparatorStub("endnote", "0", "continuationSeparator"),
47
- ...entries.map((entry) => serializeNoteDefinition("endnote", entry)),
62
+ ...entries.map((entry) =>
63
+ serializeNoteDefinition("endnote", entry, revisionsByNoteId[entry.noteId] ?? [])),
48
64
  ].join("");
49
65
 
50
66
  return [
@@ -67,9 +83,11 @@ function serializeSeparatorStub(
67
83
  function serializeNoteDefinition(
68
84
  kind: "footnote" | "endnote",
69
85
  definition: FootnoteDefinition,
86
+ revisions: readonly ReviewRevisionRecord[] = [],
70
87
  ): string {
71
88
  const tag = kind === "footnote" ? "w:footnote" : "w:endnote";
72
- const blocks = definition.blocks
89
+ const revisionReadyBlocks = splitStoryBlocksForRuntimeRevisions(definition.blocks, revisions);
90
+ const blocks = revisionReadyBlocks
73
91
  .map((block) => {
74
92
  if (block.type === "paragraph") {
75
93
  return serializeParagraph(block);
@@ -85,7 +103,17 @@ function serializeNoteDefinition(
85
103
  .join("");
86
104
 
87
105
  const body = blocks || `<w:p><w:r><w:t></w:t></w:r></w:p>`;
88
- return `<${tag} w:id="${escapeAttribute(definition.noteId)}">${body}</${tag}>`;
106
+ const noteXml = `<${tag} w:id="${escapeAttribute(definition.noteId)}">${body}</${tag}>`;
107
+ if (revisions.length === 0) {
108
+ return noteXml;
109
+ }
110
+ const serialized = serializeRuntimeRevisionsIntoStoryXml(noteXml, revisions);
111
+ if (serialized.skippedRevisionIds.length > 0) {
112
+ throw new Error(
113
+ `Cannot safely serialize ${serialized.skippedRevisionIds.length} revisions for ${kind} ${definition.noteId}.`,
114
+ );
115
+ }
116
+ return serialized.documentXml;
89
117
  }
90
118
 
91
119
  function serializeParagraph(paragraph: ParagraphNode): string {
@@ -107,8 +135,9 @@ function serializeParagraph(paragraph: ParagraphNode): string {
107
135
 
108
136
  function serializeTable(table: TableNode): string {
109
137
  let xml = "<w:tbl>";
110
- if (table.propertiesXml) {
111
- xml += table.propertiesXml;
138
+ const propertiesXml = serializeTablePropertiesXml(table);
139
+ if (propertiesXml) {
140
+ xml += propertiesXml;
112
141
  }
113
142
  if (table.gridColumns.length > 0) {
114
143
  xml += "<w:tblGrid>";
@@ -126,8 +155,9 @@ function serializeTable(table: TableNode): string {
126
155
 
127
156
  function serializeTableRow(row: TableRowNode): string {
128
157
  let xml = "<w:tr>";
129
- if (row.propertiesXml) {
130
- xml += wrapPropertiesXml("w:trPr", row.propertiesXml);
158
+ const propertiesXml = serializeTableRowPropertiesXml(row);
159
+ if (propertiesXml) {
160
+ xml += propertiesXml;
131
161
  }
132
162
  for (const cell of row.cells) {
133
163
  xml += serializeTableCell(cell);
@@ -359,41 +389,6 @@ function serializeHyperlinkNode(node: Extract<InlineNode, { type: "hyperlink" }>
359
389
  return `<w:hyperlink r:id="${escapeAttribute(node.href)}">${childrenXml}</w:hyperlink>`;
360
390
  }
361
391
 
362
- function wrapPropertiesXml(tagName: "w:trPr" | "w:tcPr", xml: string): string {
363
- const trimmed = xml.trim();
364
- if (trimmed.startsWith(`<${tagName}`)) {
365
- return trimmed;
366
- }
367
- return `<${tagName}>${trimmed}</${tagName}>`;
368
- }
369
-
370
- function extractWrappedChildren(tagName: "w:tcPr", xml: string | undefined): string {
371
- if (!xml) {
372
- return "";
373
- }
374
- const trimmed = xml.trim();
375
- const wrapped = new RegExp(`^<${tagName}\\b[^>]*>([\\s\\S]*)</${tagName}>$`, "u").exec(trimmed);
376
- return wrapped?.[1] ?? trimmed;
377
- }
378
-
379
392
  function buildTableCellPropertiesXml(cell: TableCellNode): string {
380
- const innerXml = extractWrappedChildren("w:tcPr", cell.propertiesXml)
381
- .replace(/<w:gridSpan\b[^>]*\/>/gu, "")
382
- .replace(/<w:vMerge\b[^>]*\/>/gu, "")
383
- .trim();
384
- const parts: string[] = [];
385
- if (cell.gridSpan && cell.gridSpan > 1) {
386
- parts.push(`<w:gridSpan w:val="${cell.gridSpan}"/>`);
387
- }
388
- if (cell.verticalMerge) {
389
- parts.push(
390
- cell.verticalMerge === "restart"
391
- ? `<w:vMerge w:val="restart"/>`
392
- : `<w:vMerge/>`,
393
- );
394
- }
395
- if (innerXml.length > 0) {
396
- parts.push(innerXml);
397
- }
398
- return parts.length > 0 ? `<w:tcPr>${parts.join("")}</w:tcPr>` : "";
393
+ return serializeTableCellPropertiesXml(cell);
399
394
  }
@@ -9,6 +9,13 @@ import type {
9
9
  TableRowNode,
10
10
  TextMark,
11
11
  } from "../../model/canonical-document.ts";
12
+ import type { RevisionRecord as ReviewRevisionRecord } from "../../review/store/revision-types.ts";
13
+ import { splitStoryBlocksForRuntimeRevisions } from "./split-story-blocks-for-runtime-revisions.ts";
14
+ import {
15
+ serializeTableCellPropertiesXml,
16
+ serializeTablePropertiesXml,
17
+ serializeTableRowPropertiesXml,
18
+ } from "./table-properties-xml.ts";
12
19
 
13
20
  export const WORD_HEADER_CONTENT_TYPE =
14
21
  "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml";
@@ -40,6 +47,17 @@ export function serializeHeaderXml(header: HeaderDocument): string {
40
47
  ].join("\n");
41
48
  }
42
49
 
50
+ export function serializeHeaderXmlWithRevisions(
51
+ header: HeaderDocument,
52
+ revisions: readonly ReviewRevisionRecord[] = [],
53
+ ): string {
54
+ const body = serializeBlocks(splitStoryBlocksForRuntimeRevisions(header.blocks, revisions));
55
+ return [
56
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
57
+ `<w:hdr ${W_NS} ${R_NS} ${EXTENDED_ROOT_ATTRS}>${body}</w:hdr>`,
58
+ ].join("\n");
59
+ }
60
+
43
61
  /**
44
62
  * Serialize a FooterDocument into a footerN.xml string.
45
63
  */
@@ -51,6 +69,17 @@ export function serializeFooterXml(footer: FooterDocument): string {
51
69
  ].join("\n");
52
70
  }
53
71
 
72
+ export function serializeFooterXmlWithRevisions(
73
+ footer: FooterDocument,
74
+ revisions: readonly ReviewRevisionRecord[] = [],
75
+ ): string {
76
+ const body = serializeBlocks(splitStoryBlocksForRuntimeRevisions(footer.blocks, revisions));
77
+ return [
78
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
79
+ `<w:ftr ${W_NS} ${R_NS} ${EXTENDED_ROOT_ATTRS}>${body}</w:ftr>`,
80
+ ].join("\n");
81
+ }
82
+
54
83
  // ---- Internal serialization ----
55
84
 
56
85
  function serializeBlocks(
@@ -96,8 +125,9 @@ function serializeParagraph(paragraph: ParagraphNode): string {
96
125
 
97
126
  function serializeTable(table: TableNode): string {
98
127
  let xml = "<w:tbl>";
99
- if (table.propertiesXml) {
100
- xml += table.propertiesXml;
128
+ const propertiesXml = serializeTablePropertiesXml(table);
129
+ if (propertiesXml) {
130
+ xml += propertiesXml;
101
131
  }
102
132
  if (table.gridColumns.length > 0) {
103
133
  xml += "<w:tblGrid>";
@@ -115,8 +145,9 @@ function serializeTable(table: TableNode): string {
115
145
 
116
146
  function serializeTableRow(row: TableRowNode): string {
117
147
  let xml = "<w:tr>";
118
- if (row.propertiesXml) {
119
- xml += wrapPropertiesXml("w:trPr", row.propertiesXml);
148
+ const propertiesXml = serializeTableRowPropertiesXml(row);
149
+ if (propertiesXml) {
150
+ xml += propertiesXml;
120
151
  }
121
152
  for (const cell of row.cells) {
122
153
  xml += serializeTableCell(cell);
@@ -332,41 +363,6 @@ function serializeHyperlinkNode(node: Extract<InlineNode, { type: "hyperlink" }>
332
363
  return `<w:hyperlink r:id="${escapeAttribute(node.href)}">${childrenXml}</w:hyperlink>`;
333
364
  }
334
365
 
335
- function wrapPropertiesXml(tagName: "w:trPr" | "w:tcPr", xml: string): string {
336
- const trimmed = xml.trim();
337
- if (trimmed.startsWith(`<${tagName}`)) {
338
- return trimmed;
339
- }
340
- return `<${tagName}>${trimmed}</${tagName}>`;
341
- }
342
-
343
- function extractWrappedChildren(tagName: "w:tcPr", xml: string | undefined): string {
344
- if (!xml) {
345
- return "";
346
- }
347
- const trimmed = xml.trim();
348
- const wrapped = new RegExp(`^<${tagName}\\b[^>]*>([\\s\\S]*)</${tagName}>$`, "u").exec(trimmed);
349
- return wrapped?.[1] ?? trimmed;
350
- }
351
-
352
366
  function buildTableCellPropertiesXml(cell: TableCellNode): string {
353
- const innerXml = extractWrappedChildren("w:tcPr", cell.propertiesXml)
354
- .replace(/<w:gridSpan\b[^>]*\/>/gu, "")
355
- .replace(/<w:vMerge\b[^>]*\/>/gu, "")
356
- .trim();
357
- const parts: string[] = [];
358
- if (cell.gridSpan && cell.gridSpan > 1) {
359
- parts.push(`<w:gridSpan w:val="${cell.gridSpan}"/>`);
360
- }
361
- if (cell.verticalMerge) {
362
- parts.push(
363
- cell.verticalMerge === "restart"
364
- ? `<w:vMerge w:val="restart"/>`
365
- : `<w:vMerge/>`,
366
- );
367
- }
368
- if (innerXml.length > 0) {
369
- parts.push(innerXml);
370
- }
371
- return parts.length > 0 ? `<w:tcPr>${parts.join("")}</w:tcPr>` : "";
367
+ return serializeTableCellPropertiesXml(cell);
372
368
  }
@@ -18,6 +18,11 @@ import type { RevisionParagraphBoundary } from "../ooxml/revision-boundaries.ts"
18
18
  import { getOpaqueFragment } from "../../preservation/store.ts";
19
19
  import { retainRelationshipsForFragment } from "../../preservation/relationship-retention.ts";
20
20
  import { serializeParagraphNumberingProperties } from "./serialize-numbering.ts";
21
+ import {
22
+ serializeTableCellPropertiesXml,
23
+ serializeTablePropertiesXml,
24
+ serializeTableRowPropertiesXml,
25
+ } from "./table-properties-xml.ts";
21
26
 
22
27
  const HYPERLINK_RELATIONSHIP_TYPE =
23
28
  "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
@@ -209,7 +214,7 @@ function serializeTableNode(
209
214
  table: TableNode,
210
215
  state: SerializationState,
211
216
  ): string {
212
- const propertiesXml = table.propertiesXml ?? buildTablePropertiesXml(table);
217
+ const propertiesXml = buildTablePropertiesXml(table);
213
218
  const gridXml =
214
219
  table.gridColumns.length > 0
215
220
  ? `<w:tblGrid>${table.gridColumns
@@ -218,7 +223,7 @@ function serializeTableNode(
218
223
  : "";
219
224
  const rowsXml = table.rows
220
225
  .map((row) => {
221
- const rowPropertiesXml = row.propertiesXml ?? buildTableRowPropertiesXml(row);
226
+ const rowPropertiesXml = buildTableRowPropertiesXml(row);
222
227
  const cellsXml = row.cells
223
228
  .map((cell) => serializeTableCellNode(cell, state))
224
229
  .join("");
@@ -232,7 +237,7 @@ function serializeTableCellNode(
232
237
  cell: TableCellNode,
233
238
  state: SerializationState,
234
239
  ): string {
235
- const propertiesXml = cell.propertiesXml ?? buildCellPropertiesXml(cell);
240
+ const propertiesXml = buildCellPropertiesXml(cell);
236
241
  const blocksXml = cell.children
237
242
  .map((child) => serializeBlockNode(child, state))
238
243
  .join("");
@@ -257,47 +262,26 @@ function serializeBlockNode(
257
262
  case "opaque_block":
258
263
  return lookupOpaqueXml(block.fragmentId, state);
259
264
  case "section_break":
260
- if (block.sectionPropertiesXml ?? block.propertiesXml) {
261
- return block.sectionPropertiesXml ?? block.propertiesXml!;
262
- }
263
- if (block.sectionProperties) return serializeSectionPropertiesXml(block.sectionProperties);
264
- return "<w:sectPr/>";
265
+ return serializeNestedSectionBreak(block);
265
266
  }
266
267
  }
267
268
 
269
+ function serializeNestedSectionBreak(
270
+ block: Extract<DocumentRootNode["children"][number], { type: "section_break" }>,
271
+ ): string {
272
+ const sectionPropertiesXml =
273
+ block.sectionPropertiesXml ??
274
+ block.propertiesXml ??
275
+ (block.sectionProperties ? serializeSectionPropertiesXml(block.sectionProperties) : "<w:sectPr/>");
276
+ return `<w:p><w:pPr>${sectionPropertiesXml}</w:pPr></w:p>`;
277
+ }
278
+
268
279
  function buildCellPropertiesXml(cell: TableCellNode): string {
269
- const children: string[] = [];
270
- if (cell.gridSpan && cell.gridSpan > 1) {
271
- children.push(`<w:gridSpan w:val="${cell.gridSpan}"/>`);
272
- }
273
- if (cell.verticalMerge) {
274
- children.push(
275
- cell.verticalMerge === "restart"
276
- ? `<w:vMerge w:val="restart"/>`
277
- : `<w:vMerge/>`,
278
- );
279
- }
280
- return children.length > 0 ? `<w:tcPr>${children.join("")}</w:tcPr>` : "";
280
+ return serializeTableCellPropertiesXml(cell);
281
281
  }
282
282
 
283
283
  function buildTableRowPropertiesXml(row: TableNode["rows"][number]): string {
284
- const children: string[] = [];
285
- if (row.gridBefore !== undefined) {
286
- children.push(`<w:gridBefore w:val="${row.gridBefore}"/>`);
287
- }
288
- if (row.widthBefore) {
289
- children.push(`<w:wBefore w:w="${row.widthBefore.value}" w:type="${row.widthBefore.type}"/>`);
290
- }
291
- if (row.gridAfter !== undefined) {
292
- children.push(`<w:gridAfter w:val="${row.gridAfter}"/>`);
293
- }
294
- if (row.widthAfter) {
295
- children.push(`<w:wAfter w:w="${row.widthAfter.value}" w:type="${row.widthAfter.type}"/>`);
296
- }
297
- if (children.length === 0) {
298
- return "";
299
- }
300
- return `<w:trPr>${children.join("")}</w:trPr>`;
284
+ return serializeTableRowPropertiesXml(row);
301
285
  }
302
286
 
303
287
  function serializeSdtNode(
@@ -313,6 +297,9 @@ function serializeCustomXmlNode(
313
297
  block: CustomXmlNode,
314
298
  state: SerializationState,
315
299
  ): string {
300
+ if (block.rawXml) {
301
+ return block.rawXml;
302
+ }
316
303
  const attrs: string[] = [];
317
304
  if (block.uri) {
318
305
  attrs.push(`w:uri="${escapeAttribute(block.uri)}"`);
@@ -573,33 +560,7 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
573
560
  }
574
561
 
575
562
  function buildTablePropertiesXml(table: TableNode): string {
576
- const children: string[] = [];
577
- if (table.styleId) {
578
- children.push(`<w:tblStyle w:val="${escapeAttribute(table.styleId)}"/>`);
579
- }
580
- if (table.tblLook) {
581
- const attrs: string[] = [];
582
- if (table.tblLook.val) {
583
- attrs.push(`w:val="${escapeAttribute(table.tblLook.val)}"`);
584
- }
585
- for (const [key, attr] of [
586
- ["firstRow", "w:firstRow"],
587
- ["lastRow", "w:lastRow"],
588
- ["firstColumn", "w:firstColumn"],
589
- ["lastColumn", "w:lastColumn"],
590
- ["noHBand", "w:noHBand"],
591
- ["noVBand", "w:noVBand"],
592
- ] as const) {
593
- const value = table.tblLook[key];
594
- if (value !== undefined) {
595
- attrs.push(`${attr}="${value ? "1" : "0"}"`);
596
- }
597
- }
598
- if (attrs.length > 0) {
599
- children.push(`<w:tblLook ${attrs.join(" ")}/>`);
600
- }
601
- }
602
- return children.length > 0 ? `<w:tblPr>${children.join("")}</w:tblPr>` : "";
563
+ return serializeTablePropertiesXml(table);
603
564
  }
604
565
 
605
566
  function serializeParagraphBorders(borders: ParagraphNode["borders"]): string {
@@ -1347,30 +1308,35 @@ function wrapInlineRawXml(rawXml: string): string {
1347
1308
  }
1348
1309
 
1349
1310
  function documentNeedsW14Namespace(content: DocumentRootNode): boolean {
1350
- const blockQueue = [...content.children];
1351
- while (blockQueue.length > 0) {
1352
- const block = blockQueue.shift();
1353
- if (!block) {
1354
- continue;
1355
- }
1356
- if (block.type === "paragraph") {
1357
- for (const child of block.children) {
1358
- if (
1359
- (child.type === "text" || child.type === "symbol") &&
1360
- child.marks?.some((mark) => mark.type === "textFill")
1361
- ) {
1362
- return true;
1363
- }
1364
- }
1365
- continue;
1366
- }
1367
- if (block.type === "table") {
1368
- for (const row of block.rows) {
1369
- for (const cell of row.cells) {
1370
- blockQueue.push(...cell.children);
1371
- }
1372
- }
1373
- }
1311
+ return content.children.some(blockNeedsW14Namespace);
1312
+ }
1313
+
1314
+ function blockNeedsW14Namespace(block: DocumentRootNode["children"][number]): boolean {
1315
+ switch (block.type) {
1316
+ case "paragraph":
1317
+ return block.children.some(inlineNodeNeedsW14Namespace);
1318
+ case "table":
1319
+ return block.rows.some((row) =>
1320
+ row.cells.some((cell) => cell.children.some(blockNeedsW14Namespace)),
1321
+ );
1322
+ case "sdt":
1323
+ return Boolean(block.properties.checkbox) || block.children.some(blockNeedsW14Namespace);
1324
+ case "custom_xml":
1325
+ return block.children.some(blockNeedsW14Namespace);
1326
+ default:
1327
+ return false;
1328
+ }
1329
+ }
1330
+
1331
+ function inlineNodeNeedsW14Namespace(node: InlineNode): boolean {
1332
+ switch (node.type) {
1333
+ case "text":
1334
+ case "symbol":
1335
+ return Boolean(node.marks?.some((mark) => mark.type === "textFill"));
1336
+ case "hyperlink":
1337
+ case "field":
1338
+ return node.children.some(inlineNodeNeedsW14Namespace);
1339
+ default:
1340
+ return false;
1374
1341
  }
1375
- return false;
1376
1342
  }
@@ -65,17 +65,56 @@ function serializeAbstractDefinition(definition: NumberingCatalog["abstractDefin
65
65
  return `<w:abstractNum w:abstractNumId="${abstractNumId}">${levels}</w:abstractNum>`;
66
66
  }
67
67
 
68
- function serializeLevel(level: NumberingCatalog["abstractDefinitions"][string]["levels"][number]): string {
68
+ function serializeLevel(
69
+ level: NumberingCatalog["abstractDefinitions"][string]["levels"][number],
70
+ serializedLevel = level.level,
71
+ ): string {
69
72
  const start = level.startAt !== undefined ? `<w:start w:val="${level.startAt}"/>` : "";
70
73
  const paragraphStyle = level.paragraphStyleId
71
74
  ? `<w:pStyle w:val="${escapeAttribute(level.paragraphStyleId)}"/>`
72
75
  : "";
73
76
  const isLegal = level.isLegalNumbering ? "<w:isLgl/>" : "";
74
77
  const suffix = level.suffix ? `<w:suff w:val="${escapeAttribute(level.suffix)}"/>` : "";
78
+ const justification = level.paragraphGeometry?.justification
79
+ ? `<w:lvlJc w:val="${escapeAttribute(level.paragraphGeometry.justification)}"/>`
80
+ : "";
81
+ const paragraphProperties = serializeLevelParagraphGeometry(level.paragraphGeometry);
75
82
 
76
- return `<w:lvl w:ilvl="${level.level}">${start}<w:numFmt w:val="${escapeAttribute(
83
+ return `<w:lvl w:ilvl="${serializedLevel}">${start}<w:numFmt w:val="${escapeAttribute(
77
84
  level.format,
78
- )}"/><w:lvlText w:val="${escapeAttribute(level.text)}"/>${paragraphStyle}${isLegal}${suffix}</w:lvl>`;
85
+ )}"/><w:lvlText w:val="${escapeAttribute(level.text)}"/>${paragraphStyle}${isLegal}${suffix}${justification}${paragraphProperties}</w:lvl>`;
86
+ }
87
+
88
+ function serializeLevelOverride(
89
+ level: NumberingCatalog["instances"][string]["overrides"][number]["levelDefinition"],
90
+ serializedLevel: number,
91
+ ): string {
92
+ if (!level) {
93
+ return "";
94
+ }
95
+
96
+ const start = level.startAt !== undefined ? `<w:start w:val="${level.startAt}"/>` : "";
97
+ const format = level.format ? `<w:numFmt w:val="${escapeAttribute(level.format)}"/>` : "";
98
+ const text = level.text !== undefined
99
+ ? `<w:lvlText w:val="${escapeAttribute(level.text)}"/>`
100
+ : "";
101
+ const paragraphStyle = level.paragraphStyleId
102
+ ? `<w:pStyle w:val="${escapeAttribute(level.paragraphStyleId)}"/>`
103
+ : "";
104
+ const isLegal =
105
+ level.isLegalNumbering === undefined
106
+ ? ""
107
+ : level.isLegalNumbering
108
+ ? "<w:isLgl/>"
109
+ : `<w:isLgl w:val="0"/>`;
110
+ const suffix = level.suffix ? `<w:suff w:val="${escapeAttribute(level.suffix)}"/>` : "";
111
+ const justification = level.paragraphGeometry?.justification
112
+ ? `<w:lvlJc w:val="${escapeAttribute(level.paragraphGeometry.justification)}"/>`
113
+ : "";
114
+ const paragraphProperties = serializeLevelParagraphGeometry(level.paragraphGeometry);
115
+ const body = `${start}${format}${text}${paragraphStyle}${isLegal}${suffix}${justification}${paragraphProperties}`;
116
+
117
+ return body.length > 0 ? `<w:lvl w:ilvl="${serializedLevel}">${body}</w:lvl>` : "";
79
118
  }
80
119
 
81
120
  function serializeInstance(instance: NumberingCatalog["instances"][string]): string {
@@ -94,7 +133,68 @@ function serializeInstance(instance: NumberingCatalog["instances"][string]): str
94
133
  function serializeOverride(override: NumberingCatalog["instances"][string]["overrides"][number]): string {
95
134
  const startOverride =
96
135
  override.startAt !== undefined ? `<w:startOverride w:val="${override.startAt}"/>` : "";
97
- return `<w:lvlOverride w:ilvl="${override.level}">${startOverride}</w:lvlOverride>`;
136
+ const levelDefinition = override.levelDefinition
137
+ ? serializeLevelOverride(override.levelDefinition, override.level)
138
+ : "";
139
+ return `<w:lvlOverride w:ilvl="${override.level}">${startOverride}${levelDefinition}</w:lvlOverride>`;
140
+ }
141
+
142
+ function serializeLevelParagraphGeometry(
143
+ paragraphGeometry: NumberingCatalog["abstractDefinitions"][string]["levels"][number]["paragraphGeometry"],
144
+ ): string {
145
+ if (!paragraphGeometry) {
146
+ return "";
147
+ }
148
+
149
+ const children: string[] = [];
150
+ if (paragraphGeometry.spacing) {
151
+ const attrs: string[] = [];
152
+ if (paragraphGeometry.spacing.before !== undefined) {
153
+ attrs.push(`w:before="${paragraphGeometry.spacing.before}"`);
154
+ }
155
+ if (paragraphGeometry.spacing.after !== undefined) {
156
+ attrs.push(`w:after="${paragraphGeometry.spacing.after}"`);
157
+ }
158
+ if (paragraphGeometry.spacing.line !== undefined) {
159
+ attrs.push(`w:line="${paragraphGeometry.spacing.line}"`);
160
+ }
161
+ if (paragraphGeometry.spacing.lineRule !== undefined) {
162
+ attrs.push(`w:lineRule="${paragraphGeometry.spacing.lineRule}"`);
163
+ }
164
+ if (attrs.length > 0) {
165
+ children.push(`<w:spacing ${attrs.join(" ")}/>`);
166
+ }
167
+ }
168
+
169
+ if (paragraphGeometry.indentation) {
170
+ const attrs: string[] = [];
171
+ if (paragraphGeometry.indentation.left !== undefined) {
172
+ attrs.push(`w:left="${paragraphGeometry.indentation.left}"`);
173
+ }
174
+ if (paragraphGeometry.indentation.right !== undefined) {
175
+ attrs.push(`w:right="${paragraphGeometry.indentation.right}"`);
176
+ }
177
+ if (paragraphGeometry.indentation.firstLine !== undefined) {
178
+ attrs.push(`w:firstLine="${paragraphGeometry.indentation.firstLine}"`);
179
+ }
180
+ if (paragraphGeometry.indentation.hanging !== undefined) {
181
+ attrs.push(`w:hanging="${paragraphGeometry.indentation.hanging}"`);
182
+ }
183
+ if (attrs.length > 0) {
184
+ children.push(`<w:ind ${attrs.join(" ")}/>`);
185
+ }
186
+ }
187
+
188
+ if (paragraphGeometry.tabStops && paragraphGeometry.tabStops.length > 0) {
189
+ const tabsXml = paragraphGeometry.tabStops.map((tab) => {
190
+ const leader = tab.leader === "middleDot" ? "middledot" : tab.leader;
191
+ const leaderAttr = leader ? ` w:leader="${leader}"` : "";
192
+ return `<w:tab w:val="${tab.align}" w:pos="${tab.position}"${leaderAttr}/>`;
193
+ }).join("");
194
+ children.push(`<w:tabs>${tabsXml}</w:tabs>`);
195
+ }
196
+
197
+ return children.length > 0 ? `<w:pPr>${children.join("")}</w:pPr>` : "";
98
198
  }
99
199
 
100
200
  function compareSerializedIds(left: string, right: string): number {