@stll/folio-core 0.12.0 → 0.14.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 (138) 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/docxToMarkdown.d.ts +15 -0
  50. package/dist/docx/server/docxToMarkdown.js +17 -0
  51. package/dist/docx/server/extractDocxText.js +55 -8
  52. package/dist/docx/settingsParser.js +15 -2
  53. package/dist/docx/tableParser.d.ts +8 -1
  54. package/dist/docx/tableParser.js +29 -8
  55. package/dist/docx/unzip.js +1 -0
  56. package/dist/docx/vmlImageParser.js +11 -3
  57. package/dist/docx/xmlParser.js +11 -1
  58. package/dist/fonts/embeddedFonts.d.ts +33 -5
  59. package/dist/fonts/embeddedFonts.js +37 -8
  60. package/dist/index.d.ts +3 -3
  61. package/dist/index.js +3 -3
  62. package/dist/layout-bridge/convert/toFlowBlocks.js +15 -0
  63. package/dist/layout-engine/index.js +19 -12
  64. package/dist/layout-engine/measure/cache.js +2 -0
  65. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
  66. package/dist/layout-engine/measure/floatingTablePosition.d.ts +9 -0
  67. package/dist/layout-engine/measure/floatingTablePosition.js +17 -0
  68. package/dist/layout-engine/measure/index.d.ts +2 -1
  69. package/dist/layout-engine/measure/index.js +2 -1
  70. package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
  71. package/dist/layout-engine/measure/lineBreakProvider.js +3 -3
  72. package/dist/layout-engine/measure/measureBlocks.js +11 -8
  73. package/dist/layout-engine/measure/measureHelpers.d.ts +10 -1
  74. package/dist/layout-engine/measure/measureHelpers.js +12 -1
  75. package/dist/layout-engine/measure/measureParagraph.js +16 -0
  76. package/dist/layout-engine/types.d.ts +22 -2
  77. package/dist/layout-painter/renderImage.d.ts +12 -1
  78. package/dist/layout-painter/renderImage.js +21 -4
  79. package/dist/layout-painter/renderPage.js +5 -8
  80. package/dist/layout-painter/renderParagraph.js +33 -9
  81. package/dist/layout-painter/renderTable.js +2 -1
  82. package/dist/layout-painter/renderWatermark.d.ts +1 -1
  83. package/dist/layout-painter/renderWatermark.js +2 -1
  84. package/dist/markdown/fromMarkdown.js +1 -1
  85. package/dist/prosemirror/attrs/index.d.ts +1 -1
  86. package/dist/prosemirror/attrs/index.js +21 -0
  87. package/dist/prosemirror/commands/comments.d.ts +76 -1
  88. package/dist/prosemirror/commands/comments.js +401 -28
  89. package/dist/prosemirror/commands/hyperlink.js +15 -1
  90. package/dist/prosemirror/commands/image.js +7 -1
  91. package/dist/prosemirror/commands/index.d.ts +2 -2
  92. package/dist/prosemirror/commands/index.js +2 -2
  93. package/dist/prosemirror/commands/tableCellMergeResolution.js +1 -1
  94. package/dist/prosemirror/commentIdAllocator.js +13 -3
  95. package/dist/prosemirror/conversion/fromProseDoc.js +122 -5
  96. package/dist/prosemirror/conversion/index.js +1 -1
  97. package/dist/prosemirror/conversion/toProseDoc.js +20 -8
  98. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
  99. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
  100. package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
  101. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
  102. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +49 -13
  103. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  104. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -3
  105. package/dist/prosemirror/index.d.ts +1 -1
  106. package/dist/prosemirror/index.js +1 -1
  107. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +2 -2
  108. package/dist/prosemirror/plugins/contentControlWidgets.js +4 -4
  109. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
  110. package/dist/prosemirror/plugins/revisionIds.js +57 -0
  111. package/dist/prosemirror/plugins/suggestionMode.js +4 -3
  112. package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
  113. package/dist/prosemirror/runFormattingMarkNames.js +40 -0
  114. package/dist/prosemirror/schema/index.d.ts +1 -1
  115. package/dist/prosemirror/schema/marks.d.ts +27 -4
  116. package/dist/prosemirror/schema/marks.js +15 -0
  117. package/dist/prosemirror/schema/nodes.d.ts +50 -2
  118. package/dist/prosemirror/utils/tabCalculator.js +3 -3
  119. package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
  120. package/dist/server.d.ts +3 -2
  121. package/dist/server.js +4 -3
  122. package/dist/utils/clipboard.d.ts +10 -1
  123. package/dist/utils/clipboard.js +33 -6
  124. package/dist/utils/colorResolver.d.ts +8 -1
  125. package/dist/utils/colorResolver.js +35 -5
  126. package/dist/utils/fontResolver.d.ts +2 -1
  127. package/dist/utils/fontResolver.js +9 -3
  128. package/dist/utils/hexId.d.ts +11 -1
  129. package/dist/utils/hexId.js +11 -1
  130. package/dist/utils/sanitizeImageSrc.d.ts +21 -0
  131. package/dist/utils/sanitizeImageSrc.js +30 -0
  132. package/dist/utils/stripXmlDeclarations.d.ts +11 -0
  133. package/dist/utils/stripXmlDeclarations.js +35 -0
  134. package/dist/version-comparison.d.ts +22 -2
  135. package/dist/version-comparison.js +41 -12
  136. package/dist/watermark/index.d.ts +11 -1
  137. package/dist/watermark/index.js +22 -1
  138. package/package.json +3 -2
