@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.
Files changed (105) hide show
  1. package/README.md +8 -2
  2. package/package.json +24 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +710 -4
  5. package/src/api/session-state.ts +60 -0
  6. package/src/core/commands/formatting-commands.ts +2 -1
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +19 -3
  9. package/src/core/commands/list-commands.ts +231 -36
  10. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  11. package/src/core/commands/section-layout-commands.ts +680 -0
  12. package/src/core/commands/style-commands.ts +262 -0
  13. package/src/core/search/search-text.ts +357 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +4 -1
  16. package/src/index.ts +51 -0
  17. package/src/io/docx-session.ts +623 -56
  18. package/src/io/export/serialize-comments.ts +104 -34
  19. package/src/io/export/serialize-footnotes.ts +198 -1
  20. package/src/io/export/serialize-headers-footers.ts +203 -10
  21. package/src/io/export/serialize-main-document.ts +285 -8
  22. package/src/io/export/serialize-numbering.ts +28 -7
  23. package/src/io/export/split-review-boundaries.ts +181 -19
  24. package/src/io/normalize/normalize-text.ts +144 -32
  25. package/src/io/ooxml/highlight-colors.ts +39 -0
  26. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  27. package/src/io/ooxml/parse-comments.ts +85 -19
  28. package/src/io/ooxml/parse-fields.ts +396 -0
  29. package/src/io/ooxml/parse-footnotes.ts +452 -22
  30. package/src/io/ooxml/parse-headers-footers.ts +657 -29
  31. package/src/io/ooxml/parse-inline-media.ts +30 -0
  32. package/src/io/ooxml/parse-main-document.ts +807 -20
  33. package/src/io/ooxml/parse-numbering.ts +7 -0
  34. package/src/io/ooxml/parse-revisions.ts +317 -38
  35. package/src/io/ooxml/parse-settings.ts +184 -0
  36. package/src/io/ooxml/parse-shapes.ts +25 -0
  37. package/src/io/ooxml/parse-styles.ts +463 -0
  38. package/src/io/ooxml/parse-theme.ts +32 -0
  39. package/src/legal/bookmarks.ts +44 -0
  40. package/src/legal/cross-references.ts +59 -1
  41. package/src/model/canonical-document.ts +250 -4
  42. package/src/model/cds-1.0.0.ts +13 -0
  43. package/src/model/snapshot.ts +87 -2
  44. package/src/review/store/revision-store.ts +6 -0
  45. package/src/review/store/revision-types.ts +1 -0
  46. package/src/runtime/document-layout.ts +332 -0
  47. package/src/runtime/document-navigation.ts +603 -0
  48. package/src/runtime/document-runtime.ts +1754 -78
  49. package/src/runtime/document-search.ts +145 -0
  50. package/src/runtime/numbering-prefix.ts +47 -26
  51. package/src/runtime/page-layout-estimation.ts +212 -0
  52. package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
  53. package/src/runtime/session-capabilities.ts +35 -3
  54. package/src/runtime/story-context.ts +164 -0
  55. package/src/runtime/story-targeting.ts +162 -0
  56. package/src/runtime/surface-projection.ts +324 -36
  57. package/src/runtime/table-schema.ts +89 -7
  58. package/src/runtime/view-state.ts +477 -0
  59. package/src/runtime/workflow-markup.ts +349 -0
  60. package/src/ui/WordReviewEditor.tsx +2469 -1344
  61. package/src/ui/browser-export.ts +52 -0
  62. package/src/ui/editor-command-bag.ts +120 -0
  63. package/src/ui/editor-runtime-boundary.ts +1422 -0
  64. package/src/ui/editor-shell-view.tsx +134 -0
  65. package/src/ui/editor-surface-controller.tsx +51 -0
  66. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  67. package/src/ui/headless/revision-decoration-model.ts +4 -4
  68. package/src/ui/headless/selection-helpers.ts +20 -0
  69. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  70. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  71. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  72. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  73. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  74. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  75. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  76. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  77. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
  78. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  79. package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
  80. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
  81. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  82. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
  83. package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
  84. package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
  85. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
  86. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  87. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
  88. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  89. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  90. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
  91. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  92. package/src/ui-tailwind/index.ts +2 -1
  93. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  94. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  95. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  96. package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
  97. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  98. package/src/ui-tailwind/theme/editor-theme.css +127 -0
  99. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  100. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
  101. package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
  102. package/src/validation/compatibility-engine.ts +119 -24
  103. package/src/validation/compatibility-report.ts +1 -0
  104. package/src/validation/diagnostics.ts +1 -0
  105. package/src/validation/docx-comment-proof.ts +707 -0
