@stll/folio-core 0.12.0 → 0.13.0

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 (131) hide show
  1. package/dist/ai-edits/apply.d.ts +2 -1
  2. package/dist/ai-edits/apply.js +118 -40
  3. package/dist/ai-edits/blockRange.js +1 -1
  4. package/dist/ai-edits/clean-text.js +2 -1
  5. package/dist/ai-edits/headless.js +3 -3
  6. package/dist/ai-edits/snapshot.js +21 -3
  7. package/dist/ai-edits/table-cell-mutations.js +2 -1
  8. package/dist/ai-edits/table-row-column-mutations.d.ts +9 -1
  9. package/dist/ai-edits/table-row-column-mutations.js +1 -1
  10. package/dist/ai-edits/types.d.ts +28 -1
  11. package/dist/ai-edits/word-diff.js +21 -0
  12. package/dist/compat/eigenpal.d.ts +3 -3
  13. package/dist/compat/eigenpal.js +3 -3
  14. package/dist/controller/headerFooterEditorManager.js +1 -1
  15. package/dist/controller/hiddenEditorManager.js +12 -1
  16. package/dist/controller/layoutPipeline.js +1 -1
  17. package/dist/controller/noteEditorManager.js +1 -1
  18. package/dist/document-operations.d.ts +13 -13
  19. package/dist/document-operations.js +31 -21
  20. package/dist/docx/blockContentParser.js +19 -5
  21. package/dist/docx/commentRangeIntegrity.d.ts +6 -0
  22. package/dist/docx/commentRangeIntegrity.js +186 -0
  23. package/dist/docx/conformance.js +57 -3
  24. package/dist/docx/encryption/agileDecryption.js +12 -2
  25. package/dist/docx/encryption/compoundFile.js +6 -1
  26. package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
  27. package/dist/docx/encryption/encryptionInfo.js +18 -2
  28. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  29. package/dist/docx/ensureParaIds.js +7 -6
  30. package/dist/docx/groupDrawingParser.js +10 -5
  31. package/dist/docx/hyperlinkParser.js +1 -1
  32. package/dist/docx/imageParser.js +10 -6
  33. package/dist/docx/metadataPrivacy.js +17 -3
  34. package/dist/docx/paragraphParser.js +7 -3
  35. package/dist/docx/rezip.js +19 -8
  36. package/dist/docx/runParser.js +13 -4
  37. package/dist/docx/sectionParser.js +17 -4
  38. package/dist/docx/selectiveSave.js +4 -0
  39. package/dist/docx/selectiveXmlPatch.d.ts +22 -1
  40. package/dist/docx/selectiveXmlPatch.js +61 -1
  41. package/dist/docx/serializer/blockSdtSerializer.js +3 -2
  42. package/dist/docx/serializer/commentSerializer.js +4 -4
  43. package/dist/docx/serializer/paragraphSerializer.js +16 -14
  44. package/dist/docx/serializer/runSerializer.js +21 -19
  45. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
  46. package/dist/docx/serializer/tableSerializer.js +9 -7
  47. package/dist/docx/serializer/xmlUtils.d.ts +13 -1
  48. package/dist/docx/serializer/xmlUtils.js +27 -1
  49. package/dist/docx/server/extractDocxText.js +55 -8
  50. package/dist/docx/settingsParser.js +15 -2
  51. package/dist/docx/tableParser.d.ts +8 -1
  52. package/dist/docx/tableParser.js +13 -3
  53. package/dist/docx/unzip.js +1 -0
  54. package/dist/docx/vmlImageParser.js +11 -3
  55. package/dist/docx/xmlParser.js +11 -1
  56. package/dist/fonts/embeddedFonts.d.ts +33 -5
  57. package/dist/fonts/embeddedFonts.js +37 -8
  58. package/dist/index.d.ts +3 -3
  59. package/dist/index.js +3 -3
  60. package/dist/layout-bridge/convert/toFlowBlocks.js +15 -0
  61. package/dist/layout-engine/index.js +16 -3
  62. package/dist/layout-engine/measure/cache.js +2 -0
  63. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
  64. package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
  65. package/dist/layout-engine/measure/lineBreakProvider.js +3 -3
  66. package/dist/layout-engine/measure/measureBlocks.js +9 -1
  67. package/dist/layout-engine/measure/measureHelpers.d.ts +10 -1
  68. package/dist/layout-engine/measure/measureHelpers.js +12 -1
  69. package/dist/layout-engine/measure/measureParagraph.js +16 -0
  70. package/dist/layout-engine/types.d.ts +22 -2
  71. package/dist/layout-painter/renderImage.d.ts +12 -1
  72. package/dist/layout-painter/renderImage.js +21 -4
  73. package/dist/layout-painter/renderPage.js +3 -2
  74. package/dist/layout-painter/renderParagraph.js +33 -9
  75. package/dist/layout-painter/renderTable.js +2 -1
  76. package/dist/layout-painter/renderWatermark.d.ts +1 -1
  77. package/dist/layout-painter/renderWatermark.js +2 -1
  78. package/dist/markdown/fromMarkdown.js +1 -1
  79. package/dist/prosemirror/attrs/index.d.ts +1 -1
  80. package/dist/prosemirror/attrs/index.js +21 -0
  81. package/dist/prosemirror/commands/comments.d.ts +76 -1
  82. package/dist/prosemirror/commands/comments.js +401 -28
  83. package/dist/prosemirror/commands/hyperlink.js +15 -1
  84. package/dist/prosemirror/commands/image.js +7 -1
  85. package/dist/prosemirror/commands/index.d.ts +2 -2
  86. package/dist/prosemirror/commands/index.js +2 -2
  87. package/dist/prosemirror/commands/tableCellMergeResolution.js +1 -1
  88. package/dist/prosemirror/commentIdAllocator.js +13 -3
  89. package/dist/prosemirror/conversion/fromProseDoc.js +122 -5
  90. package/dist/prosemirror/conversion/index.js +1 -1
  91. package/dist/prosemirror/conversion/toProseDoc.js +20 -8
  92. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
  93. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
  94. package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
  95. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
  96. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +49 -13
  97. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  98. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -3
  99. package/dist/prosemirror/index.d.ts +1 -1
  100. package/dist/prosemirror/index.js +1 -1
  101. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +2 -2
  102. package/dist/prosemirror/plugins/contentControlWidgets.js +4 -4
  103. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
  104. package/dist/prosemirror/plugins/revisionIds.js +57 -0
  105. package/dist/prosemirror/plugins/suggestionMode.js +4 -3
  106. package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
  107. package/dist/prosemirror/runFormattingMarkNames.js +40 -0
  108. package/dist/prosemirror/schema/index.d.ts +1 -1
  109. package/dist/prosemirror/schema/marks.d.ts +27 -4
  110. package/dist/prosemirror/schema/marks.js +15 -0
  111. package/dist/prosemirror/schema/nodes.d.ts +50 -2
  112. package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
  113. package/dist/server.d.ts +2 -2
  114. package/dist/server.js +3 -3
  115. package/dist/utils/clipboard.d.ts +10 -1
  116. package/dist/utils/clipboard.js +33 -6
  117. package/dist/utils/colorResolver.d.ts +8 -1
  118. package/dist/utils/colorResolver.js +35 -5
  119. package/dist/utils/fontResolver.d.ts +2 -1
  120. package/dist/utils/fontResolver.js +9 -3
  121. package/dist/utils/hexId.d.ts +11 -1
  122. package/dist/utils/hexId.js +11 -1
  123. package/dist/utils/sanitizeImageSrc.d.ts +21 -0
  124. package/dist/utils/sanitizeImageSrc.js +30 -0
  125. package/dist/utils/stripXmlDeclarations.d.ts +11 -0
  126. package/dist/utils/stripXmlDeclarations.js +35 -0
  127. package/dist/version-comparison.d.ts +22 -2
  128. package/dist/version-comparison.js +41 -12
  129. package/dist/watermark/index.d.ts +11 -1
  130. package/dist/watermark/index.js +22 -1
  131. package/package.json +3 -2