@@ -87,6 +87,14 @@ type RunFormatting = {
87
87
  changeAuthor?: string; /** Date of the tracked change */
88
88
  changeDate?: string; /** Revision ID of the tracked change (for sidebar matching) */
89
89
  changeRevisionId?: number;
90
+ /**
91
+ * Whether this tracked insertion/deletion is an AI-proposed *suggestion*
92
+ * rather than a user change. Suggestions render with the same grammar but a
93
+ * dotted stroke and a dedicated hue; they are stripped from serialized DOCX
94
+ * until accepted.
95
+ */
96
+ isSuggestion?: boolean; /** Groups a suggestion's runs for accept/reject and scroll-to. */
97
+ suggestionId?: string;
90
98
  };
91
99
  /**
92
100
  * Hyperlink information for a run.
@@ -179,7 +187,10 @@ type ImageRun = {
179
187
  cropTop?: number;
180
188
  cropRight?: number;
181
189
  cropBottom?: number;
182
- cropLeft?: number; /** Whether this picture is itself a tracked insertion (`<w:ins>`). eigenpal #641. */
190
+ cropLeft?: number; /** CSS border width in pixels. eigenpal #1096. */
191
+ borderWidth?: number; /** CSS border color. eigenpal #1096. */
192
+ borderColor?: string; /** CSS border style. eigenpal #1096. */
193
+ borderStyle?: string; /** Whether this picture is itself a tracked insertion (`<w:ins>`). eigenpal #641. */
183
194
  isInsertion?: boolean; /** Whether this picture is itself a tracked deletion (`<w:del>`). eigenpal #641. */
184
195
  isDeletion?: boolean; /** Author of the tracked change wrapping the picture. eigenpal #641. */
185
196
  changeAuthor?: string; /** Date of the tracked change wrapping the picture. eigenpal #641. */
@@ -377,6 +388,12 @@ type ParagraphAttrs = {
377
388
  listMarkerHidden?: boolean;
378
389
  listMarkerFontFamily?: string;
379
390
  listMarkerFontSize?: number;
391
+ /**
392
+ * Effective paragraph-mark size used only to measure a visible list
393
+ * paragraph's final line. Word lets the inherited complex-script size raise
394
+ * that line box without enlarging the marker or Western text glyphs.
395
+ */
396
+ listParagraphMarkFontSize?: number;
380
397
  listMarkerBold?: boolean; /** Horizontal alignment of the marker around the paragraph's list anchor. */
381
398
  listMarkerAlignment?: "left" | "center" | "right";
382
399
  /**
@@ -566,7 +583,10 @@ type ImageBlock = {
566
583
  cropTop?: number;
567
584
  cropRight?: number;
568
585
  cropBottom?: number;
569
- cropLeft?: number;
586
+ cropLeft?: number; /** CSS border width in pixels. eigenpal #1096. */
587
+ borderWidth?: number; /** CSS border color. eigenpal #1096. */
588
+ borderColor?: string; /** CSS border style. eigenpal #1096. */
589
+ borderStyle?: string;
570
590
  pmStart?: number;
571
591
  pmEnd?: number;
572
592
  };
@@ -21,6 +21,17 @@ type ImageVisualAttrs = {
21
21
  cropBottom?: number;
22
22
  cropLeft?: number;
23
23
  };
24
+ type ImageBorderAttrs = {
25
+ borderWidth?: number;
26
+ borderColor?: string;
27
+ borderStyle?: string;
28
+ };
29
+ /**
30
+ * Paint image borders carried through the layout model. Folio's PM schema
31
+ * uses `borderStyle` (not upstream's `borderKind`); the painter mirrors the
32
+ * editor DOM serialization while keeping the authored image box size stable.
33
+ */
34
+ declare function applyImageBorder(element: HTMLElement, border: ImageBorderAttrs): void;
24
35
  /**
25
36
  * True when any visual attribute is set. Cheap call-site guard so the no-op
26
37
  * common case skips the helper call.
@@ -86,4 +97,4 @@ type RenderImageFragmentOptions = {
86
97
  */
87
98
  declare function renderImageFragment(fragment: ImageFragment, block: ImageBlock, _measure: ImageMeasure, _context: RenderContext, options?: RenderImageFragmentOptions): HTMLElement;
88
99
  //#endregion
89
- export { IMAGE_CLASS_NAMES, ImageVisualAttrs, RenderImageFragmentOptions, applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs, renderImageFragment, wrapImageWithCrop };
100
+ export { IMAGE_CLASS_NAMES, ImageBorderAttrs, ImageVisualAttrs, RenderImageFragmentOptions, applyImageBorder, applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs, renderImageFragment, wrapImageWithCrop };
@@ -1,3 +1,5 @@
1
+ import { sanitizeExternalUrl } from "../utils/urlSecurity.js";
2
+ import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
1
3
  //#region src/layout-painter/renderImage.ts
