@beyondwork/docx-react-component 1.0.41 → 1.0.42

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 (88) hide show
  1. package/package.json +13 -1
  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/external-custody-types.ts +74 -0
  6. package/src/api/participants-types.ts +18 -0
  7. package/src/api/public-types.ts +347 -4
  8. package/src/api/scope-metadata-resolver-types.ts +88 -0
  9. package/src/core/commands/formatting-commands.ts +1 -1
  10. package/src/core/commands/index.ts +568 -1
  11. package/src/index.ts +118 -1
  12. package/src/io/export/escape-xml-attribute.ts +26 -0
  13. package/src/io/export/external-send.ts +188 -0
  14. package/src/io/export/serialize-comments.ts +13 -16
  15. package/src/io/export/serialize-footnotes.ts +17 -24
  16. package/src/io/export/serialize-headers-footers.ts +17 -24
  17. package/src/io/export/serialize-main-document.ts +59 -62
  18. package/src/io/export/serialize-numbering.ts +20 -27
  19. package/src/io/export/serialize-runtime-revisions.ts +2 -9
  20. package/src/io/export/serialize-tables.ts +8 -15
  21. package/src/io/export/table-properties-xml.ts +25 -32
  22. package/src/io/import/external-reimport.ts +40 -0
  23. package/src/io/ooxml/bw-xml.ts +244 -0
  24. package/src/io/ooxml/canonicalize-payload.ts +301 -0
  25. package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
  26. package/src/io/ooxml/comment-presentation-payload.ts +311 -0
  27. package/src/io/ooxml/external-custody-payload.ts +102 -0
  28. package/src/io/ooxml/participants-payload.ts +97 -0
  29. package/src/io/ooxml/payload-signature.ts +112 -0
  30. package/src/io/ooxml/workflow-payload-validator.ts +271 -0
  31. package/src/io/ooxml/workflow-payload.ts +146 -7
  32. package/src/runtime/awareness-identity.ts +173 -0
  33. package/src/runtime/collab/event-types.ts +27 -0
  34. package/src/runtime/collab-session-bridge.ts +157 -0
  35. package/src/runtime/collab-session-facet.ts +193 -0
  36. package/src/runtime/collab-session.ts +273 -0
  37. package/src/runtime/comment-negotiation-sync.ts +91 -0
  38. package/src/runtime/comment-negotiation.ts +158 -0
  39. package/src/runtime/comment-presentation.ts +223 -0
  40. package/src/runtime/document-runtime.ts +280 -93
  41. package/src/runtime/external-send-runtime.ts +117 -0
  42. package/src/runtime/layout/docx-font-loader.ts +11 -30
  43. package/src/runtime/layout/inert-layout-facet.ts +2 -0
  44. package/src/runtime/layout/layout-engine-instance.ts +122 -12
  45. package/src/runtime/layout/page-graph.ts +79 -7
  46. package/src/runtime/layout/paginated-layout-engine.ts +230 -34
  47. package/src/runtime/layout/public-facet.ts +185 -13
  48. package/src/runtime/layout/table-row-split.ts +316 -0
  49. package/src/runtime/markdown-sanitizer.ts +132 -0
  50. package/src/runtime/participants.ts +134 -0
  51. package/src/runtime/resign-payload.ts +120 -0
  52. package/src/runtime/tamper-gate.ts +157 -0
  53. package/src/runtime/workflow-markup.ts +9 -0
  54. package/src/runtime/workflow-rail-segments.ts +244 -5
  55. package/src/ui/WordReviewEditor.tsx +587 -0
  56. package/src/ui/editor-runtime-boundary.ts +1 -0
  57. package/src/ui/editor-shell-view.tsx +11 -0
  58. package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
  59. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
  60. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
  61. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
  62. package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
  63. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
  64. package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
  65. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
  66. package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
  67. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
  69. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
  70. package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
  71. package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
  72. package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
  73. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +58 -0
  74. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
  75. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
  76. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
  77. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
  78. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
  79. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
  80. package/src/ui-tailwind/editor-surface/pm-schema.ts +15 -13
  81. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
  82. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -14
  83. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
  84. package/src/ui-tailwind/index.ts +32 -0
  85. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
  87. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  88. package/src/ui-tailwind/tw-review-workspace.tsx +293 -34
