@beyondwork/docx-react-component 1.0.41 → 1.0.43

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 (118) hide show
  1. package/package.json +38 -37
  2. package/src/api/awareness-identity-types.ts +35 -0
  3. package/src/api/comment-negotiation-types.ts +130 -0
  4. package/src/api/comment-presentation-types.ts +106 -0
  5. package/src/api/editor-state-types.ts +110 -0
  6. package/src/api/external-custody-types.ts +74 -0
  7. package/src/api/participants-types.ts +18 -0
  8. package/src/api/public-types.ts +541 -5
  9. package/src/api/scope-metadata-resolver-types.ts +88 -0
  10. package/src/core/commands/formatting-commands.ts +1 -1
  11. package/src/core/commands/index.ts +601 -9
  12. package/src/core/search/search-text.ts +15 -2
  13. package/src/index.ts +131 -1
  14. package/src/io/docx-session.ts +672 -2
  15. package/src/io/export/escape-xml-attribute.ts +26 -0
  16. package/src/io/export/external-send.ts +188 -0
  17. package/src/io/export/serialize-comments.ts +13 -16
  18. package/src/io/export/serialize-footnotes.ts +17 -24
  19. package/src/io/export/serialize-headers-footers.ts +17 -24
  20. package/src/io/export/serialize-main-document.ts +59 -62
  21. package/src/io/export/serialize-numbering.ts +20 -27
  22. package/src/io/export/serialize-runtime-revisions.ts +2 -9
  23. package/src/io/export/serialize-tables.ts +8 -15
  24. package/src/io/export/table-properties-xml.ts +25 -32
  25. package/src/io/import/external-reimport.ts +40 -0
  26. package/src/io/load-scheduler.ts +230 -0
  27. package/src/io/normalize/normalize-text.ts +83 -0
  28. package/src/io/ooxml/bw-xml.ts +244 -0
  29. package/src/io/ooxml/canonicalize-payload.ts +301 -0
  30. package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
  31. package/src/io/ooxml/comment-presentation-payload.ts +311 -0
  32. package/src/io/ooxml/external-custody-payload.ts +102 -0
  33. package/src/io/ooxml/participants-payload.ts +97 -0
  34. package/src/io/ooxml/payload-signature.ts +112 -0
  35. package/src/io/ooxml/workflow-payload-validator.ts +367 -0
  36. package/src/io/ooxml/workflow-payload.ts +317 -7
  37. package/src/runtime/awareness-identity.ts +173 -0
  38. package/src/runtime/collab/event-types.ts +27 -0
  39. package/src/runtime/collab-session-bridge.ts +157 -0
  40. package/src/runtime/collab-session-facet.ts +193 -0
  41. package/src/runtime/collab-session.ts +273 -0
  42. package/src/runtime/comment-negotiation-sync.ts +91 -0
  43. package/src/runtime/comment-negotiation.ts +158 -0
  44. package/src/runtime/comment-presentation.ts +223 -0
  45. package/src/runtime/document-runtime.ts +639 -124
  46. package/src/runtime/editor-state-channel.ts +544 -0
  47. package/src/runtime/editor-state-integration.ts +217 -0
  48. package/src/runtime/external-send-runtime.ts +117 -0
  49. package/src/runtime/layout/docx-font-loader.ts +11 -30
  50. package/src/runtime/layout/index.ts +2 -0
  51. package/src/runtime/layout/inert-layout-facet.ts +4 -0
  52. package/src/runtime/layout/layout-engine-instance.ts +139 -14
  53. package/src/runtime/layout/page-graph.ts +79 -7
  54. package/src/runtime/layout/paginated-layout-engine.ts +441 -48
  55. package/src/runtime/layout/public-facet.ts +585 -14
  56. package/src/runtime/layout/table-row-split.ts +316 -0
  57. package/src/runtime/markdown-sanitizer.ts +132 -0
  58. package/src/runtime/participants.ts +134 -0
  59. package/src/runtime/perf-counters.ts +28 -0
  60. package/src/runtime/render/render-frame-types.ts +17 -0
  61. package/src/runtime/render/render-kernel.ts +172 -29
  62. package/src/runtime/resign-payload.ts +120 -0
  63. package/src/runtime/surface-projection.ts +10 -5
  64. package/src/runtime/tamper-gate.ts +157 -0
  65. package/src/runtime/workflow-markup.ts +80 -16
  66. package/src/runtime/workflow-rail-segments.ts +244 -5
  67. package/src/ui/WordReviewEditor.tsx +654 -45
  68. package/src/ui/editor-command-bag.ts +14 -0
  69. package/src/ui/editor-runtime-boundary.ts +111 -11
  70. package/src/ui/editor-shell-view.tsx +21 -0
  71. package/src/ui/editor-surface-controller.tsx +5 -0
  72. package/src/ui/headless/selection-helpers.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
  74. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
  75. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
  76. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
  77. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
  78. package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
  79. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
  80. package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
  81. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
  82. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
  83. package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
  84. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
  85. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
  86. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
  87. package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
  88. package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
  89. package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
  90. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
  91. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
  92. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
  93. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
  94. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
  95. package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
  96. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
  97. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
  98. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
  99. package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
  100. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
  101. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
  102. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
  103. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +10 -256
  104. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
  105. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
  106. package/src/ui-tailwind/index.ts +37 -1
  107. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
  108. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
  109. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
  110. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
  111. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
  112. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
  113. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
  114. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
  115. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
  116. package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
  117. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  118. package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
@@ -24,6 +24,7 @@ import {
24
24
  serializeTableRowPropertiesXml,
25
25
  } from "./table-properties-xml.ts";
26
26
  import { twip } from "./twip.ts";
27
+ import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
27
28
 