@@ -5,10 +5,13 @@ import { ShapeOutlineStyleSchema, narrowEnum } from "../../docx/parserEnums.js";
5
5
  import { numPrEqual } from "../../docx/numberingParser.js";
6
6
  import { pixelsToEmu } from "../../utils/units.js";
7
7
  import { autospacingMatchesBase, hasAutospacingBaseSide } from "../autospacingBase.js";
8
+ import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
8
9
  import { expectTextBoxAnchorAttrs } from "../textBoxAnchorAttrs.js";
9
10
  import { assertValidProseMirrorDocument } from "../validation.js";
10
11
  import { runShadingAttrsToShading } from "./runShadingMark.js";
11
12
  import { sdtPropertiesFromAttrs, sdtPropertiesMatchAttrs } from "./sdtAttrs.js";
13
+ import { textFormattingToMarks } from "./toProseDoc.js";
14
+ import { Fragment } from "prosemirror-model";
12
15
  //#region src/prosemirror/conversion/fromProseDoc.ts
13
16
  function normalizeShapeOutlineStyle(style) {
14
17
  if (!style) return;
@@ -71,11 +74,120 @@ function fromProseDoc(pmDoc, baseDocument) {
71
74
  };
72
75
  return { package: { document: documentBody } };
73
76
  }