2
4
  /**
3
5
  * CSS class names for image elements
@@ -7,6 +9,18 @@ const IMAGE_CLASS_NAMES = {
7
9
  imageAnchored: "layout-image-anchored"
8
10
  };
9
11
  /**
12
+ * Paint image borders carried through the layout model. Folio's PM schema
13
+ * uses `borderStyle` (not upstream's `borderKind`); the painter mirrors the
14
+ * editor DOM serialization while keeping the authored image box size stable.
15
+ */
16
+ function applyImageBorder(element, border) {
17
+ if (border.borderWidth == null || border.borderWidth <= 0) return;
18
+ const borderStyle = border.borderStyle || "solid";
19
+ const borderColor = border.borderColor || "#000000";
20
+ element.style.border = `${border.borderWidth}px ${borderStyle} ${borderColor}`;
21
+ element.style.boxSizing = "border-box";
22
+ }
23
+ /**
10
24
  * True when any visual attribute is set. Cheap call-site guard so the no-op
11
25
  * common case skips the helper call.
12
26
  *
@@ -118,7 +132,7 @@ function renderImageFragment(fragment, block, _measure, _context, options = {})
118
132
  if (fragment.pmStart !== void 0) containerEl.dataset["pmStart"] = String(fragment.pmStart);
119
133
  if (fragment.pmEnd !== void 0) containerEl.dataset["pmEnd"] = String(fragment.pmEnd);
120
134
  const imgEl = doc.createElement("img");
121
- imgEl.src = block.src;
135
+ applySanitizedImageSrc(imgEl, block.src);
122
136
  imgEl.alt = block.alt ?? "";
123
137
  imgEl.style.width = "100%";
124
138
  imgEl.style.height = "100%";
@@ -127,9 +141,10 @@ function renderImageFragment(fragment, block, _measure, _context, options = {})
127
141
  if (block.transform) imgEl.style.transform = block.transform;
128
142
  if (hasImageVisualAttrs(block)) applyImageVisualAttrs(imgEl, block);
129
143
  imgEl.draggable = false;
130
- if (block.hlinkHref) {
144
+ const hlinkHref = sanitizeExternalUrl(block.hlinkHref);
145
+ if (hlinkHref) {
131
146
  const linkEl = doc.createElement("a");
132
- linkEl.href = block.hlinkHref;
147
+ linkEl.href = hlinkHref;
133
148
  linkEl.target = "_blank";
134
149
  linkEl.rel = "noopener noreferrer";
135
150
  linkEl.style.display = "block";
@@ -138,7 +153,9 @@ function renderImageFragment(fragment, block, _measure, _context, options = {})
138
153
  linkEl.append(imgEl);
139
154
  containerEl.append(linkEl);
140
155
  } else containerEl.append(imgEl);
156
+ if (hasImageCrop(block)) applyImageBorder(containerEl, block);
157
+ else applyImageBorder(imgEl, block);
141
158
  return containerEl;
142
159
  }
143
160
  //#endregion
144
- export { IMAGE_CLASS_NAMES, applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs, renderImageFragment, wrapImageWithCrop };
161
+ export { IMAGE_CLASS_NAMES, applyImageBorder, applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs, renderImageFragment, wrapImageWithCrop };
@@ -1,9 +1,11 @@
1
1
  import { eighthsToPixels, pointsToPixels } from "../utils/units.js";
2
2
  import { resolveFontFamily } from "../utils/fontResolver.js";
3
3
  import { borderToStyle } from "../utils/formatToStyle.js";
4
+ import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
4
5
  import { floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun } from "../layout-engine/types.js";
5
6
  import { rectsToFloatingZones } from "../layout-engine/measure/floatingZones.js";
6
7
  import { measureParagraph } from "../layout-engine/measure/measureParagraph.js";
8
+ import { resolveFloatingTableX } from "../layout-engine/measure/floatingTablePosition.js";
7
9
  import { emuToPixels } from "./renderUtils.js";
8
10
  import { resolveAnchoredImagePosition as resolveAnchoredImagePosition$1 } from "./anchoredImagePosition.js";
9
11
  import { renderFragment } from "./renderFragment.js";
@@ -239,12 +241,7 @@ function resolveHeaderFooterFloatingTablePosition(floating, measure, layout, sou
239
241
  const horzFrameOffset = horzAnchor === "page" ? -layout.flowLeft : 0;
240
242
  const vertFrameHeight = vertAnchor === "page" ? layout.pageHeight : layout.pageHeight - layout.margins.top - layout.margins.bottom;
241
243
  const vertFrameOffset = vertAnchor === "page" ? -layout.flowTop : layout.margins.top - layout.flowTop;
242
- let left = 0;
243
- const xSpec = floating.tblpXSpec;
244
- if (xSpec === "left" || xSpec === "inside") left = horzFrameOffset;
245
- else if (xSpec === "right" || xSpec === "outside") left = horzFrameOffset + horzFrameWidth - measure.totalWidth;
246
- else if (xSpec === "center") left = horzFrameOffset + (horzFrameWidth - measure.totalWidth) / 2;
247
- else if (floating.tblpX !== void 0) left = horzFrameOffset + floating.tblpX;
244
+ const left = horzFrameOffset + resolveFloatingTableX(floating, void 0, measure.totalWidth, horzFrameWidth);
248
245
  let top = sourceY;
249
246
  if (floating.tblpYSpec === "top") top = vertFrameOffset;
250
247
  else if (floating.tblpYSpec === "bottom") top = vertFrameOffset + vertFrameHeight - measure.totalHeight;
@@ -380,7 +377,7 @@ function renderFloatingImagesLayer(floatingImages, doc, layerMode = "front") {
380
377
  if (floatImg.pmStart !== void 0) container.dataset["pmStart"] = String(floatImg.pmStart);
381
378
  if (floatImg.pmEnd !== void 0) container.dataset["pmEnd"] = String(floatImg.pmEnd);
382
379
  const img = doc.createElement("img");
383
- img.src = floatImg.src;
380
+ applySanitizedImageSrc(img, floatImg.src);
384
381
  img.style.width = `${floatImg.width}px`;
385
382
  img.style.height = `${floatImg.height}px`;
386
383
  img.style.display = "block";
@@ -509,7 +506,7 @@ function renderHeaderFooterContent(content, context, options, layout) {
509
506
  }
510
507
  for (const floatImg of floatingImages) {
511
508
  const img = doc.createElement("img");
512
- img.src = floatImg.src;
509
+ applySanitizedImageSrc(img, floatImg.src);
513
510
  img.width = floatImg.width;
514
511
  img.height = floatImg.height;
515
512
  if (floatImg.alt) img.alt = floatImg.alt;
@@ -1,6 +1,7 @@
1
1
  import { parseXmlDocument } from "../docx/xmlParser.js";
2
2
  import { resolveFontFamily } from "../utils/fontResolver.js";
3
3
  import { detectBaseDirection } from "../utils/baseDirection.js";
4
+ import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
4
5
  import { AUTHOR_COLORS, getAuthorColorIdx } from "../utils/authorColors.js";
5
6
  import { FONT_KERNING_MODE, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode, toPaintedText } from "../layout-engine/measure/textMeasurementPolicy.js";
6
7
  import "../utils/fontWeights.js";
@@ -12,7 +13,7 @@ import { isFloatingImageRun } from "../layout-engine/types.js";
12
13
  import { getListMarkerInlineWidth, getListMarkerVisualOffset } from "../layout-engine/measure/listMarkerWidth.js";
13
14
  import { resolveImageLineAlign } from "./renderUtils.js";
14
15
  import { applySdtDataAttrs } from "./sdtBoundary.js";
15
- import { applyImageVisualAttrs, hasImageVisualAttrs, wrapImageWithCrop } from "./renderImage.js";
16
+ import { applyImageBorder, applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs, wrapImageWithCrop } from "./renderImage.js";
16
17
  import { ommlToMathml } from "../docx/mathToMathml.js";
17
18
  import { evaluateFieldInstruction } from "../fields/evaluateField.js";
18
19
  import { borderStrokeToCss, resolveParagraphBorderHorizontalOutsets } from "./borderStroke.js";
@@ -75,6 +76,9 @@ function isMathRun(run) {
75
76
  const AUTOMATIC_TEXT_COLOR_VALUES = /* @__PURE__ */ new Set(["auto", "windowtext"]);