@@ -17,6 +17,7 @@ import {
17
17
  serializeTableRowPropertiesXml,
18
18
  } from "./table-properties-xml.ts";
19
19
  import { twip } from "./twip.ts";
20
+ import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
20
21
 
21
22
  export const WORD_HEADER_CONTENT_TYPE =
22
23
  "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml";
@@ -178,7 +179,7 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
178
179
  const parts: string[] = [];
179
180
 
180
181
  if (paragraph.styleId) {
181
- parts.push(`<w:pStyle w:val="${escapeAttribute(paragraph.styleId)}"/>`);
182
+ parts.push(`<w:pStyle w:val="${escapeXmlAttribute(paragraph.styleId)}"/>`);
182
183
  }
183
184
  if (paragraph.spacing) {
184
185
  const s = paragraph.spacing;
@@ -186,7 +187,7 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
186
187
  if (s.before !== undefined) attrs.push(`w:before="${twip(s.before)}"`);
187
188
  if (s.after !== undefined) attrs.push(`w:after="${twip(s.after)}"`);
188
189
  if (s.line !== undefined) attrs.push(`w:line="${twip(s.line)}"`);
189
- if (s.lineRule) attrs.push(`w:lineRule="${escapeAttribute(s.lineRule)}"`);
190
+ if (s.lineRule) attrs.push(`w:lineRule="${escapeXmlAttribute(s.lineRule)}"`);
190
191
  if (attrs.length > 0) parts.push(`<w:spacing ${attrs.join(" ")}/>`);
191
192
  }
192
193
  if (paragraph.indentation) {
@@ -199,11 +200,11 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
199
200
  if (attrs.length > 0) parts.push(`<w:ind ${attrs.join(" ")}/>`);
200
201
  }
201
202
  if (paragraph.alignment) {
202
- parts.push(`<w:jc w:val="${escapeAttribute(paragraph.alignment)}"/>`);
203
+ parts.push(`<w:jc w:val="${escapeXmlAttribute(paragraph.alignment)}"/>`);
203
204
  }
204
205
  if (paragraph.tabStops && paragraph.tabStops.length > 0) {
205
206
  const tabsXml = paragraph.tabStops.map((tab) => {
206
- const leaderAttr = tab.leader ? ` w:leader="${escapeAttribute(tab.leader)}"` : "";
207
+ const leaderAttr = tab.leader ? ` w:leader="${escapeXmlAttribute(tab.leader)}"` : "";
207
208
  return `<w:tab w:val="${tab.align}" w:pos="${twip(tab.position)}"${leaderAttr}/>`;
208
209
  }).join("");
209
210
  parts.push(`<w:tabs>${tabsXml}</w:tabs>`);
@@ -228,14 +229,14 @@ function serializeInlineNode(node: InlineNode): string {
228
229
  case "footnote_ref": {
229
230
  const refElement =
230
231
  node.noteKind === "footnote"
231
- ? `<w:footnoteReference w:id="${escapeAttribute(node.noteId)}"/>`
232
- : `<w:endnoteReference w:id="${escapeAttribute(node.noteId)}"/>`;
232
+ ? `<w:footnoteReference w:id="${escapeXmlAttribute(node.noteId)}"/>`
233
+ : `<w:endnoteReference w:id="${escapeXmlAttribute(node.noteId)}"/>`;
233
234
  return `<w:r><w:rPr><w:rStyle w:val="${node.noteKind === "footnote" ? "FootnoteReference" : "EndnoteReference"}"/></w:rPr>${refElement}</w:r>`;
234
235
  }
235
236
  case "bookmark_start":
236
- return `<w:bookmarkStart w:id="${escapeAttribute(node.bookmarkId)}" w:name="${escapeAttribute(node.name)}"/>`;
237
+ return `<w:bookmarkStart w:id="${escapeXmlAttribute(node.bookmarkId)}" w:name="${escapeXmlAttribute(node.name)}"/>`;
237
238
  case "bookmark_end":
238
- return `<w:bookmarkEnd w:id="${escapeAttribute(node.bookmarkId)}"/>`;
239
+ return `<w:bookmarkEnd w:id="${escapeXmlAttribute(node.bookmarkId)}"/>`;
239
240
  case "field":
240
241
  if (node.children && node.children.length > 0) {
241
242
  const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
@@ -248,10 +249,10 @@ function serializeInlineNode(node: InlineNode): string {
248
249
  `<w:r><w:fldChar w:fldCharType="end"/></w:r>`
249
250
  );
250
251
  }
251
- return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
252
+ return `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
252
253
  }
253
254
  if (node.fieldType === "simple") {
254
- return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
255
+ return `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}"/>`;
255
256
  }