@@ -577,56 +577,126 @@ function parseOoxmlNumericId(value: string): number | undefined {
577
577
  return Number.isFinite(numericId) ? numericId : undefined;
578
578
  }
579
579
 
580
- function mapParagraphBoundaries(documentXml: string): ParagraphBoundaryMap[] {
580
+ export function mapParagraphBoundaries(documentXml: string): ParagraphBoundaryMap[] {
581
581
  const root = parseXml(documentXml);
582
582
  const documentElement = findChildElement(root, "document");
583
583
  const bodyElement = findChildElement(documentElement, "body");
584
584
  const paragraphs: ParagraphBoundaryMap[] = [];
585
- let globalCursor = 0;
586
- let paragraphIndex = -1;
587
- let previousWasParagraph = false;
585
+ walkBlockNodesForParagraphBoundaries(
586
+ bodyElement.children,
587
+ documentXml,
588
+ paragraphs,
589
+ 0,
590
+ -1,
591
+ true,
592
+ );
593
+
594
+ return paragraphs;
595
+ }
596
+
597
+ function walkBlockNodesForParagraphBoundaries(
598
+ nodes: readonly XmlNode[],
599
+ documentXml: string,
600
+ paragraphs: ParagraphBoundaryMap[],
601
+ globalCursor: number,
602
+ paragraphIndex: number,
603
+ useSurfaceParagraphSeparators: boolean,
604
+ ): {
605
+ globalCursor: number;
606
+ paragraphIndex: number;
607
+ } {
608
+ let nextCursor = globalCursor;
609
+ let nextParagraphIndex = paragraphIndex;
610
+ let elementIndex = -1;
588
611
 
589
- for (const child of bodyElement.children) {
590
- if (child.type !== "element") {
612
+ for (const node of nodes) {
613
+ if (node.type !== "element") {
614
+ continue;
615
+ }
616
+ elementIndex += 1;
617
+
618
+ const name = localName(node.name);
619
+ if (name === "p") {
620
+ if (useSurfaceParagraphSeparators && elementIndex > 0) {
621
+ nextCursor += 1;
622
+ }
623
+ nextParagraphIndex += 1;
624
+ const boundaries = new Map<number, number>();
625
+ boundaries.set(nextCursor, node.start + openingTagLength(documentXml, node.start));
626
+
627
+ walkParagraphForBoundaries(
628
+ node,
629
+ documentXml,
630
+ boundaries,
631
+ () => nextCursor,
632
+ (next) => {
633
+ nextCursor = next;
634
+ },
635
+ );
636
+
637
+ if (!boundaries.has(nextCursor)) {
638
+ boundaries.set(nextCursor, node.end - 4);
639
+ }
640
+ paragraphs.push({
641
+ paragraphIndex: nextParagraphIndex,
642
+ start: Math.min(...boundaries.keys()),
643
+ end: Math.max(...boundaries.keys()),
644
+ boundaries,
645
+ });
591
646
  continue;
592
647
  }
593
648
 
594
- if (localName(child.name) !== "p") {
595
- globalCursor += 1;
596
- previousWasParagraph = false;
649
+ if (name === "tbl") {
650
+ for (const child of node.children) {
651
+ if (child.type !== "element" || localName(child.name) !== "tr") {
652
+ continue;
653
+ }
654
+ for (const rowChild of child.children) {
655
+ if (rowChild.type !== "element" || localName(rowChild.name) !== "tc") {
656
+ continue;
657
+ }
658
+ const result = walkBlockNodesForParagraphBoundaries(
659
+ rowChild.children,
660
+ documentXml,
661
+ paragraphs,
662
+ nextCursor,
663
+ nextParagraphIndex,
664
+ false,
665
+ );
666
+ nextCursor = result.globalCursor;
667
+ nextParagraphIndex = result.paragraphIndex;
668
+ }
669
+ }
597
670
  continue;
598
671
  }
599
672
 
600
- if (previousWasParagraph) {
601
- globalCursor += 1;
673
+ if (name === "sdt") {
674
+ const sdtContent = findChildElement(node, "sdtContent");
675
+ const result = walkBlockNodesForParagraphBoundaries(
676
+ sdtContent.children,
677
+ documentXml,
678
+ paragraphs,
679
+ nextCursor,
680
+ nextParagraphIndex,
681
+ false,
682
+ );
683
+ nextCursor = result.globalCursor;
684
+ nextParagraphIndex = result.paragraphIndex;
685
+ continue;
602
686
  }
603
- paragraphIndex += 1;
604
- const boundaries = new Map<number, number>();
605
- boundaries.set(globalCursor, child.start + openingTagLength(documentXml, child.start));
606
-
607
- walkParagraphForBoundaries(
608
- child,
609
- documentXml,
610
- boundaries,
611
- () => globalCursor,
612
- (next) => {
613
- globalCursor = next;
614
- },
615
- );
616
687
 
617
- if (!boundaries.has(globalCursor)) {
618
- boundaries.set(globalCursor, child.end - 4);
688
+ if (name === "customXml") {
689
+ nextCursor += 1;
690
+ continue;
619
691
  }
620
- paragraphs.push({
621
- paragraphIndex,
622
- start: Math.min(...boundaries.keys()),
623
- end: Math.max(...boundaries.keys()),
624
- boundaries,
625
- });
626
- previousWasParagraph = true;
692
+
693
+ nextCursor += 0;
627
694
  }
628
695
 
629
- return paragraphs;
696
+ return {
697
+ globalCursor: nextCursor,
698
+ paragraphIndex: nextParagraphIndex,
699
+ };
630
700
  }
631
701
 
632
702
  function walkParagraphForBoundaries(
@@ -3,6 +3,9 @@ import type {
3
3
  FootnoteDefinition,
4
4
  InlineNode,
5
5
  ParagraphNode,
6
+ TableCellNode,
7
+ TableNode,
8
+ TableRowNode,
6
9
  TextMark,
7
10
  } from "../../model/canonical-document.ts";
8
11
 
@@ -71,6 +74,12 @@ function serializeNoteDefinition(
71
74
  if (block.type === "paragraph") {
72
75
  return serializeParagraph(block);
73
76
  }
77
+ if (block.type === "table") {
78
+ return serializeTable(block);
79
+ }
80
+ if (block.type === "opaque_block" && typeof block.rawXml === "string") {
81
+ return block.rawXml;
82
+ }
74
83
  throw new Error(`Cannot safely serialize ${block.type} content in note sub-parts.`);
75
84
  })
76
85
  .join("");
@@ -96,6 +105,54 @@ function serializeParagraph(paragraph: ParagraphNode): string {
96
105
  return xml;
97
106
  }
98
107
 
108
+ function serializeTable(table: TableNode): string {
109
+ let xml = "<w:tbl>";
110
+ if (table.propertiesXml) {
111
+ xml += table.propertiesXml;
112
+ }
113
+ if (table.gridColumns.length > 0) {
114
+ xml += "<w:tblGrid>";
115
+ for (const width of table.gridColumns) {
116
+ xml += `<w:gridCol w:w="${width}"/>`;
117
+ }
118
+ xml += "</w:tblGrid>";
119
+ }
120
+ for (const row of table.rows) {
121
+ xml += serializeTableRow(row);
122
+ }
123
+ xml += "</w:tbl>";
124
+ return xml;
125
+ }
126
+
127
+ function serializeTableRow(row: TableRowNode): string {
128
+ let xml = "<w:tr>";
129
+ if (row.propertiesXml) {
130
+ xml += wrapPropertiesXml("w:trPr", row.propertiesXml);
131
+ }
132
+ for (const cell of row.cells) {
133
+ xml += serializeTableCell(cell);
134
+ }
135
+ xml += "</w:tr>";
136
+ return xml;
137
+ }
138
+
139
+ function serializeTableCell(cell: TableCellNode): string {
140
+ let xml = "<w:tc>";
141
+ const propertiesXml = buildTableCellPropertiesXml(cell);
142
+ if (propertiesXml) {
143
+ xml += propertiesXml;
144
+ }
145
+ for (const child of cell.children) {
146
+ if (child.type === "paragraph") {
147
+ xml += serializeParagraph(child);
148
+ } else {
149
+ throw new Error(`Cannot safely serialize ${child.type} content in note table cells.`);
150
+ }
151
+ }
152
+ xml += "</w:tc>";
153
+ return xml;
154
+ }
155
+
99
156
  function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
100
157
  const parts: string[] = [];
101
158
 
@@ -105,6 +162,26 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
105
162
  if (paragraph.alignment) {
106
163
  parts.push(`<w:jc w:val="${escapeAttribute(paragraph.alignment)}"/>`);
107
164
  }
165
+ if (paragraph.spacing) {
166
+ const attrs: string[] = [];
167
+ if (paragraph.spacing.before !== undefined) attrs.push(`w:before="${paragraph.spacing.before}"`);
168
+ if (paragraph.spacing.after !== undefined) attrs.push(`w:after="${paragraph.spacing.after}"`);
169
+ if (paragraph.spacing.line !== undefined) attrs.push(`w:line="${paragraph.spacing.line}"`);
170
+ if (paragraph.spacing.lineRule) attrs.push(`w:lineRule="${escapeAttribute(paragraph.spacing.lineRule)}"`);
171
+ if (attrs.length > 0) {
172
+ parts.push(`<w:spacing ${attrs.join(" ")}/>`);
173
+ }
174
+ }
175
+ if (paragraph.indentation) {
176
+ const attrs: string[] = [];
177
+ if (paragraph.indentation.left !== undefined) attrs.push(`w:left="${paragraph.indentation.left}"`);
178
+ if (paragraph.indentation.right !== undefined) attrs.push(`w:right="${paragraph.indentation.right}"`);
179
+ if (paragraph.indentation.firstLine !== undefined) attrs.push(`w:firstLine="${paragraph.indentation.firstLine}"`);
180
+ if (paragraph.indentation.hanging !== undefined) attrs.push(`w:hanging="${paragraph.indentation.hanging}"`);
181
+ if (attrs.length > 0) {
182
+ parts.push(`<w:ind ${attrs.join(" ")}/>`);
183
+ }
184
+ }
108
185
 
109
186
  return parts.length > 0 ? `<w:pPr>${parts.join("")}</w:pPr>` : "";
110
187
  }
@@ -133,9 +210,62 @@ function serializeInlineNode(node: InlineNode): string {
133
210
  : "EndnoteReference";
134
211
  return `<w:r><w:rPr><w:rStyle w:val="${styleVal}"/></w:rPr>${refElement}</w:r>`;
135
212
  }
213
+ case "bookmark_start":
214
+ return `<w:bookmarkStart w:id="${escapeAttribute(node.bookmarkId)}" w:name="${escapeAttribute(node.name)}"/>`;
215
+ case "bookmark_end":
216
+ return `<w:bookmarkEnd w:id="${escapeAttribute(node.bookmarkId)}"/>`;
217
+ case "field":
218
+ if (node.children && node.children.length > 0) {
219
+ const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
220
+ if (node.fieldType === "complex") {
221
+ return (
222
+ `<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
223
+ `<w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r>` +
224
+ `<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
225
+ childrenXml +
226
+ `<w:r><w:fldChar w:fldCharType="end"/></w:r>`
227
+ );
228
+ }
229
+ return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
230
+ }
231
+ if (node.fieldType === "simple") {
232
+ return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
233
+ }
234
+ return (
235
+ `<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
236
+ `<w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r>` +
237
+ `<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
238
+ `<w:r><w:fldChar w:fldCharType="end"/></w:r>`
239
+ );
240
+ case "hyperlink":
241
+ return serializeHyperlinkNode(node);
242
+ case "field":
243
+ if (node.children && node.children.length > 0) {
244
+ const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
245
+ if (node.fieldType === "complex") {
246
+ return (
247
+ `<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
248
+ `<w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r>` +
249
+ `<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
250
+ childrenXml +
251
+ `<w:r><w:fldChar w:fldCharType="end"/></w:r>`
252
+ );
253
+ }
254
+ return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
255
+ }
256
+ if (node.fieldType === "simple") {
257
+ return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
258
+ }
259
+ return (
260
+ `<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
261
+ `<w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r>` +
262
+ `<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
263
+ `<w:r><w:fldChar w:fldCharType="end"/></w:r>`
264
+ );
265
+ case "hyperlink":
266
+ return serializeHyperlinkNode(node);
136
267
  case "opaque_inline":
137
268
  throw new Error(`Cannot safely serialize ${node.type} content in note sub-parts.`);
138
- case "hyperlink":
139
269
  default:
140
270
  throw new Error(`Cannot safely serialize ${node.type} content in note sub-parts.`);
141
271
  }
@@ -164,6 +294,23 @@ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
164
294
  case "doubleStrikethrough":
165
295
  parts.push("<w:dstrike/>");
166
296
  break;
297
+ case "fontFamily":
298
+ parts.push(
299
+ `<w:rFonts w:ascii="${escapeAttribute(mark.val)}" w:hAnsi="${escapeAttribute(mark.val)}"/>`,
300
+ );
301
+ break;
302
+ case "fontSize":
303
+ parts.push(`<w:sz w:val="${mark.val}"/>`);
304
+ break;
305
+ case "textColor":
306
+ parts.push(`<w:color w:val="${escapeAttribute(mark.color)}"/>`);
307
+ break;
308
+ case "smallCaps":
309
+ parts.push("<w:smallCaps/>");
310
+ break;
311
+ case "allCaps":
312
+ parts.push("<w:caps/>");
313
+ break;
167
314
  default:
168
315
  throw new Error(`Cannot safely serialize ${mark.type} marks in note sub-parts.`);
169
316
  }
@@ -200,3 +347,53 @@ function escapeAttribute(value: string): string {
200
347
  .replace(/>/g, "&gt;")
201
348
  .replace(/"/g, "&quot;");
202
349
  }
350
+
351
+ function serializeHyperlinkNode(node: Extract<InlineNode, { type: "hyperlink" }>): string {
352
+ const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
353
+ if (node.href.startsWith("#")) {
354
+ return `<w:hyperlink w:anchor="${escapeAttribute(node.href.slice(1))}">${childrenXml}</w:hyperlink>`;
355
+ }
356
+ if (!/^rId[A-Za-z0-9._-]+$/u.test(node.href)) {
357
+ throw new Error("Cannot safely serialize URL-backed note hyperlinks without relationship context.");
358
+ }
359
+ return `<w:hyperlink r:id="${escapeAttribute(node.href)}">${childrenXml}</w:hyperlink>`;
360
+ }
361
+
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
+ 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>` : "";
399
+ }