28
29
  const HYPERLINK_RELATIONSHIP_TYPE =
29
30
  "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
@@ -338,10 +339,10 @@ function serializeCustomXmlNode(
338
339
  }
339
340
  const attrs: string[] = [];
340
341
  if (block.uri) {
341
- attrs.push(`w:uri="${escapeAttribute(block.uri)}"`);
342
+ attrs.push(`w:uri="${escapeXmlAttribute(block.uri)}"`);
342
343
  }
343
344
  if (block.element) {
344
- attrs.push(`w:element="${escapeAttribute(block.element)}"`);
345
+ attrs.push(`w:element="${escapeXmlAttribute(block.element)}"`);
345
346
  }
346
347
  const attrXml = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
347
348
  const childrenXml = block.children.map((child) => serializeBlockNode(child, state)).join("");
@@ -351,19 +352,19 @@ function serializeCustomXmlNode(
351
352
  function serializeAltChunkNode(
352
353
  block: AltChunkNode,
353
354
  ): string {
354
- return `<w:altChunk r:id="${escapeAttribute(block.relationshipId)}"/>`;
355
+ return `<w:altChunk r:id="${escapeXmlAttribute(block.relationshipId)}"/>`;
355
356
  }
356
357
 
357
358
  function buildSdtPropertiesXml(block: SdtNode): string {
358
359
  const children: string[] = [];
359
360
  if (block.properties.alias) {
360
- children.push(`<w:alias w:val="${escapeAttribute(block.properties.alias)}"/>`);
361
+ children.push(`<w:alias w:val="${escapeXmlAttribute(block.properties.alias)}"/>`);
361
362
  }
362
363
  if (block.properties.tag) {
363
- children.push(`<w:tag w:val="${escapeAttribute(block.properties.tag)}"/>`);
364
+ children.push(`<w:tag w:val="${escapeXmlAttribute(block.properties.tag)}"/>`);
364
365
  }
365
366
  if (block.properties.lock) {
366
- children.push(`<w:lock w:val="${escapeAttribute(block.properties.lock)}"/>`);
367
+ children.push(`<w:lock w:val="${escapeXmlAttribute(block.properties.lock)}"/>`);
367
368
  }
368
369
  if (block.properties.showingPlcHdr) {
369
370
  children.push(`<w:showingPlcHdr/>`);
@@ -374,26 +375,26 @@ function buildSdtPropertiesXml(block: SdtNode): string {
374
375
  // ST_OnOff (A.3): w14:val is typed as ST_OnOff; emit "true"/"false"
375
376
  // instead of "1"/"0" to satisfy DocumentFormat.OpenXml validation.
376
377
  cbParts.push(`<w14:checked w14:val="${cb.checked ? "true" : "false"}"/>`);
377
- if (cb.checkedChar) cbParts.push(`<w14:checkedState w14:val="${escapeAttribute(cb.checkedChar)}"/>`);
378
- if (cb.uncheckedChar) cbParts.push(`<w14:uncheckedState w14:val="${escapeAttribute(cb.uncheckedChar)}"/>`);
378
+ if (cb.checkedChar) cbParts.push(`<w14:checkedState w14:val="${escapeXmlAttribute(cb.checkedChar)}"/>`);
379
+ if (cb.uncheckedChar) cbParts.push(`<w14:uncheckedState w14:val="${escapeXmlAttribute(cb.uncheckedChar)}"/>`);
379
380
  children.push(`<w14:checkbox>${cbParts.join("")}</w14:checkbox>`);
380
381
  } else if (block.properties.datePicker) {
381
382
  const dp = block.properties.datePicker;
382
- const dateAttrs = dp.fullDate ? ` w:fullDate="${escapeAttribute(dp.fullDate)}"` : "";
383
+ const dateAttrs = dp.fullDate ? ` w:fullDate="${escapeXmlAttribute(dp.fullDate)}"` : "";
383
384
  const dpParts: string[] = [];
384
- if (dp.dateFormat) dpParts.push(`<w:dateFormat w:val="${escapeAttribute(dp.dateFormat)}"/>`);
385
- if (dp.lid) dpParts.push(`<w:lid w:val="${escapeAttribute(dp.lid)}"/>`);
385
+ if (dp.dateFormat) dpParts.push(`<w:dateFormat w:val="${escapeXmlAttribute(dp.dateFormat)}"/>`);
386
+ if (dp.lid) dpParts.push(`<w:lid w:val="${escapeXmlAttribute(dp.lid)}"/>`);
386
387
  children.push(dpParts.length > 0 ? `<w:date${dateAttrs}>${dpParts.join("")}</w:date>` : `<w:date${dateAttrs}/>`);
387
388
  } else if (block.properties.dropdownList) {
388
389
  const items = block.properties.dropdownList.map((item) => {
389
- const dt = item.displayText ? ` w:displayText="${escapeAttribute(item.displayText)}"` : "";
390
- return `<w:listItem${dt} w:value="${escapeAttribute(item.value)}"/>`;
390
+ const dt = item.displayText ? ` w:displayText="${escapeXmlAttribute(item.displayText)}"` : "";
391
+ return `<w:listItem${dt} w:value="${escapeXmlAttribute(item.value)}"/>`;
391
392
  }).join("");
392
393
  children.push(`<w:dropDownList>${items}</w:dropDownList>`);
393
394
  } else if (block.properties.comboBox) {
394
395
  const items = block.properties.comboBox.map((item) => {
395
- const dt = item.displayText ? ` w:displayText="${escapeAttribute(item.displayText)}"` : "";
396
- return `<w:listItem${dt} w:value="${escapeAttribute(item.value)}"/>`;
396
+ const dt = item.displayText ? ` w:displayText="${escapeXmlAttribute(item.displayText)}"` : "";
397
+ return `<w:listItem${dt} w:value="${escapeXmlAttribute(item.value)}"/>`;
397
398
  }).join("");
398
399
  children.push(`<w:comboBox>${items}</w:comboBox>`);
399
400
  } else if (block.properties.sdtType === "plainText") {
@@ -514,8 +515,8 @@ function serializeTableInlineNode(
514
515
  return "<w:r><w:br/></w:r>";
515
516
  case "symbol": {
516
517
  const properties = serializeRunPropertiesFromMarks(node.marks);
517
- const fontAttribute = node.font ? ` w:font="${escapeAttribute(node.font)}"` : "";
518
- return `<w:r>${properties}<w:sym${fontAttribute} w:char="${escapeAttribute(node.char)}"/></w:r>`;
518
+ const fontAttribute = node.font ? ` w:font="${escapeXmlAttribute(node.font)}"` : "";
519
+ return `<w:r>${properties}<w:sym${fontAttribute} w:char="${escapeXmlAttribute(node.char)}"/></w:r>`;
519
520
  }
520
521
  case "image":
521
522
  return serializeImageNode(node, state);
@@ -529,7 +530,7 @@ function serializeTableInlineNode(
529
530
  return wrapInlineRawXml(node.rawXml);
530
531
  case "hyperlink": {
531
532
  const hyperlinkOpen = node.href.startsWith("#")
532
- ? `<w:hyperlink w:anchor="${escapeAttribute(node.href.slice(1))}">`
533
+ ? `<w:hyperlink w:anchor="${escapeXmlAttribute(node.href.slice(1))}">`
533
534
  : (() => {
534
535
  const relationshipId = `rIdHyperlink${state.nextHyperlinkRelationshipIndex}`;
535
536
  state.nextHyperlinkRelationshipIndex += 1;
@@ -559,22 +560,22 @@ function serializeTableInlineNode(
559
560
  `<w:r><w:fldChar w:fldCharType="end"/></w:r>`
560
561
  );
561
562
  }
562
- return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
563
+ return `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
563
564
  }
564
- return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
565
+ return `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}"/>`;
565
566
  }
566
567
  case "bookmark_start":
567
568
  return (
568
- `<w:bookmarkStart w:id="${escapeAttribute(node.bookmarkId)}"` +
569
- ` w:name="${escapeAttribute(node.name)}"/>`
569
+ `<w:bookmarkStart w:id="${escapeXmlAttribute(node.bookmarkId)}"` +
570
+ ` w:name="${escapeXmlAttribute(node.name)}"/>`
570
571
  );
571
572
  case "bookmark_end":
572
- return `<w:bookmarkEnd w:id="${escapeAttribute(node.bookmarkId)}"/>`;
573
+ return `<w:bookmarkEnd w:id="${escapeXmlAttribute(node.bookmarkId)}"/>`;
573
574
  case "footnote_ref": {
574
575
  const refElement =
575
576
  node.noteKind === "footnote"
576
- ? `<w:footnoteReference w:id="${escapeAttribute(node.noteId)}"/>`
577
- : `<w:endnoteReference w:id="${escapeAttribute(node.noteId)}"/>`;
577
+ ? `<w:footnoteReference w:id="${escapeXmlAttribute(node.noteId)}"/>`
578
+ : `<w:endnoteReference w:id="${escapeXmlAttribute(node.noteId)}"/>`;
578
579
  const styleVal =
579
580
  node.noteKind === "footnote"
580
581
  ? "FootnoteReference"
@@ -590,7 +591,7 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
590
591
  const children: string[] = [];
591
592
 
592
593
  if (paragraph.styleId) {
593
- children.push(`<w:pStyle w:val="${escapeAttribute(paragraph.styleId)}"/>`);
594
+ children.push(`<w:pStyle w:val="${escapeXmlAttribute(paragraph.styleId)}"/>`);
594
595
  }
595
596
  if (paragraph.keepNext) {
596
597
  children.push("<w:keepNext/>");
@@ -662,7 +663,7 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
662
663
  children.push("<w:suppressLineNumbers/>");
663
664
  }
664
665
  if (paragraph.cnfStyle) {
665
- children.push(`<w:cnfStyle w:val="${escapeAttribute(paragraph.cnfStyle)}"/>`);
666
+ children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(paragraph.cnfStyle)}"/>`);
666
667
  }
667
668
  if (paragraph.tabStops && paragraph.tabStops.length > 0) {
668
669
  const tabsXml = paragraph.tabStops.map((tab) => {
@@ -706,10 +707,10 @@ function serializeBorder(name: string, border: BorderSpec | undefined): string {
706
707
  return "";
707
708
  }
708
709
  const attrs: string[] = [];
709
- if (border.value) attrs.push(`w:val="${escapeAttribute(border.value)}"`);
710
+ if (border.value) attrs.push(`w:val="${escapeXmlAttribute(border.value)}"`);
710
711
  if (border.size !== undefined) attrs.push(`w:sz="${twip(border.size)}"`);
711
712
  if (border.space !== undefined) attrs.push(`w:space="${twip(border.space)}"`);
712
- if (border.color) attrs.push(`w:color="${escapeAttribute(border.color)}"`);
713
+ if (border.color) attrs.push(`w:color="${escapeXmlAttribute(border.color)}"`);
713
714
  return attrs.length > 0 ? `<w:${name} ${attrs.join(" ")}/>` : "";
714
715
  }
715
716
 
@@ -718,13 +719,13 @@ function serializeParagraphShading(shading: ParagraphNode["shading"]): string {
718
719
  return "";
719
720
  }
720
721
  const attrs: string[] = [];
721
- if (shading.val) attrs.push(`w:val="${escapeAttribute(shading.val)}"`);
722
- if (shading.color) attrs.push(`w:color="${escapeAttribute(shading.color)}"`);
722
+ if (shading.val) attrs.push(`w:val="${escapeXmlAttribute(shading.val)}"`);
723
+ if (shading.color) attrs.push(`w:color="${escapeXmlAttribute(shading.color)}"`);
723
724
  // A.9: w:shd w:val="clear" must always carry w:fill. Emit w:fill="auto"
724
725
  // when the canonical model has no explicit fill; otherwise the SDK
725
726
  // flags the shading as incomplete.
726
727
  if (shading.fill !== undefined) {
727
- attrs.push(`w:fill="${escapeAttribute(shading.fill)}"`);
728
+ attrs.push(`w:fill="${escapeXmlAttribute(shading.fill)}"`);
728
729
  } else if (shading.val === "clear") {
729
730
  attrs.push(`w:fill="auto"`);
730
731
  }
@@ -946,7 +947,7 @@ function serializeInlineNode(
946
947
  }
947
948
  case "hyperlink": {
948
949
  const hyperlinkOpen = node.href.startsWith("#")
949
- ? `<w:hyperlink w:anchor="${escapeAttribute(node.href.slice(1))}">`
950
+ ? `<w:hyperlink w:anchor="${escapeXmlAttribute(node.href.slice(1))}">`
950
951
  : (() => {
951
952
  const relationshipId = `rIdHyperlink${state.nextHyperlinkRelationshipIndex}`;
952
953
  state.nextHyperlinkRelationshipIndex += 1;
@@ -1015,7 +1016,7 @@ function serializeInlineNode(
1015
1016
  }
1016
1017
 
1017
1018
  // Simple field with children
1018
- const openXml = `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}">`;
1019
+ const openXml = `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}">`;
1019
1020
  nextOffset += openXml.length;
1020
1021
  const children: string[] = [openXml];
1021
1022
  for (const child of node.children) {
@@ -1033,7 +1034,7 @@ function serializeInlineNode(
1033
1034
  return { xml: children.join(""), cursor: nextCursor, boundaries };
1034
1035
  }
1035
1036
 
1036
- const xml = `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
1037
+ const xml = `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}"/>`;
1037
1038
  const boundaries = new Map<number, number>();
1038
1039
  boundaries.set(cursor, xmlOffset);
1039
1040
  boundaries.set(cursor + 1, xmlOffset + xml.length);
@@ -1045,15 +1046,15 @@ function serializeInlineNode(
1045
1046
  }
1046
1047
  case "bookmark_start": {
1047
1048
  const xml =
1048
- `<w:bookmarkStart w:id="${escapeAttribute(node.bookmarkId)}"` +
1049
- ` w:name="${escapeAttribute(node.name)}"/>`;
1049
+ `<w:bookmarkStart w:id="${escapeXmlAttribute(node.bookmarkId)}"` +
1050
+ ` w:name="${escapeXmlAttribute(node.name)}"/>`;
1050
1051
  const boundaries = new Map<number, number>();
1051
1052
  boundaries.set(cursor, xmlOffset);
1052
1053
  boundaries.set(cursor, xmlOffset + xml.length);
1053
1054
  return { xml, cursor, boundaries };
1054
1055
  }
1055
1056
  case "bookmark_end": {
1056
- const xml = `<w:bookmarkEnd w:id="${escapeAttribute(node.bookmarkId)}"/>`;
1057
+ const xml = `<w:bookmarkEnd w:id="${escapeXmlAttribute(node.bookmarkId)}"/>`;
1057
1058
  const boundaries = new Map<number, number>();
1058
1059
  boundaries.set(cursor, xmlOffset);
1059
1060
  boundaries.set(cursor, xmlOffset + xml.length);
@@ -1062,8 +1063,8 @@ function serializeInlineNode(
1062
1063
  case "footnote_ref": {
1063
1064
  const refElement =
1064
1065
  node.noteKind === "footnote"
1065
- ? `<w:footnoteReference w:id="${escapeAttribute(node.noteId)}"/>`
1066
- : `<w:endnoteReference w:id="${escapeAttribute(node.noteId)}"/>`;
1066
+ ? `<w:footnoteReference w:id="${escapeXmlAttribute(node.noteId)}"/>`
1067
+ : `<w:endnoteReference w:id="${escapeXmlAttribute(node.noteId)}"/>`;
1067
1068
  const styleVal =
1068
1069
  node.noteKind === "footnote"
1069
1070
  ? "FootnoteReference"
@@ -1098,7 +1099,7 @@ function serializeImageNode(
1098
1099
  const mediaItem = state.media.items[node.mediaId];
1099
1100
  if (mediaItem?.relationshipId && state.existingRelationshipMap.has(mediaItem.relationshipId)) {
1100
1101
  const altText = node.altText ?? mediaItem.altText ?? mediaItem.filename;
1101
- return `<w:r><w:drawing><wp:inline xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="9525" cy="9525"/><wp:docPr id="1" name="${escapeAttribute(mediaItem.filename)}" descr="${escapeAttribute(altText ?? "")}"/><wp:cNvGraphicFramePr/><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="0" name="${escapeAttribute(mediaItem.filename)}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${escapeAttribute(mediaItem.relationshipId)}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="9525" cy="9525"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
1102
+ return `<w:r><w:drawing><wp:inline xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="9525" cy="9525"/><wp:docPr id="1" name="${escapeXmlAttribute(mediaItem.filename)}" descr="${escapeXmlAttribute(altText ?? "")}"/><wp:cNvGraphicFramePr/><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="0" name="${escapeXmlAttribute(mediaItem.filename)}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${escapeXmlAttribute(mediaItem.relationshipId)}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="9525" cy="9525"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
1102
1103
  }
1103
1104
 
1104
1105
  return serializeRun({
@@ -1141,14 +1142,14 @@ function serializeRunProperties(marks: TextMark[] | undefined): string {
1141
1142
  markParts.push("<w:vanish/>");
1142
1143
  break;
1143
1144
  case "lang":
1144
- markParts.push(`<w:lang w:val="${escapeAttribute(mark.val)}"/>`);
1145
+ markParts.push(`<w:lang w:val="${escapeXmlAttribute(mark.val)}"/>`);
1145
1146
  break;
1146
1147
  case "highlight":
1147
- markParts.push(`<w:highlight w:val="${escapeAttribute(mark.val)}"/>`);
1148
+ markParts.push(`<w:highlight w:val="${escapeXmlAttribute(mark.val)}"/>`);
1148
1149
  break;
1149
1150
  case "backgroundColor":
1150
1151
  markParts.push(
1151
- `<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttribute(mark.color)}"/>`,
1152
+ `<w:shd w:val="clear" w:color="auto" w:fill="${escapeXmlAttribute(mark.color)}"/>`,
1152
1153
  );
1153
1154
  break;
1154
1155
  case "charSpacing":
@@ -1173,13 +1174,13 @@ function serializeRunProperties(marks: TextMark[] | undefined): string {
1173
1174
  markParts.push(mark.xml);
1174
1175
  break;
1175
1176
  case "fontFamily":
1176
- markParts.push(`<w:rFonts w:ascii="${escapeAttribute(mark.val)}" w:hAnsi="${escapeAttribute(mark.val)}"/>`);
1177
+ markParts.push(`<w:rFonts w:ascii="${escapeXmlAttribute(mark.val)}" w:hAnsi="${escapeXmlAttribute(mark.val)}"/>`);
1177
1178
  break;
1178
1179
  case "fontSize":
1179
1180
  markParts.push(`<w:sz w:val="${mark.val}"/>`);
1180
1181
  break;
1181
1182
  case "textColor":
1182
- markParts.push(`<w:color w:val="${escapeAttribute(mark.color)}"/>`);
1183
+ markParts.push(`<w:color w:val="${escapeXmlAttribute(mark.color)}"/>`);
1183
1184
  break;
1184
1185
  case "smallCaps":
1185
1186
  markParts.push("<w:smallCaps/>");
@@ -1228,10 +1229,6 @@ function escapeXml(value: string): string {
1228
1229
  .replace(/>/g, "&gt;");
1229
1230
  }
1230
1231
 
1231
- function escapeAttribute(value: string): string {
1232
- return escapeXml(value).replace(/"/g, "&quot;");
1233
- }
1234
-
1235
1232
  function offsetParagraphBoundary(
1236
1233
  boundary: RevisionParagraphBoundary,
1237
1234
  offset: number,
@@ -1426,7 +1423,7 @@ export function renderDeterministicRootAttributes(
1426
1423
  ...otherEntries,
1427
1424
  ];
1428
1425
  return ordered
1429
- .map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`)
1426
+ .map(([name, value]) => ` ${name}="${escapeXmlAttribute(value)}"`)
1430
1427
  .join("");
1431
1428
  }
1432
1429
 
@@ -1436,20 +1433,20 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
1436
1433
  // Header references
1437
1434
  if (props.headerReferences) {
1438
1435
  for (const ref of props.headerReferences) {
1439
- children.push(`<w:headerReference w:type="${escapeAttribute(ref.variant)}" r:id="${escapeAttribute(ref.relationshipId)}"/>`);
1436
+ children.push(`<w:headerReference w:type="${escapeXmlAttribute(ref.variant)}" r:id="${escapeXmlAttribute(ref.relationshipId)}"/>`);
1440
1437
  }
1441
1438
  }
1442
1439
 
1443
1440
  // Footer references
1444
1441
  if (props.footerReferences) {
1445
1442
  for (const ref of props.footerReferences) {
1446
- children.push(`<w:footerReference w:type="${escapeAttribute(ref.variant)}" r:id="${escapeAttribute(ref.relationshipId)}"/>`);
1443
+ children.push(`<w:footerReference w:type="${escapeXmlAttribute(ref.variant)}" r:id="${escapeXmlAttribute(ref.relationshipId)}"/>`);
1447
1444
  }
1448
1445
  }
1449
1446
 
1450
1447
  // Section type
1451
1448
  if (props.sectionType) {
1452
- children.push(`<w:type w:val="${escapeAttribute(props.sectionType)}"/>`);
1449
+ children.push(`<w:type w:val="${escapeXmlAttribute(props.sectionType)}"/>`);
1453
1450
  }
1454
1451
 
1455
1452
  // Page size
@@ -1502,10 +1499,10 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
1502
1499
  // Page numbering
1503
1500
  if (props.pageNumbering) {
1504
1501
  let pgNum = "<w:pgNumType";
1505
- if (props.pageNumbering.format) pgNum += ` w:fmt="${escapeAttribute(props.pageNumbering.format)}"`;
1502
+ if (props.pageNumbering.format) pgNum += ` w:fmt="${escapeXmlAttribute(props.pageNumbering.format)}"`;
1506
1503
  if (props.pageNumbering.start !== undefined) pgNum += ` w:start="${twip(props.pageNumbering.start)}"`;
1507
- if (props.pageNumbering.chapStyle) pgNum += ` w:chapStyle="${escapeAttribute(props.pageNumbering.chapStyle)}"`;
1508
- if (props.pageNumbering.chapSep) pgNum += ` w:chapSep="${escapeAttribute(props.pageNumbering.chapSep)}"`;
1504
+ if (props.pageNumbering.chapStyle) pgNum += ` w:chapStyle="${escapeXmlAttribute(props.pageNumbering.chapStyle)}"`;
1505
+ if (props.pageNumbering.chapSep) pgNum += ` w:chapSep="${escapeXmlAttribute(props.pageNumbering.chapSep)}"`;
1509
1506
  pgNum += "/>";
1510
1507
  children.push(pgNum);
1511
1508
  }
@@ -1522,7 +1519,7 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
1522
1519
  lineNumbering += ` w:distance="${twip(props.lineNumbering.distance)}"`;
1523
1520
  }
1524
1521
  if (props.lineNumbering.restart) {
1525
- lineNumbering += ` w:restart="${escapeAttribute(props.lineNumbering.restart)}"`;
1522
+ lineNumbering += ` w:restart="${escapeXmlAttribute(props.lineNumbering.restart)}"`;
1526
1523
  }
1527
1524
  lineNumbering += "/>";
1528
1525
  children.push(lineNumbering);
@@ -1531,13 +1528,13 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
1531
1528
  if (props.pageBorders) {
1532
1529
  const attrs: string[] = [];
1533
1530
  if (props.pageBorders.offsetFrom) {
1534
- attrs.push(`w:offsetFrom="${escapeAttribute(props.pageBorders.offsetFrom)}"`);
1531
+ attrs.push(`w:offsetFrom="${escapeXmlAttribute(props.pageBorders.offsetFrom)}"`);
1535
1532
  }
1536
1533
  if (props.pageBorders.display) {
1537
- attrs.push(`w:display="${escapeAttribute(props.pageBorders.display)}"`);
1534
+ attrs.push(`w:display="${escapeXmlAttribute(props.pageBorders.display)}"`);
1538
1535
  }
1539
1536
  if (props.pageBorders.zOrder) {
1540
- attrs.push(`w:zOrder="${escapeAttribute(props.pageBorders.zOrder)}"`);
1537
+ attrs.push(`w:zOrder="${escapeXmlAttribute(props.pageBorders.zOrder)}"`);
1541
1538
  }
1542
1539
  const borderXml = [
1543
1540
  serializeBorder("top", props.pageBorders.top),
@@ -1560,7 +1557,7 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
1560
1557
  if (props.documentGrid) {
1561
1558
  const attrs: string[] = [];
1562
1559
  if (props.documentGrid.type) {
1563
- attrs.push(`w:type="${escapeAttribute(props.documentGrid.type)}"`);
1560
+ attrs.push(`w:type="${escapeXmlAttribute(props.documentGrid.type)}"`);
1564
1561
  }
1565
1562
  if (props.documentGrid.linePitch !== undefined) {
1566
1563
  attrs.push(`w:linePitch="${props.documentGrid.linePitch}"`);
@@ -5,6 +5,7 @@ import {
5
5
  } from "../ooxml/numbering-sentinels.ts";
6
6
  import { buildRunPropertiesXml } from "./serialize-run-formatting.ts";
7
7
  import { twip } from "./twip.ts";
8
+ import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
8
9
 
9
10
  export const WORD_NUMBERING_CONTENT_TYPE =
10
11
  "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
@@ -50,7 +51,7 @@ export function serializeParagraphNumberingProperties(
50
51
  return "";
51
52
  }
52
53
 
53
- return `<w:numPr><w:ilvl w:val="${clampIlvl(numbering.level)}"/><w:numId w:val="${escapeAttribute(
54
+ return `<w:numPr><w:ilvl w:val="${clampIlvl(numbering.level)}"/><w:numId w:val="${escapeXmlAttribute(
54
55
  clampNonNegativeIdString(
55
56
  stripCanonicalPrefix(numbering.numberingInstanceId, "num:"),
56
57
  ),
@@ -58,7 +59,7 @@ export function serializeParagraphNumberingProperties(
58
59
  }
59
60
 
60
61
  function serializeAbstractDefinition(definition: NumberingCatalog["abstractDefinitions"][string]): string {
61
- const abstractNumId = escapeAttribute(
62
+ const abstractNumId = escapeXmlAttribute(
62
63
  clampNonNegativeIdString(
63
64
  stripCanonicalPrefix(definition.abstractNumberingId, "abstract-num:"),
64
65
  ),
@@ -67,19 +68,19 @@ function serializeAbstractDefinition(definition: NumberingCatalog["abstractDefin
67
68
  // ECMA-376 abstractNum child order: nsid, multiLevelType, tmpl, styleLink,
68
69
  // numStyleLink — all before the <w:lvl> children.
69
70
  const nsid = definition.nsid
70
- ? `<w:nsid w:val="${escapeAttribute(definition.nsid)}"/>`
71
+ ? `<w:nsid w:val="${escapeXmlAttribute(definition.nsid)}"/>`
71
72
  : "";
72
73
  const multiLevelType = definition.multiLevelType
73
- ? `<w:multiLevelType w:val="${escapeAttribute(definition.multiLevelType)}"/>`
74
+ ? `<w:multiLevelType w:val="${escapeXmlAttribute(definition.multiLevelType)}"/>`
74
75
  : "";
75
76
  const tmpl = definition.tplc
76
- ? `<w:tmpl w:val="${escapeAttribute(definition.tplc)}"/>`
77
+ ? `<w:tmpl w:val="${escapeXmlAttribute(definition.tplc)}"/>`
77
78
  : "";
78
79
  const styleLink = definition.styleLink
79
- ? `<w:styleLink w:val="${escapeAttribute(definition.styleLink)}"/>`
80
+ ? `<w:styleLink w:val="${escapeXmlAttribute(definition.styleLink)}"/>`
80
81
  : "";
81
82
  const numStyleLink = definition.numStyleLink
82
- ? `<w:numStyleLink w:val="${escapeAttribute(definition.numStyleLink)}"/>`
83
+ ? `<w:numStyleLink w:val="${escapeXmlAttribute(definition.numStyleLink)}"/>`
83
84
  : "";
84
85
 
85
86
  const levels = [...definition.levels]
@@ -100,19 +101,19 @@ function serializeLevel(
100
101
  level.startAt !== undefined
101
102
  ? `<w:start w:val="${clampStart(level.startAt)}"/>`
102
103
  : "";
103
- const numFmt = `<w:numFmt w:val="${escapeAttribute(level.format)}"/>`;
104
+ const numFmt = `<w:numFmt w:val="${escapeXmlAttribute(level.format)}"/>`;
104
105
  const lvlRestart =
105
106
  level.restartAfterLevel !== undefined
106
107
  ? `<w:lvlRestart w:val="${level.restartAfterLevel}"/>`
107
108
  : "";
108
109
  const paragraphStyle = level.paragraphStyleId
109
- ? `<w:pStyle w:val="${escapeAttribute(level.paragraphStyleId)}"/>`
110
+ ? `<w:pStyle w:val="${escapeXmlAttribute(level.paragraphStyleId)}"/>`
110
111
  : "";
111
112
  const isLegal = level.isLegalNumbering ? "<w:isLgl/>" : "";
112
- const suffix = level.suffix ? `<w:suff w:val="${escapeAttribute(level.suffix)}"/>` : "";
113
- const lvlText = `<w:lvlText w:val="${escapeAttribute(level.text)}"/>`;
113
+ const suffix = level.suffix ? `<w:suff w:val="${escapeXmlAttribute(level.suffix)}"/>` : "";
114
+ const lvlText = `<w:lvlText w:val="${escapeXmlAttribute(level.text)}"/>`;
114
115
  const justification = level.paragraphGeometry?.justification
115
- ? `<w:lvlJc w:val="${escapeAttribute(level.paragraphGeometry.justification)}"/>`
116
+ ? `<w:lvlJc w:val="${escapeXmlAttribute(level.paragraphGeometry.justification)}"/>`
116
117
  : "";
117
118
  const paragraphProperties = serializeLevelParagraphGeometry(level.paragraphGeometry);
118
119
  const runProperties = buildRunPropertiesXml(level.runProperties);
@@ -134,13 +135,13 @@ function serializeLevelOverride(
134
135
  level.startAt !== undefined
135
136
  ? `<w:start w:val="${clampStart(level.startAt)}"/>`
136
137
  : "";
137
- const format = level.format ? `<w:numFmt w:val="${escapeAttribute(level.format)}"/>` : "";
138
+ const format = level.format ? `<w:numFmt w:val="${escapeXmlAttribute(level.format)}"/>` : "";
138
139
  const lvlRestart =
139
140
  level.restartAfterLevel !== undefined
140
141
  ? `<w:lvlRestart w:val="${level.restartAfterLevel}"/>`
141
142
  : "";
142
143
  const paragraphStyle = level.paragraphStyleId
143
- ? `<w:pStyle w:val="${escapeAttribute(level.paragraphStyleId)}"/>`
144
+ ? `<w:pStyle w:val="${escapeXmlAttribute(level.paragraphStyleId)}"/>`
144
145
  : "";
145
146
  // ST_OnOff element (A.3):
146
147
  // - true → <w:isLgl/>.
@@ -153,12 +154,12 @@ function serializeLevelOverride(
153
154
  : level.isLegalNumbering === false
154
155
  ? `<w:isLgl w:val="false"/>`
155
156
  : "";
156
- const suffix = level.suffix ? `<w:suff w:val="${escapeAttribute(level.suffix)}"/>` : "";
157
+ const suffix = level.suffix ? `<w:suff w:val="${escapeXmlAttribute(level.suffix)}"/>` : "";
157
158
  const text = level.text !== undefined
158
- ? `<w:lvlText w:val="${escapeAttribute(level.text)}"/>`
159
+ ? `<w:lvlText w:val="${escapeXmlAttribute(level.text)}"/>`
159
160
  : "";
160
161
  const justification = level.paragraphGeometry?.justification
161
- ? `<w:lvlJc w:val="${escapeAttribute(level.paragraphGeometry.justification)}"/>`
162
+ ? `<w:lvlJc w:val="${escapeXmlAttribute(level.paragraphGeometry.justification)}"/>`
162
163
  : "";
163
164
  const paragraphProperties = serializeLevelParagraphGeometry(level.paragraphGeometry);
164
165
  const runProperties = buildRunPropertiesXml(level.runProperties);
@@ -170,10 +171,10 @@ function serializeLevelOverride(
170
171
  }
171
172
 
172
173
  function serializeInstance(instance: NumberingCatalog["instances"][string]): string {
173
- const numId = escapeAttribute(
174
+ const numId = escapeXmlAttribute(
174
175
  clampNonNegativeIdString(stripCanonicalPrefix(instance.numberingInstanceId, "num:")),
175
176
  );
176
- const abstractNumId = escapeAttribute(
177
+ const abstractNumId = escapeXmlAttribute(
177
178
  clampNonNegativeIdString(
178
179
  stripCanonicalPrefix(instance.abstractNumberingId, "abstract-num:"),
179
180
  ),
@@ -332,11 +333,3 @@ function stripKnownPrefix(value: string): string {
332
333
  function stripCanonicalPrefix(value: string, prefix: "abstract-num:" | "num:"): string {
333
334
  return value.startsWith(prefix) ? value.slice(prefix.length) : value;
334
335
  }
335
-
336
- function escapeAttribute(value: string): string {
337
- return value
338
- .replace(/&/g, "&amp;")
339
- .replace(/"/g, "&quot;")
340
- .replace(/</g, "&lt;")
341
- .replace(/>/g, "&gt;");
342
- }
@@ -3,6 +3,7 @@ import {
3
3
  mapRevisionBoundaries,
4
4
  type RevisionParagraphBoundary,
5
5
  } from "../ooxml/revision-boundaries.ts";
6
+ import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
6
7
 
7
8
  interface XmlReplacement {
8
9
  start: number;
@@ -360,7 +361,7 @@ function serializeRevisionAttributes(revision: RevisionRecord): string {
360
361
 
361
362
  return Object.entries(attributes)
362
363
  .filter(([, value]) => value && value.length > 0)
363
- .map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`)
364
+ .map(([name, value]) => ` ${name}="${escapeXmlAttribute(value)}"`)
364
365
  .join("");
365
366
  }
366
367
 
@@ -453,11 +454,3 @@ function applyReplacements(documentXml: string, replacements: readonly XmlReplac
453
454
 
454
455
  return output;
455
456
  }
456
-
457
- function escapeAttribute(value: string): string {
458
- return value
459
- .replace(/&/g, "&amp;")
460
- .replace(/</g, "&lt;")
461
- .replace(/>/g, "&gt;")
462
- .replace(/"/g, "&quot;");
463
- }
@@ -11,6 +11,7 @@ import type {
11
11
  ParsedTableWidth,
12
12
  } from "../ooxml/parse-tables.ts";
13
13
  import { twip } from "./twip.ts";
14
+ import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
14
15
 
15
16
  export function serializeTable(table: ParsedTable): string {
16
17
  const propertiesXml = table.propertiesXml ?? buildTablePropertiesXml(table);
@@ -60,10 +61,10 @@ function buildTablePropertiesXml(table: ParsedTable): string {
60
61
  children.push(table.bidiVisual ? `<w:bidiVisual/>` : `<w:bidiVisual w:val="0"/>`);
61
62
  }
62
63
  if (table.caption !== undefined) {
63
- children.push(`<w:tblCaption w:val="${escapeAttribute(table.caption)}"/>`);
64
+ children.push(`<w:tblCaption w:val="${escapeXmlAttribute(table.caption)}"/>`);
64
65
  }
65
66
  if (table.description !== undefined) {
66
- children.push(`<w:tblDescription w:val="${escapeAttribute(table.description)}"/>`);
67
+ children.push(`<w:tblDescription w:val="${escapeXmlAttribute(table.description)}"/>`);
67
68
  }
68
69
  if (table.floating) {
69
70
  const floatingXml = serializeTableFloating(table.floating);
@@ -106,7 +107,7 @@ function serializeTableFloating(floating: NonNullable<ParsedTable["floating"]>):
106
107
  function buildRowPropertiesXml(row: ParsedTableRow): string {
107
108
  const children: string[] = [];
108
109
  if (row.cnfStyle) {
109
- children.push(`<w:cnfStyle w:val="${escapeAttribute(row.cnfStyle)}"/>`);
110
+ children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(row.cnfStyle)}"/>`);
110
111
  }
111
112
  if (row.cantSplit !== undefined) {
112
113
  children.push(row.cantSplit ? `<w:cantSplit/>` : `<w:cantSplit w:val="0"/>`);
@@ -131,7 +132,7 @@ function ensureCellProperties(cell: ParsedTableCell): string {
131
132
 
132
133
  const children: string[] = [];
133
134
  if (cell.cnfStyle) {
134
- children.push(`<w:cnfStyle w:val="${escapeAttribute(cell.cnfStyle)}"/>`);
135
+ children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(cell.cnfStyle)}"/>`);
135
136
  }
136
137
  if (cell.width) {
137
138
  children.push(serializeWidth("tcW", cell.width));
@@ -174,15 +175,15 @@ function ensureCellProperties(cell: ParsedTableCell): string {
174
175
  }
175
176
 
176
177
  function serializeWidth(element: string, width: ParsedTableWidth): string {
177
- return `<w:${element} w:w="${twip(width.value)}" w:type="${width.type}"/>`;
178
+ return `<w:${element} w:w="${twip(width.value)}" w:type="${escapeXmlAttribute(width.type)}"/>`;
178
179
  }
179
180
 
180
181
  function serializeBorderSpec(element: string, spec: ParsedBorderSpec): string {
181
182
  const attrs: string[] = [];
182
- if (spec.value) attrs.push(`w:val="${spec.value}"`);
183
+ if (spec.value) attrs.push(`w:val="${escapeXmlAttribute(spec.value)}"`);
183
184
  if (spec.size !== undefined) attrs.push(`w:sz="${twip(spec.size)}"`);
184
185
  if (spec.space !== undefined) attrs.push(`w:space="${twip(spec.space)}"`);
185
- if (spec.color) attrs.push(`w:color="${spec.color}"`);
186
+ if (spec.color) attrs.push(`w:color="${escapeXmlAttribute(spec.color)}"`);
186
187
  const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
187
188
  return `<w:${element}${attrsStr}/>`;
188
189
  }
@@ -240,11 +241,3 @@ function serializeTableCellMargins(margins: ParsedCellMargins): string {
240
241
  if (margins.right !== undefined) parts.push(`<w:right w:w="${twip(margins.right)}" w:type="dxa"/>`);
241
242
  return parts.join("");
242
243
  }
243
-
244
- function escapeAttribute(value: string): string {
245
- return value
246
- .replace(/&/gu, "&amp;")
247
- .replace(/"/gu, "&quot;")
248
- .replace(/</gu, "&lt;")
249
- .replace(/>/gu, "&gt;");
250
- }