256
257
  return `<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>`;
257
258
  case "hyperlink":
@@ -301,19 +302,19 @@ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
301
302
  parts.push("<w:dstrike/>");
302
303
  break;
303
304
  case "fontFamily":
304
- parts.push(`<w:rFonts w:ascii="${escapeAttribute(mark.val)}" w:hAnsi="${escapeAttribute(mark.val)}"/>`);
305
+ parts.push(`<w:rFonts w:ascii="${escapeXmlAttribute(mark.val)}" w:hAnsi="${escapeXmlAttribute(mark.val)}"/>`);
305
306
  break;
306
307
  case "fontSize":
307
308
  parts.push(`<w:sz w:val="${twip(mark.val)}"/>`);
308
309
  break;
309
310
  case "textColor":
310
- parts.push(`<w:color w:val="${escapeAttribute(mark.color)}"/>`);
311
+ parts.push(`<w:color w:val="${escapeXmlAttribute(mark.color)}"/>`);
311
312
  break;
312
313
  case "highlight":
313
- parts.push(`<w:highlight w:val="${escapeAttribute(mark.val)}"/>`);
314
+ parts.push(`<w:highlight w:val="${escapeXmlAttribute(mark.val)}"/>`);
314
315
  break;
315
316
  case "backgroundColor":
316
- parts.push(`<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttribute(mark.color)}"/>`);
317
+ parts.push(`<w:shd w:val="clear" w:color="auto" w:fill="${escapeXmlAttribute(mark.color)}"/>`);
317
318
  break;
318
319
  case "smallCaps":
319
320
  parts.push("<w:smallCaps/>");
@@ -345,23 +346,15 @@ function escapeXml(text: string): string {
345
346
  .replace(/>/g, "&gt;");
346
347
  }
347
348
 
348
- function escapeAttribute(value: string): string {
349
- return value
350
- .replace(/&/g, "&amp;")
351
- .replace(/</g, "&lt;")
352
- .replace(/>/g, "&gt;")
353
- .replace(/"/g, "&quot;");
354
- }
355
-
356
349
  function serializeHyperlinkNode(node: Extract<InlineNode, { type: "hyperlink" }>): string {
357
350
  const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
358
351
  if (node.href.startsWith("#")) {
359
- return `<w:hyperlink w:anchor="${escapeAttribute(node.href.slice(1))}">${childrenXml}</w:hyperlink>`;
352
+ return `<w:hyperlink w:anchor="${escapeXmlAttribute(node.href.slice(1))}">${childrenXml}</w:hyperlink>`;
360
353
  }
361
354
  if (!/^rId[A-Za-z0-9._-]+$/u.test(node.href)) {
362
355
  throw new Error("Cannot safely serialize URL-backed header/footer hyperlinks without relationship context.");
363
356
  }
364
- return `<w:hyperlink r:id="${escapeAttribute(node.href)}">${childrenXml}</w:hyperlink>`;
357
+ return `<w:hyperlink r:id="${escapeXmlAttribute(node.href)}">${childrenXml}</w:hyperlink>`;
365
358
  }
366
359
 
367
360
  function buildTableCellPropertiesXml(cell: TableCellNode): string {
@@ -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
- }