77
+ const isSuggestedMark = (mark) => mark.attrs["provenance"] === "suggested";
78
+ const hasSuggestedInsertion = (marks) => marks.some((mark) => mark.type.name === "insertion" && isSuggestedMark(mark));
79
+ /**
80
+ * Compute the mark set an inline node should keep once its suggested tracked
81
+ * changes are stripped. Suggested deletions are dropped so their text survives
82
+ * as plain content; a suggested `runPropertyChange` is dropped and the run's
83
+ * live formatting is reverted to the recorded `previousFormatting`.
84
+ *
85
+ * Returns the input array unchanged when there is nothing to strip so callers
86
+ * can skip rebuilding the node.
87
+ */
88
+ function stripSuggestedInlineMarks(marks) {
89
+ const hasSuggestedDeletion = marks.some((mark) => mark.type.name === "deletion" && isSuggestedMark(mark));
90
+ const suggestedRunPropertyChange = marks.find((mark) => mark.type.name === "runPropertyChange" && isSuggestedMark(mark));
91
+ if (!hasSuggestedDeletion && !suggestedRunPropertyChange) return marks;
92
+ let next = marks.filter((mark) => !((mark.type.name === "deletion" || mark.type.name === "runPropertyChange") && isSuggestedMark(mark)));
93
+ if (suggestedRunPropertyChange) {
94
+ const previousFormatting = expectRunPropertyChangeMarkAttrs(suggestedRunPropertyChange).changes.at(0)?.previousFormatting;
95
+ next = next.filter((mark) => !RUN_FORMATTING_MARK_NAMES.has(mark.type.name));
96
+ for (const restored of textFormattingToMarks(previousFormatting)) next = restored.addToSet(next);
97
+ }
98
+ return next;
99
+ }
100
+ /**
101
+ * Compute the node attrs a block/structural node keeps once its suggested
102
+ * revision markers are neutralized. Returns `null` when nothing changes.
103
+ *
104
+ * - a suggested `trDel` / `cellMarker` (insertion or deletion) is cleared so
105
+ * the row/cell serializes as though the proposed change never happened;
106
+ * merge markers never carry suggestion provenance (structurally excluded);
107
+ * - suggested INSERT markers are handled by the caller, which drops the whole
108
+ * node instead of clearing an attr.
109
+ *
110
+ * Reads go through the typed attr readers (adapter-boundary convention).
111
+ */
112
+ function stripSuggestedNodeAttrs(node) {
113
+ const name = node.type.name;
114
+ if (name === "tableRow") {
115
+ const rowAttrs = expectTableRowAttrs(node);
116
+ if (rowAttrs.trDel?.provenance === "suggested") return {
117
+ ...rowAttrs,
118
+ trDel: null
119
+ };
120
+ return null;
121
+ }
122
+ if (name === "tableCell" || name === "tableHeader") {
123
+ const cellAttrs = expectTableCellAttrs(node);
124
+ const marker = cellAttrs.cellMarker;
125
+ if (marker && marker.kind !== "merge" && marker.info.provenance === "suggested") return {
126
+ ...cellAttrs,
127
+ cellMarker: null
128
+ };
129
+ }
130
+ return null;
131
+ }
132
+ /**
133
+ * Whether a block/structural node is a suggested whole-node INSERT and must be
134
+ * dropped from serialization entirely (paragraph, table, row, or column cell).
135
+ */
136
+ function isSuggestedInsertedNode(node) {
137
+ const name = node.type.name;
138
+ if (name === "paragraph") return expectParagraphAttrs(node)._suggestedInsert != null;
139
+ if (name === "table") return expectTableAttrs(node)._suggestedInsert != null;
140
+ if (name === "tableRow") return expectTableRowAttrs(node).trIns?.provenance === "suggested";
141
+ if (name === "tableCell" || name === "tableHeader") {
142
+ const marker = expectTableCellAttrs(node).cellMarker;
143
+ return marker?.kind === "ins" && marker.info.provenance === "suggested";
144
+ }
145
+ return false;
146
+ }
147
+ /**
148
+ * Recursively rewrite a ProseMirror node tree, removing every suggested
149
+ * tracked change. Returns `null` when the node itself must be dropped: a
150
+ * suggested-inserted inline node, or a suggested-inserted block/row/cell.
151
+ * Surviving block nodes are rebuilt with their suggested delete/merge markers
152
+ * cleared and their children stripped.
153
+ */
154
+ function mapSuggestionStrippedNode(node) {
155
+ if (node.isInline) {
156
+ if (hasSuggestedInsertion(node.marks)) return null;
157
+ const marks = stripSuggestedInlineMarks(node.marks);
158
+ return marks === node.marks ? node : node.mark(marks);
159
+ }
160
+ if (isSuggestedInsertedNode(node)) return null;
161
+ const nextAttrs = stripSuggestedNodeAttrs(node);
162
+ const children = [];
163
+ let changed = nextAttrs !== null;
164
+ node.forEach((child) => {
165
+ const mapped = mapSuggestionStrippedNode(child);
166
+ if (mapped === null) {
167
+ changed = true;
168
+ return;
169
+ }
170
+ if (mapped !== child) changed = true;
171
+ children.push(mapped);
172
+ });
173
+ if (!changed) return node;
174
+ const content = Fragment.fromArray(children);
175
+ return nextAttrs === null ? node.copy(content) : node.type.create(nextAttrs, content, node.marks);
176
+ }
177
+ /**
178
+ * Strip suggested (AI-proposed) tracked changes from a document so it
179
+ * serializes as though the suggestion never happened. See the call site in
180
+ * {@link extractBlocks} for why this is the sole serialization boundary.
181
+ */
182
+ function stripSuggestedProvenance(doc) {
183
+ return mapSuggestionStrippedNode(doc) ?? doc;
184
+ }
74
185
  /**
75
186
  * Extract block content (paragraphs, tables, block SDTs) from a ProseMirror
76
187
  * document.
77
188
  */