76
77
  const DEFAULT_BLACK_TEXT_COLOR_VALUES = /* @__PURE__ */ new Set(["000000", "000"]);
77
78
  const DOCX_SUPERSCRIPT_SCALE = .75;
79
+ const SUGGESTION_COLOR_CSS = "var(--suggestion-color, #6d3bd6)";
80
+ const SUGGESTION_TINT_CSS = "var(--suggestion-bg, color-mix(in oklch, #6d3bd6 12%, transparent))";
81
+ const SUGGESTION_TINT_LAYER_CSS = `linear-gradient(${SUGGESTION_TINT_CSS}, ${SUGGESTION_TINT_CSS})`;
78
82
  function normalizeTextColorValue(color) {
79
83
  return color.trim().toLowerCase().replace(/^#/u, "");
80
84
  }
@@ -192,13 +196,21 @@ function applyRunStyles(element, run) {
192
196
  if (run.isInsertion) {
193
197
  const authorIdx = getAuthorColorIdx(run.changeAuthor ?? "");
194
198
  const authorColor = AUTHOR_COLORS[authorIdx];
195
- element.style.color = authorColor;
199
+ const strokeColor = run.isSuggestion ? SUGGESTION_COLOR_CSS : authorColor;
200
+ element.style.color = strokeColor;
196
201
  if (!decorations.includes("underline")) decorations.push("underline");
197
- element.style.textDecorationColor = authorColor;
202
+ element.style.textDecorationColor = strokeColor;
198
203
  element.classList.add("docx-insertion");
199
204
  element.dataset["tcAuthorIdx"] = String(authorIdx);
205
+ if (run.isSuggestion) {
206
+ element.classList.add("docx-insertion--suggested");
207
+ element.style.textDecorationStyle = "dotted";
208
+ element.style.backgroundImage = SUGGESTION_TINT_LAYER_CSS;
209
+ element.dataset["provenance"] = "suggested";
210
+ if (run.suggestionId) element.dataset["suggestionId"] = run.suggestionId;
211
+ }
200
212
  const insertionParts = [run.changeAuthor, run.changeDate ? new Date(run.changeDate).toLocaleDateString() : ""].filter(Boolean);
201
- if (insertionParts.length > 0) element.title = `Inserted: ${insertionParts.join(", ")}`;
213
+ if (insertionParts.length > 0) element.title = `${run.isSuggestion ? "Suggested" : "Inserted"}: ${insertionParts.join(", ")}`;
202
214
  if (run.changeAuthor) element.dataset["changeAuthor"] = run.changeAuthor;
203
215
  if (run.changeDate) element.dataset["changeDate"] = run.changeDate;
204
216
  if (run.changeRevisionId !== void 0) element.dataset["revisionId"] = String(run.changeRevisionId);
@@ -206,13 +218,21 @@ function applyRunStyles(element, run) {
206
218
  if (run.isDeletion) {
207
219
  const authorIdx = getAuthorColorIdx(run.changeAuthor ?? "");
208
220
  const authorColor = AUTHOR_COLORS[authorIdx];
209
- element.style.color = authorColor;
221
+ const strokeColor = run.isSuggestion ? SUGGESTION_COLOR_CSS : authorColor;
222
+ element.style.color = strokeColor;
210
223
  if (!decorations.includes("line-through")) decorations.push("line-through");
211
- element.style.textDecorationColor = authorColor;
224
+ element.style.textDecorationColor = strokeColor;
212
225
  element.classList.add("docx-deletion");
213
226
  element.dataset["tcAuthorIdx"] = String(authorIdx);
227
+ if (run.isSuggestion) {
228
+ element.classList.add("docx-deletion--suggested");
229
+ element.style.textDecorationStyle = "dotted";
230
+ element.style.backgroundImage = SUGGESTION_TINT_LAYER_CSS;
231
+ element.dataset["provenance"] = "suggested";
232
+ if (run.suggestionId) element.dataset["suggestionId"] = run.suggestionId;
233
+ }
214
234
  const deletionParts = [run.changeAuthor, run.changeDate ? new Date(run.changeDate).toLocaleDateString() : ""].filter(Boolean);
215
- if (deletionParts.length > 0) element.title = `Deleted: ${deletionParts.join(", ")}`;
235
+ if (deletionParts.length > 0) element.title = `${run.isSuggestion ? "Suggested deletion" : "Deleted"}: ${deletionParts.join(", ")}`;
216
236
  if (run.changeAuthor) element.dataset["changeAuthor"] = run.changeAuthor;
217
237
  if (run.changeDate) element.dataset["changeDate"] = run.changeDate;
218
238
  if (run.changeRevisionId !== void 0) element.dataset["revisionId"] = String(run.changeRevisionId);
@@ -368,7 +388,7 @@ function getLeaderChar(leader) {
368
388
  function renderInlineImageRun(run, doc) {
369
389
  const img = doc.createElement("img");
370
390
  img.className = `${PARAGRAPH_CLASS_NAMES.run} ${PARAGRAPH_CLASS_NAMES.image}`;
371
- img.src = run.src;
391
+ applySanitizedImageSrc(img, run.src);
372
392
  img.width = run.width;
373
393
  img.height = run.height;
374
394
  img.style.width = `${run.width}px`;
@@ -378,6 +398,7 @@ function renderInlineImageRun(run, doc) {
378
398
  img.style.transform = run.transform;
379
399
  img.style.transformOrigin = "center center";
380
400
  }
401
+ if (!hasImageCrop(run)) applyImageBorder(img, run);
381
402
  const rotation = parseRotationDegrees(run.transform);
382
403
  if (rotation !== 0) {
383
404
  const bbox = rotatedBoundingBox(run.width, run.height, rotation);
@@ -407,6 +428,7 @@ function renderInlineImageRun(run, doc) {
407
428
  wrapper.style.verticalAlign = "middle";
408
429
  if (run.distTop) wrapper.style.marginTop = `${run.distTop}px`;
409
430
  if (run.distBottom) wrapper.style.marginBottom = `${run.distBottom}px`;
431
+ if (hasImageCrop(run)) applyImageBorder(wrapper, run);
410
432
  applyPmPositions(wrapper, run.pmStart, run.pmEnd);
411
433
  return wrapper;
412
434
  }
@@ -434,7 +456,7 @@ function renderBlockImage(run, doc) {
434
456
  container.style.marginTop = `${run.distTop ?? 6}px`;
435
457
  container.style.marginBottom = `${run.distBottom ?? 6}px`;
436
458
  const img = doc.createElement("img");
437
- img.src = run.src;
459
+ applySanitizedImageSrc(img, run.src);
438
460
  img.width = run.width;
439
461
  img.height = run.height;
440
462
  if (run.alt) img.alt = run.alt;
@@ -442,6 +464,7 @@ function renderBlockImage(run, doc) {
442
464
  img.style.transform = run.transform;
443
465
  img.style.transformOrigin = "center center";
444
466
  }
467
+ if (!hasImageCrop(run)) applyImageBorder(img, run);
445
468
  const rotation = parseRotationDegrees(run.transform);
446
469
  if (rotation !== 0) {
447
470
  const bbox = rotatedBoundingBox(run.width, run.height, rotation);
@@ -463,6 +486,7 @@ function renderBlockImage(run, doc) {
463
486
  widthPx: run.width,
464
487
  heightPx: run.height
465
488
  });
489
+ if (hasImageCrop(run)) applyImageBorder(wrapper, run);
466
490
  applyPmPositions(container, run.pmStart, run.pmEnd);
467
491
  container.append(wrapper);
468
492
  return container;
@@ -1,4 +1,5 @@
1
1
  import { emuToPixels } from "../utils/units.js";
2
+ import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
2
3
  import { getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, tableColumnsArePinned } from "../layout-engine/types.js";
3
4
  import { measureParagraph } from "../layout-engine/measure/measureParagraph.js";
4
5
  import { resolveAnchoredImagePosition } from "./anchoredImagePosition.js";
@@ -70,7 +71,7 @@ function renderCellContent({ cell, cellMeasure, context, doc, contentWidthOverri
70
71
  if (img.pmStart !== void 0) imgContainer.dataset["pmStart"] = String(img.pmStart);
71
72
  if (img.pmEnd !== void 0) imgContainer.dataset["pmEnd"] = String(img.pmEnd);
72
73
  const imgEl = doc.createElement("img");
73
- imgEl.src = img.src;
74
+ applySanitizedImageSrc(imgEl, img.src);
74
75
  imgEl.style.width = `${img.width}px`;
75
76
  imgEl.style.height = `${img.height}px`;
76
77
  imgEl.style.display = "block";
@@ -3,7 +3,7 @@ import { Page } from "../layout-engine/types.js";
3
3
 
4
4
  //#region src/layout-painter/renderWatermark.d.ts
5
5
  type RenderWatermarkOptions = {
6
- /** Resolved image src for picture watermarks (data URL or http URL). */imageSrc?: string;
6
+ /** Resolved image src for picture watermarks (`data:` / `blob:` only). */imageSrc?: string;
7
7
  };
8
8
  /**
9
9
  * Build the watermark overlay element for a page. Returns `null` when
@@ -1,4 +1,5 @@
1
1
  import { resolveFontFamily } from "../utils/fontResolver.js";
2
+ import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
2
3
  //#region src/layout-painter/renderWatermark.ts
3
4
  const WATERMARK_CLASS = "layout-page-watermark";
4
5
  /**
@@ -47,7 +48,7 @@ function renderTextWatermark(watermark, doc) {
47
48
  }
48
49
  function renderPictureWatermark(watermark, imageSrc, doc) {
49
50
  const img = doc.createElement("img");
50
- img.src = imageSrc;
51
+ applySanitizedImageSrc(img, imageSrc);
51
52
  img.alt = "";
52
53
  img.setAttribute("aria-hidden", "true");
53
54
  const scalePct = (watermark.scale ?? 1) * 100;
@@ -1,5 +1,5 @@
1
- import { createEmptyDocument } from "../utils/createDocument.js";
2
1
  import { sanitizeExternalUrl } from "../utils/urlSecurity.js";
2
+ import { createEmptyDocument } from "../utils/createDocument.js";
3
3
  import { marked } from "marked";
4
4
  //#region src/markdown/fromMarkdown.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { BlockSdtAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, MathAttrs, ParagraphAttrs, SdtAttrs, ShapeAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAttrs } from "../schema/nodes.js";
2
1
  import { CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, LanguageAttrs, RunFormattingOverrideAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, UnderlineAttrs } from "../schema/marks.js";
2
+ import { BlockSdtAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, MathAttrs, ParagraphAttrs, SdtAttrs, ShapeAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAttrs } from "../schema/nodes.js";
3
3
  import { Mark, Node } from "prosemirror-model";
4
4
 
5
5
  //#region src/prosemirror/attrs/index.d.ts
@@ -1,5 +1,6 @@
1
1
  import { FIELD_TYPE_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LINE_SPACING_RULE_VALUES, OUTLINE_STYLE_ATTR_VALUES, PARAGRAPH_ALIGNMENT_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_EFFECT_VALUES, THEME_COLOR_SLOT_VALUES, UNDERLINE_STYLE_VALUES } from "../../types/documentEnumValues.js";
2
2
  import { isParagraphDirection } from "../paragraphDirection.js";
3
+ import { TRACKED_CHANGE_PROVENANCE_VALUES } from "../schema/marks.js";
3
4
  import { panic } from "better-result";
4
5
  //#region src/prosemirror/attrs/index.ts
5
6
  const SECTION_BREAK_TYPES = [
@@ -600,7 +601,10 @@ const readTrackedChangeMarkAttrs = (mark) => {
600
601
  requiredNumber(attrs, "revisionId", `${mark.type.name}.attrs.revisionId`, issues);
601
602
  requiredString(attrs, "author", `${mark.type.name}.attrs.author`, issues);
602
603
  optionalString(attrs, "date", `${mark.type.name}.attrs.date`, issues);
604
+ optionalString(attrs, "initials", `${mark.type.name}.attrs.initials`, issues);
603
605
  optionalOneOf(attrs, "moveKind", `${mark.type.name}.attrs.moveKind`, issues, TRACKED_CHANGE_MOVE_KINDS);
606
+ optionalOneOf(attrs, "provenance", `${mark.type.name}.attrs.provenance`, issues, TRACKED_CHANGE_PROVENANCE_VALUES);
607
+ optionalString(attrs, "suggestionId", `${mark.type.name}.attrs.suggestionId`, issues);
604
608
  return attrsResult(attrs, issues);
605
609
  };
606
610
  const expectTrackedChangeMarkAttrs = (mark) => expectCachedMarkAttrs(mark, trackedChangeAttrsCache, readTrackedChangeMarkAttrs, "tracked change attrs");
@@ -609,6 +613,8 @@ const readRunPropertyChangeMarkAttrs = (mark) => {
609
613
  const issues = [];
610
614
  expectMarkType(mark, "runPropertyChange", issues);
611
615
  optionalPropertyChanges(attrs, "changes", "runPropertyChange.attrs.changes", issues, ["runPropertyChange"]);
616
+ optionalOneOf(attrs, "provenance", "runPropertyChange.attrs.provenance", issues, TRACKED_CHANGE_PROVENANCE_VALUES);
617
+ optionalString(attrs, "suggestionId", "runPropertyChange.attrs.suggestionId", issues);
612
618
  return attrsResult(attrs, issues);
613
619
  };
614
620
  const expectRunPropertyChangeMarkAttrs = (mark) => expectCachedMarkAttrs(mark, runPropertyChangeAttrsCache, readRunPropertyChangeMarkAttrs, "run property change attrs");
@@ -934,6 +940,9 @@ const optionalTableRowRevision = (attrs, key, issues) => {
934
940
  requiredNumber(value, "revisionId", `${path}.revisionId`, issues);
935
941
  requiredString(value, "author", `${path}.author`, issues);
936
942
  optionalString(value, "date", `${path}.date`, issues);
943
+ optionalString(value, "initials", `${path}.initials`, issues);
944
+ optionalOneOf(value, "provenance", `${path}.provenance`, issues, TRACKED_CHANGE_PROVENANCE_VALUES);
945
+ optionalString(value, "suggestionId", `${path}.suggestionId`, issues);
937
946
  };
938
947
  const optionalTableCellRevision = (attrs, issues) => {
939
948
  const value = attrs["cellMarker"];
@@ -962,9 +971,21 @@ const optionalTableCellRevision = (attrs, issues) => {
962
971
  requiredNumber(info, "revisionId", `${path}.info.revisionId`, issues);
963
972
  requiredString(info, "author", `${path}.info.author`, issues);
964
973
  optionalString(info, "date", `${path}.info.date`, issues);
974
+ optionalString(info, "initials", `${path}.info.initials`, issues);
965
975
  if (value["kind"] === "merge") {
976
+ if (info["provenance"] !== void 0) issues.push({
977
+ path: `${path}.info.provenance`,
978
+ message: "Not allowed on a merge marker."
979
+ });
980
+ if (info["suggestionId"] !== void 0) issues.push({
981
+ path: `${path}.info.suggestionId`,
982
+ message: "Not allowed on a merge marker."
983
+ });
966
984
  optionalOneOf(value, "verticalMerge", `${path}.verticalMerge`, issues, ["continue", "rest"]);
967
985
  optionalOneOf(value, "verticalMergeOriginal", `${path}.verticalMergeOriginal`, issues, ["continue", "rest"]);
986
+ } else {
987
+ optionalOneOf(info, "provenance", `${path}.info.provenance`, issues, TRACKED_CHANGE_PROVENANCE_VALUES);
988
+ optionalString(info, "suggestionId", `${path}.info.suggestionId`, issues);
968
989
  }
969
990
  };
970
991
  const optionalTextBoxTrackedChange = (attrs, issues) => {
@@ -62,6 +62,81 @@ declare function acceptAIEditRevision(revisionIds: number | readonly number[]):
62
62
  * See {@link acceptAIEditRevision} for the id semantics.
63
63
  */
64
64
  declare function rejectAIEditRevision(revisionIds: number | readonly number[]): Command;
65
+ type SuggestionKind = "insertion" | "deletion" | "formatting" | "insertBlock" | "insertTable" | "insertRow" | "deleteRow" | "insertColumn" | "deleteColumn";
66
+ /**
67
+ * How a suggestion is applied when accepted:
68
+ * - `"tracked"` — converts to a normal OOXML tracked change (`w:ins`/`w:del`,
69
+ * paragraph-mark `w:ins`, row/cell `w:ins`/`w:del`);
70
+ * - `"direct"` — no OOXML tracked representation exists (a whole inserted
71
+ * table), so accepting applies it directly;
72
+ * - `"mixed"` — a heterogeneous group (one `suggestionId` spanning a whole
73
+ * inserted table AND other edits) whose parts apply both ways. Accept still
74
+ * resolves the whole group in one transaction; this flag lets the host
75
+ * message that some parts landed directly.
76
+ */
77
+ type SuggestionAppliedAs = "tracked" | "direct" | "mixed";
78
+ type FolioSuggestion = {
79
+ suggestionId: string; /** Contiguous document ranges the suggestion covers, in document order. */
80
+ ranges: readonly {
81
+ from: number;
82
+ to: number;
83
+ }[]; /** Which kinds of change the suggestion contains (deduped, in a stable order). */
84
+ kinds: readonly SuggestionKind[]; /** How accepting this suggestion applies it (see {@link SuggestionAppliedAs}). */
85
+ appliedAs: SuggestionAppliedAs;
86
+ };
87
+ /**
88
+ * List every suggestion in the document for host consumption: its id, the
89
+ * contiguous ranges it covers, which kinds of change it contains, and how
90
+ * accepting it applies (tracked vs direct).
91
+ */
92
+ declare function getSuggestions(state: EditorState): FolioSuggestion[];
93
+ /**
94
+ * The document range covering every mark belonging to `suggestionId`, plus the
95
+ * revision ids those marks carry. Returns null when the suggestion is absent
96
+ * (already accepted/rejected, or never existed).
97
+ */
98
+ declare function findSuggestionRange(state: EditorState, suggestionId: string): {
99
+ from: number;
100
+ to: number;
101
+ revisionIds: number[];
102
+ } | null;
103
+ type AcceptSuggestionOptions = {
104
+ author: string;
105
+ /**
106
+ * Date stamped on the resulting user tracked change (serialized as `w:date`);
107
+ * defaults to now. A malformed / non-parseable value is normalized to now, and
108
+ * a valid value is canonicalized to ISO 8601, so an invalid date can never be
109
+ * written into the document.
110
+ */
111
+ date?: string;
112
+ };
113
+ /**
114
+ * Accept one suggestion: convert its inline marks and block/table node
115
+ * revisions into normal tracked changes authored by `author`, keeping revision
116
+ * ids. A whole inserted table (which OOXML cannot track) is applied directly;
117
+ * everything else becomes a tracked change. See {@link getSuggestions} for the
118
+ * per-suggestion `appliedAs`.
119
+ */
120
+ declare function acceptSuggestion(suggestionId: string, options: AcceptSuggestionOptions): Command;
121
+ /** Accept every suggestion in the document. */
122
+ declare function acceptAllSuggestions(options: AcceptSuggestionOptions): Command;
123
+ /**
124
+ * Reject one suggestion: inverse-apply it in a single transaction. Runs BOTH
125
+ * phases so a heterogeneous group (one `suggestionId` spanning whole-node
126
+ * inserts AND other edits) is fully resolved:
127
+ * 1. inline marks and suggested `trIns`/`trDel`/`cellMarker` revisions are
128
+ * inverse-applied through {@link resolveChange} (delete suggested-inserted
129
+ * text/rows/cells, drop suggested deletions/formatting);
130
+ * 2. any remaining whole-node inserts (paragraph/table) are then deleted,
131
+ * mapped through the accumulated transaction so positions stay valid.
132
+ *
133
+ * A paragraph whole-node insert carries inline marks with the same revision id,
134
+ * so phase 1 empties its text and phase 2 removes the now-empty node; a whole
135
+ * inserted table has no matching revision, so only phase 2 removes it.
136
+ */
137
+ declare function rejectSuggestion(suggestionId: string): Command;
138
+ /** Reject every suggestion in the document. */
139
+ declare function rejectAllSuggestions(): Command;
65
140
  type ChangeRange = {
66
141
  from: number;
67
142
  to: number;
@@ -90,4 +165,4 @@ declare function findNextChange(state: EditorState, startPos: number): ChangeRan
90
165
  */
91
166
  declare function findPreviousChange(state: EditorState, startPos: number): ChangeRange | null;
92
167
  //#endregion
93
- export { ParagraphBoundaryChange, acceptAIEditRevision, acceptAllChanges, acceptChange, addCommentMark, findAIEditRevisionRange, findChangeAtPosition, findNextChange, findParagraphBoundaryChangeAtPosition, findPreviousChange, rejectAIEditRevision, rejectAllChanges, rejectChange, removeCommentMark };
168
+ export { AcceptSuggestionOptions, FolioSuggestion, ParagraphBoundaryChange, SuggestionAppliedAs, SuggestionKind, acceptAIEditRevision, acceptAllChanges, acceptAllSuggestions, acceptChange, acceptSuggestion, addCommentMark, findAIEditRevisionRange, findChangeAtPosition, findNextChange, findParagraphBoundaryChangeAtPosition, findPreviousChange, findSuggestionRange, getSuggestions, rejectAIEditRevision, rejectAllChanges, rejectAllSuggestions, rejectChange, rejectSuggestion, removeCommentMark };