78
- function extractBlocks(pmDoc) {
189
+ function extractBlocks(inputDoc) {
190
+ const pmDoc = stripSuggestedProvenance(inputDoc);
79
191
  const blocks = [];
80
192
  const textBoxAnchorMarkers = /* @__PURE__ */ new Map();
81
193
  const documentCounts = buildDocumentTrackedChangeCounts(pmDoc);
@@ -605,7 +717,8 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
605
717
  const info = {
606
718
  id: changeAttrs.revisionId,
607
719
  author: changeAttrs.author || "Unknown",
608
- ...changeAttrs.date ? { date: changeAttrs.date } : {}
720
+ ...changeAttrs.date ? { date: changeAttrs.date } : {},
721
+ ...changeAttrs.initials ? { initials: changeAttrs.initials } : {}
609
722
  };
610
723
  if (insertionMark) content.push({
611
724
  type: changeAttrs.moveKind === "moveTo" ? "moveTo" : "insertion",
@@ -635,6 +748,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
635
748
  author: changeAttrs.author || "Unknown"
636
749
  };
637
750
  if (changeAttrs.date) info.date = changeAttrs.date;
751
+ if (changeAttrs.initials) info.initials = changeAttrs.initials;
638
752
  if (insertionMark) if (changeAttrs.moveKind === "moveTo") content.push({
639
753
  type: "moveTo",
640
754
  info,
@@ -1563,7 +1677,8 @@ function convertPMTableRow(node, documentCounts, activeVerticalMerges) {
1563
1677
  info: {
1564
1678
  id: attrs.trIns.revisionId,
1565
1679
  author: attrs.trIns.author,
1566
- ...attrs.trIns.date != null && { date: attrs.trIns.date }
1680
+ ...attrs.trIns.date != null && { date: attrs.trIns.date },
1681
+ ...attrs.trIns.initials != null && { initials: attrs.trIns.initials }
1567
1682
  }
1568
1683
  };
1569
1684
  else if (attrs.trDel) row.structuralChange = {
@@ -1571,7 +1686,8 @@ function convertPMTableRow(node, documentCounts, activeVerticalMerges) {
1571
1686
  info: {
1572
1687
  id: attrs.trDel.revisionId,
1573
1688
  author: attrs.trDel.author,
1574
- ...attrs.trDel.date != null && { date: attrs.trDel.date }
1689
+ ...attrs.trDel.date != null && { date: attrs.trDel.date },
1690
+ ...attrs.trDel.initials != null && { initials: attrs.trDel.initials }
1575
1691
  }
1576
1692
  };
1577
1693
  return row;
@@ -1652,7 +1768,8 @@ function convertPMTableCell(node, documentCounts) {
1652
1768
  const info = {
1653
1769
  id: attrs.cellMarker.info.revisionId,
1654
1770
  author: attrs.cellMarker.info.author,
1655
- ...attrs.cellMarker.info.date != null && { date: attrs.cellMarker.info.date }
1771
+ ...attrs.cellMarker.info.date != null && { date: attrs.cellMarker.info.date },
1772
+ ...attrs.cellMarker.info.initials != null && { initials: attrs.cellMarker.info.initials }
1656
1773
  };
1657
1774
  if (attrs.cellMarker.kind === "merge") cell.structuralChange = {
1658
1775
  type: "tableCellMerge",
@@ -1,4 +1,4 @@
1
1
  import { assertValidProseMirrorDocument, formatProseMirrorDocumentIssues, validateProseMirrorDocument } from "../validation.js";
2
- import { fromProseDoc, proseDocToBlocks, updateDocumentContent } from "./fromProseDoc.js";
3
2
  import { createEmptyDoc, headerFooterToProseDoc, toProseDoc } from "./toProseDoc.js";
3
+ import { fromProseDoc, proseDocToBlocks, updateDocumentContent } from "./fromProseDoc.js";
4
4
  export { assertValidProseMirrorDocument, createEmptyDoc, formatProseMirrorDocumentIssues, fromProseDoc, headerFooterToProseDoc, proseDocToBlocks, toProseDoc, updateDocumentContent, validateProseMirrorDocument };
@@ -4,17 +4,32 @@ import { setAutospacingBaseValue } from "../autospacingBase.js";
4
4
  import { assertValidProseMirrorDocument } from "../validation.js";
5
5
  import { shadingToRunShadingAttrs } from "./runShadingMark.js";
6
6
  import { sdtAttrsFromProperties } from "./sdtAttrs.js";
7
- import { marksToTextFormatting } from "./fromProseDoc.js";
8
7
  import { mergeTextFormatting } from "../../utils/textFormattingMerge.js";
9
8
  import { mergeParagraphFormatting, mergeParagraphTabStops } from "../../utils/paragraphFormattingMerge.js";
10
9
  import { createStyleEngine } from "../../style-engine/styleEngine.js";
11
10
  import { buildRunFormattingOverrideAttrs } from "../extensions/marks/RunFormattingOverrideExtension.js";
12
11
  import { schema } from "../schema/index.js";
13
12
  import { resolveEffectiveTableCellFormatting } from "./effectiveTableCellFormatting.js";
13
+ import { marksToTextFormatting } from "./fromProseDoc.js";
14
14
  //#region src/prosemirror/conversion/toProseDoc.ts
15
15
  const TOC_STYLE_ID = /^TOC\d*$/iu;
16
16
  const isTocStyleId = (styleId) => styleId !== void 0 && TOC_STYLE_ID.test(styleId);
17
17
  /**
18
+ * Build a `nextTextBoxGroupId()` generator salted with a random per-load
19
+ * nonce, so minted text-box anchor ids (`<salt>:<group>:<index>`) are unique
20
+ * across separate conversions instead of purely sequential ("0:0", "0:1",
21
+ * ...) starting fresh every load. `TextBoxAnchorExtension` parses
22
+ * `data-docx-textbox-anchor` straight off pasted DOM and `fromProseDoc`
23
+ * registers anchors first-seen-wins — without the salt, a pasted external
24
+ * span carrying a guessed/copied id could collide with and hijack a real
25
+ * text box anchored in a completely different document.
26
+ */
27
+ const createTextBoxGroupIdFactory = () => {
28
+ const salt = Math.random().toString(36).slice(2, 10);
29
+ let index = 0;
30
+ return () => `${salt}:${index++}`;
31
+ };
32
+ /**
18
33
  * Convert a Document to a ProseMirror document
19
34
  *
20
35
  * @param document - The Document to convert
@@ -25,8 +40,7 @@ function toProseDoc(document, options) {
25
40
  const nodes = [];
26
41
  const styleResolver = createStyleEngine(options?.styles ?? document.package.styles);
27
42
  const theme = options?.theme ?? document.package.theme ?? null;
28
- let textBoxGroupIndex = 0;
29
- const nextTextBoxGroupId = () => String(textBoxGroupIndex++);
43
+ const nextTextBoxGroupId = createTextBoxGroupIdFactory();
30
44
  const convertBodyBlocks = (blocks) => {
31
45
  const out = [];
32
46
  for (const block of blocks) if (block.type === "paragraph") {
@@ -244,6 +258,7 @@ function convertTrackedChange(change, markType, getInheritedRunFormatting, style
244
258
  revisionId: change.info.id,
245
259
  author: change.info.author,
246
260
  date: change.info.date ?? null,
261
+ initials: change.info.initials ?? null,
247
262
  moveKind
248
263
  });
249
264
  return nodes.map((node) => {
@@ -896,14 +911,12 @@ function convertTableCell({ cell, styleResolver, context, isHeader, gridWidthPer
896
911
  return schema.node(nodeType, attrs, contentNodes);
897
912
  }
898
913
  function standaloneTableCellToProseMirror(cell, nodeType) {
899
- let textBoxGroupIndex = 0;
900
- const nextTextBoxGroupId = () => String(textBoxGroupIndex++);
901
914
  return convertTableCell({
902
915
  cell,
903
916
  styleResolver: null,
904
917
  context: {
905
918
  theme: null,
906
- nextTextBoxGroupId
919
+ nextTextBoxGroupId: createTextBoxGroupIdFactory()
907
920
  },
908
921
  isHeader: nodeType === "tableHeader",
909
922
  gridWidthPercent: void 0,
@@ -1602,8 +1615,7 @@ function headerFooterToProseDoc(content, options) {
1602
1615
  const nodes = [];
1603
1616
  const styleResolver = options?.styles ? createStyleEngine(options.styles) : null;
1604
1617
  const theme = options?.theme ?? null;
1605
- let textBoxGroupIndex = 0;
1606
- const nextTextBoxGroupId = () => String(textBoxGroupIndex++);
1618
+ const nextTextBoxGroupId = createTextBoxGroupIdFactory();
1607
1619
  const convertBlocks = (blocks) => {
1608
1620
  const out = [];
1609
1621
  for (const block of blocks) if (block.type === "paragraph") out.push(...convertParagraphWithTextBoxes(block, styleResolver, {
@@ -237,7 +237,8 @@ const paragraphNodeSpec = {
237
237
  _autospacingBase: { default: null },
238
238
  _sectionProperties: { default: null },
239
239
  _propertyChanges: { default: null },
240
- pPrMark: { default: null }
240
+ pPrMark: { default: null },
241
+ _suggestedInsert: { default: null }
241
242
  },
242
243
  parseDOM: [{
243
244
  tag: "p",
@@ -1,4 +1,5 @@
1
1
  import { createExtension } from "../create.js";
2
+ import { sanitizeImageSrc } from "../../../utils/sanitizeImageSrc.js";
2
3
  import { getClipboardImageFiles } from "../../../utils/clipboard.js";
3
4
  import { isSafeImageFile } from "../../../utils/imageValidation.js";
4
5
  import { Plugin, TextSelection } from "prosemirror-state";
@@ -20,13 +21,18 @@ async function readFileAsDataUrl(file) {
20
21
  }
21
22
  async function loadImageSize(src) {
22
23
  return await new Promise((resolve, reject) => {
24
+ const safeSrc = sanitizeImageSrc(src);
25
+ if (!safeSrc) {
26
+ reject(/* @__PURE__ */ new Error("Rejected unsafe image source"));
27
+ return;
28
+ }
23
29
  const img = new Image();
24
30
  img.addEventListener("load", () => resolve({
25
31
  width: img.naturalWidth || 1,
26
32
  height: img.naturalHeight || 1
27
33
  }));
28
34
  img.addEventListener("error", () => reject(/* @__PURE__ */ new Error("Failed to load pasted image")));
29
- img.src = src;
35
+ img.src = safeSrc;
30
36
  });
31
37
  }
32
38
  async function insertImageFiles(view, files) {
@@ -37,7 +43,9 @@ async function insertImageFiles(view, files) {
37
43
  if (!await isSafeImageFile(file)) continue;
38
44
  let dataUrl;
39
45
  try {
40
- dataUrl = await readFileAsDataUrl(file);
46
+ const safeSrc = sanitizeImageSrc(await readFileAsDataUrl(file));
47
+ if (!safeSrc) continue;
48
+ dataUrl = safeSrc;
41
49
  } catch {
42
50
  continue;
43
51
  }
@@ -1,3 +1,4 @@
1
+ import { stripXmlDeclarations } from "../../../utils/stripXmlDeclarations.js";
1
2
  //#region src/prosemirror/extensions/features/pasteCleanup.ts
2
3
  /**
3
4
  * Office / web paste cleanup
@@ -112,11 +113,22 @@ function splitStyleDeclarations(value) {
112
113
  }
113
114
  const TAG_TAIL = `(?:"[^"]*"|'[^']*'|[^>"'])*`;
114
115
  const NAMESPACED_TAG = new RegExp(`<\\/?(?:[owvm]|st\\d+):${TAG_TAIL}>`, "gi");
115
- const XML_PROCESSING_INSTRUCTION = new RegExp(`<\\?xml${TAG_TAIL}>`, "gi");
116
116
  const STRAY_XML_TAG = new RegExp(`<\\/?xml${TAG_TAIL}>`, "gi");
117
117
  const NOISE_TAG = new RegExp(`<\\/?(?:font|meta|link)${TAG_TAIL}>`, "gi");
118
118
  const EMPTY_SPAN = new RegExp(`<span(?:\\s${TAG_TAIL})?><\\/span>`, "gi");
119
119
  const MAX_EMPTY_SPAN_PASSES = 5;
120
+ const PM_SLICE_MARKER = /\bdata-pm-slice\s*=/i;
121
+ const TEXTBOX_ANCHOR_ATTR = /\s+data-docx-textbox-anchor(?:="[^"]*"|='[^']*')?/gi;
122
+ /**
123
+ * Remove the `data-docx-textbox-anchor` marker from HTML that did not come
124
+ * from a ProseMirror clipboard slice (see {@link PM_SLICE_MARKER}). Internal
125
+ * copy/paste of a real text box carries the marker HTML unmodified so it
126
+ * keeps working; anything else has the marker stripped defensively.
127
+ */
128
+ function stripForeignTextBoxAnchors(html, originalHtml) {
129
+ if (PM_SLICE_MARKER.test(originalHtml)) return html;
130
+ return html.replace(TEXTBOX_ANCHOR_ATTR, "");
131
+ }
120
132
  /**
121
133
  * Remove empty spans left behind after stripping `mso-*` styles. Only truly
122
134
  * empty spans are removed (whitespace-only spans are kept so word-separating
@@ -144,12 +156,13 @@ function cleanPastedHtml(html) {
144
156
  if (!html) return html;
145
157
  try {
146
158
  let cleaned = stripHtmlComments(html);
147
- cleaned = cleaned.replace(XML_PROCESSING_INSTRUCTION, "");
159
+ cleaned = stripXmlDeclarations(cleaned);
148
160
  cleaned = cleaned.replace(NAMESPACED_TAG, "");
149
161
  cleaned = cleaned.replace(STRAY_XML_TAG, "");
150
162
  cleaned = cleaned.replace(NOISE_TAG, "");
151
163
  cleaned = stripMsoStyles(cleaned);
152
164
  cleaned = stripEmptySpans(cleaned);
165
+ cleaned = stripForeignTextBoxAnchors(cleaned, html);
153
166
  return cleaned.trim();
154
167
  } catch {
155
168
  return html;
@@ -1,11 +1,37 @@
1
1
  import { expectHyperlinkMarkAttrs } from "../../attrs/index.js";
2
2
  import { createMarkExtension } from "../create.js";
3
3
  import { isMarkActive } from "./markUtils.js";
4
+ import { normalizeUserUrl, sanitizeExternalUrl } from "../../../utils/urlSecurity.js";
4
5
  import { panic } from "better-result";
5
6
  //#region src/prosemirror/extensions/marks/HyperlinkExtension.ts
6
7
  /**
7
8
  * Hyperlink Mark Extension
8
9
  */
10
+ /**
11
+ * Sanitize an href arriving from (or being emitted to) the DOM: pasted HTML,
12
+ * clipboard content, or a mark attr already stored on the document. Only
13
+ * allow-listed absolute URLs (http/https/mailto/tel) and internal bookmark
14
+ * anchors survive; everything else (javascript:, data:, file:, ...) becomes
15
+ * an empty href, matching how the codebase already signals "no link" for an
16
+ * unresolved hyperlink (see `toProseDoc.ts`'s `hyperlink.href || ""`).
17
+ */
18
+ function sanitizeStoredHref(rawHref) {
19
+ if (!rawHref) return "";
20
+ const trimmed = rawHref.trim();
21
+ if (trimmed.startsWith("#")) return trimmed;
22
+ return sanitizeExternalUrl(trimmed) ?? "";
23
+ }
24
+ /**
25
+ * Normalize/sanitize a URL typed by a user (hyperlink dialog/popup) before it
26
+ * is written into a mark. Protocol-less input (e.g. "example.com") is
27
+ * accepted and normalized to https; anything resolving to a disallowed
28
+ * scheme is dropped.
29
+ */
30
+ function normalizeHyperlinkInput(rawHref) {
31
+ const trimmed = rawHref.trim();
32
+ if (trimmed.startsWith("#")) return trimmed;
33
+ return normalizeUserUrl(trimmed);
34
+ }
9
35
  function isHyperlinkActive(state) {
10
36
  const hlType = state.schema.marks["hyperlink"];
11
37
  if (!hlType) return false;
@@ -62,14 +88,14 @@ const HyperlinkExtension = createMarkExtension({
62
88
  parseDOM: [{
63
89
  tag: "a[href]",
64
90
  getAttrs: (dom) => ({
65
- href: dom.getAttribute("href") ?? "",
91
+ href: sanitizeStoredHref(dom.getAttribute("href") ?? void 0),
66
92
  tooltip: dom.getAttribute("title") ?? void 0
67
93
  })
68
94
  }],
69
95
  toDOM(mark) {
70
96
  const { href, tooltip } = expectHyperlinkMarkAttrs(mark);
71
97
  const domAttrs = {
72
- href,
98
+ href: sanitizeStoredHref(href),
73
99
  target: "_blank",
74
100
  rel: "noopener noreferrer"
75
101
  };
@@ -89,7 +115,7 @@ const HyperlinkExtension = createMarkExtension({
89
115
  if (empty) return false;
90
116
  if (dispatch) {
91
117
  const mark = hlType.create({
92
- href,
118
+ href: normalizeHyperlinkInput(href),
93
119
  tooltip: tooltip || null
94
120
  });
95
121
  let tr = state.tr.addMark(from, to, mark);
@@ -127,7 +153,7 @@ const HyperlinkExtension = createMarkExtension({
127
153
  const insertHyperlink = (text, href, tooltip) => (state, dispatch) => {
128
154
  if (dispatch) {
129
155
  const mark = hlType.create({
130
- href,
156
+ href: normalizeHyperlinkInput(href),
131
157
  tooltip: tooltip || null
132
158
  });
133
159
  const textNode = state.schema.text(text, [mark]);
@@ -18,6 +18,20 @@ import { AUTHOR_COLORS, getAuthorColorIdx } from "../../../utils/authorColors.js
18
18
  const insertionStyle = (color) => `color: ${color}; text-decoration: underline; text-decoration-color: ${color};`;
19
19
  const deletionStyle = (color) => `color: ${color}; text-decoration: line-through; text-decoration-color: ${color};`;
20
20
  /**
21
+ * Suggested (AI-proposed) tracked changes reuse the insertion/deletion visual
22
+ * grammar but with a DOTTED stroke and a dedicated suggestion hue, kept
23
+ * distinct from the per-author redline colors. The hue and tint are driven by
24
+ * the `--suggestion-color` / `--suggestion-bg` CSS custom properties (see
25
+ * editor.css); the inline fallbacks below keep the paged-canvas projection —
26
+ * which reuses this same `toDOM` style string — legible when the stylesheet
27
+ * variables are not in scope.
28
+ */
29
+ const SUGGESTION_COLOR = "var(--suggestion-color, #6d3bd6)";
30
+ const SUGGESTION_TINT = "var(--suggestion-bg, color-mix(in oklch, #6d3bd6 12%, transparent))";
31
+ const SUGGESTION_TINT_LAYER = `linear-gradient(${SUGGESTION_TINT}, ${SUGGESTION_TINT})`;
32
+ const suggestedInsertionStyle = () => `color: ${SUGGESTION_COLOR}; text-decoration: underline; text-decoration-style: dotted; text-decoration-color: ${SUGGESTION_COLOR}; background-image: ${SUGGESTION_TINT_LAYER};`;
33
+ const suggestedDeletionStyle = () => `color: ${SUGGESTION_COLOR}; text-decoration: line-through; text-decoration-style: dotted; text-decoration-color: ${SUGGESTION_COLOR}; background-image: ${SUGGESTION_TINT_LAYER};`;
34
+ /**
21
35
  * Insertion mark — text added in tracked changes
22
36
  * Renders with per-author colored underline.
23
37
  */
@@ -29,7 +43,10 @@ const InsertionExtension = createMarkExtension({
29
43
  revisionId: { default: 0 },
30
44
  author: { default: "" },
31
45
  date: { default: null },
32
- moveKind: { default: null }
46
+ moveKind: { default: null },
47
+ initials: { default: null },
48
+ provenance: { default: "user" },
49
+ suggestionId: { default: null }
33
50
  },
34
51
  inclusive: false,
35
52
  parseDOM: [{
@@ -43,20 +60,23 @@ const InsertionExtension = createMarkExtension({
43
60
  }
44
61
  }],
45
62
  toDOM(mark) {
46
- const { revisionId, author, date } = expectTrackedChangeMarkAttrs(mark);
63
+ const { revisionId, author, date, provenance, suggestionId } = expectTrackedChangeMarkAttrs(mark);
47
64
  const idx = getAuthorColorIdx(author);
48
65
  const color = AUTHOR_COLORS[idx] ?? "#000000";
49
66
  const titleParts = [author, date ? new Date(date).toLocaleDateString() : ""].filter(Boolean);
67
+ const suggested = provenance === "suggested";
50
68
  return [
51
69
  "span",
52
70
  {
53
- class: "docx-insertion",
71
+ class: suggested ? "docx-insertion docx-insertion--suggested" : "docx-insertion",
54
72
  "data-revision-id": String(revisionId),
55
73
  "data-author": author,
56
74
  "data-tc-author-idx": String(idx),
75
+ ...suggested ? { "data-provenance": "suggested" } : {},
76
+ ...suggested && suggestionId ? { "data-suggestion-id": suggestionId } : {},
57
77
  ...date ? { "data-date": date } : {},
58
- ...titleParts.length > 0 ? { title: `Inserted: ${titleParts.join(", ")}` } : {},
59
- style: insertionStyle(color)
78
+ ...titleParts.length > 0 ? { title: `${suggested ? "Suggested" : "Inserted"}: ${titleParts.join(", ")}` } : {},
79
+ style: suggested ? suggestedInsertionStyle() : insertionStyle(color)
60
80
  },
61
81
  0
62
82
  ];
@@ -75,7 +95,10 @@ const DeletionExtension = createMarkExtension({
75
95
  revisionId: { default: 0 },
76
96
  author: { default: "" },
77
97
  date: { default: null },
78
- moveKind: { default: null }
98
+ moveKind: { default: null },
99
+ initials: { default: null },
100
+ provenance: { default: "user" },
101
+ suggestionId: { default: null }
79
102
  },
80
103
  inclusive: false,
81
104
  parseDOM: [{
@@ -89,20 +112,23 @@ const DeletionExtension = createMarkExtension({
89
112
  }
90
113
  }],
91
114
  toDOM(mark) {
92
- const { revisionId, author, date } = expectTrackedChangeMarkAttrs(mark);
115
+ const { revisionId, author, date, provenance, suggestionId } = expectTrackedChangeMarkAttrs(mark);
93
116
  const idx = getAuthorColorIdx(author);
94
117
  const color = AUTHOR_COLORS[idx] ?? "#000000";
95
118
  const titleParts = [author, date ? new Date(date).toLocaleDateString() : ""].filter(Boolean);
119
+ const suggested = provenance === "suggested";
96
120
  return [
97
121
  "span",
98
122
  {
99
- class: "docx-deletion",
123
+ class: suggested ? "docx-deletion docx-deletion--suggested" : "docx-deletion",
100
124
  "data-revision-id": String(revisionId),
101
125
  "data-author": author,
102
126
  "data-tc-author-idx": String(idx),
127
+ ...suggested ? { "data-provenance": "suggested" } : {},
128
+ ...suggested && suggestionId ? { "data-suggestion-id": suggestionId } : {},
103
129
  ...date ? { "data-date": date } : {},
104
- ...titleParts.length > 0 ? { title: `Deleted: ${titleParts.join(", ")}` } : {},
105
- style: deletionStyle(color)
130
+ ...titleParts.length > 0 ? { title: `${suggested ? "Suggested deletion" : "Deleted"}: ${titleParts.join(", ")}` } : {},
131
+ style: suggested ? suggestedDeletionStyle() : deletionStyle(color)
106
132
  },
107
133
  0
108
134
  ];
@@ -120,13 +146,23 @@ const RunPropertyChangeExtension = createMarkExtension({
120
146
  name: "runPropertyChange",
121
147
  schemaMarkName: "runPropertyChange",
122
148
  markSpec: {
123
- attrs: { changes: { default: [] } },
149
+ attrs: {
150
+ changes: { default: [] },
151
+ provenance: { default: "user" },
152
+ suggestionId: { default: null }
153
+ },
124
154
  inclusive: false,
125
155
  parseDOM: [{ tag: "span.docx-run-property-change" }],
126
- toDOM() {
156
+ toDOM(mark) {
157
+ const suggested = mark.attrs["provenance"] === "suggested";
158
+ const suggestionId = mark.attrs["suggestionId"];
127
159
  return [
128
160
  "span",
129
- { class: "docx-run-property-change" },
161
+ {
162
+ class: suggested ? "docx-run-property-change docx-run-property-change--suggested" : "docx-run-property-change",
163
+ ...suggested ? { "data-provenance": "suggested" } : {},
164
+ ...suggested && typeof suggestionId === "string" ? { "data-suggestion-id": suggestionId } : {}
165
+ },
130
166
  0
131
167
  ];
132
168
  }
@@ -1,5 +1,5 @@
1
- import { AnyExtension, Extension, NodeExtension } from "../types.js";
2
1
  import { ColorValue } from "../../../types/colors.js";
2
+ import { AnyExtension, Extension, NodeExtension } from "../types.js";
3
3
  import { Command, EditorState } from "prosemirror-state";
4
4
  import { Node } from "prosemirror-model";
5
5
 
@@ -1,7 +1,7 @@
1
1
  import { TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_WIDTH_TYPE_VALUES } from "../../../types/documentEnumValues.js";
2
2
  import { expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, mergeTableAttrs, mergeTableCellAttrs, mergeTableRowAttrs } from "../../attrs/index.js";
3
3
  import { createExtension, createNodeExtension } from "../create.js";
4
- import { resolveColor } from "../../../utils/colorResolver.js";
4
+ import { isValidHexColor, resolveColor } from "../../../utils/colorResolver.js";
5
5
  import { panic } from "better-result";
6
6
  import { CellSelection, TableMap, columnResizing, mergeCells, removeRow, selectedRect, splitCell, tableEditing } from "prosemirror-tables";
7
7
  import { Plugin, PluginKey, Selection, TextSelection } from "prosemirror-state";
@@ -127,7 +127,8 @@ function parseCellAttrsFromDOM(element) {
127
127
  const margins = extractCellMarginsFromCSS(style);
128
128
  const rawValign = element.dataset["valign"];
129
129
  const verticalAlign = (rawValign === "top" || rawValign === "center" || rawValign === "bottom" ? rawValign : void 0) ?? mapCssVerticalAlign(style.verticalAlign) ?? void 0;
130
- const backgroundColor = element.dataset["bgcolor"] || parseCssColorToHex(style.backgroundColor) || void 0;
130
+ const rawBgColor = element.dataset["bgcolor"];
131
+ const backgroundColor = (isValidHexColor(rawBgColor) ? rawBgColor : void 0) || parseCssColorToHex(style.backgroundColor) || void 0;
131
132
  return {
132
133
  colspan: Number(element.getAttribute("colspan") ?? "1") || 1,
133
134
  rowspan: Number(element.getAttribute("rowspan") ?? "1") || 1,
@@ -154,7 +155,8 @@ const tableSpec = {
154
155
  borders: { default: null },
155
156
  _resolvedIndent: { default: null },
156
157
  _originalFormatting: { default: null },
157
- tblPrChange: { default: null }
158
+ tblPrChange: { default: null },
159
+ _suggestedInsert: { default: null }
158
160
  },
159
161
  parseDOM: [{
160
162
  tag: "table",
@@ -1,5 +1,5 @@
1
- import { ImageAttrs, ParagraphAttrs } from "./schema/nodes.js";
2
1
  import { FontFamilyAttrs, FontSizeAttrs, HyperlinkAttrs, TextColorAttrs, UnderlineAttrs } from "./schema/marks.js";
2
+ import { ImageAttrs, ParagraphAttrs } from "./schema/nodes.js";
3
3
  import { schema, singletonManager } from "./schema/index.js";
4
4
  import { PAINTABLE_MARK_NAMES, applyFormatMarks, captureFormatMarks } from "./commands/formatPainter.js";
5
5
  import { clearFormatting, getMarkAttr, isMarkActive } from "./extensions/marks/markUtils.js";
@@ -1,5 +1,4 @@
1
1
  import { assertValidProseMirrorDocument, formatProseMirrorDocumentIssues, validateProseMirrorDocument } from "./validation.js";
2
- import { fromProseDoc, updateDocumentContent } from "./conversion/fromProseDoc.js";
3
2
  import { StyleResolver, createStyleResolver } from "./styles/styleResolver.js";
4
3
  import { getParagraphAlignment, getParagraphBidi, getStyleId } from "./extensions/core/ParagraphExtension.js";
5
4
  import { createDocumentStylesPlugin, documentStylesKey, getDocumentStyleResolver } from "./plugins/documentStyles.js";
@@ -11,6 +10,7 @@ import { createSelectionTrackerPlugin, extractSelectionContext, getSelectionCont
11
10
  import { getHyperlinkAttrs, getSelectedText, isHyperlinkActive } from "./extensions/marks/HyperlinkExtension.js";
12
11
  import { schema, singletonManager } from "./schema/index.js";
13
12
  import { createEmptyDoc, toProseDoc } from "./conversion/toProseDoc.js";
13
+ import { fromProseDoc, updateDocumentContent } from "./conversion/fromProseDoc.js";
14
14
  import { addColumnLeft, addColumnRight, addRowAbove, addRowBelow, applyTableStyle, autoFitContents, deleteColumn, deleteRow, deleteTable, distributeColumns, insertTable, mergeCells, removeTableBorders, selectColumn, selectRow, selectTable, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setInsideTableBorders, setOutsideTableBorders, setRowHeight, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, splitCell, toggleHeaderRow, toggleNoWrap } from "./commands/table.js";
15
15
  import { extractSelectionState } from "./selectionState.js";
16
16
  import { clearFontFamily, clearFontSize, clearHighlight, clearTextColor, insertHyperlink, removeHyperlink, setFontFamily, setFontSize, setHighlight, setHyperlink, setTextColor, toggleBold, toggleItalic, toggleStrike, toggleSubscript, toggleSuperscript, toggleUnderline } from "./commands/formatting.js";
@@ -1,4 +1,4 @@
1
- import { ContentControlLockedError, ContentControlTypeError } from "../../content-controls/errors.js";
1
+ import { ContentControlBoundError, ContentControlLockedError, ContentControlTypeError } from "../../content-controls/errors.js";
2
2
  import { Plugin, PluginKey } from "prosemirror-state";
3
3
  import { EditorView } from "prosemirror-view";
4
4
 
@@ -33,7 +33,7 @@ type ContentControlWidgetEvent = {
33
33
  pmPos: number;
34
34
  sdtType: string;
35
35
  anchor: ContentControlWidgetAnchor;
36
- error: ContentControlLockedError | ContentControlTypeError;
36
+ error: ContentControlLockedError | ContentControlTypeError | ContentControlBoundError;
37
37
  };
38
38
  type ContentControlWidgetCallback = (event: ContentControlWidgetEvent) => void;
39
39
  type ContentControlWidgetClickEvent = {