@stll/folio-core 0.48.0 → 0.49.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 (86) hide show
  1. package/dist/ai-edits/apply.js +565 -121
  2. package/dist/ai-edits/minimal-replacement.d.ts +64 -0
  3. package/dist/ai-edits/minimal-replacement.js +282 -0
  4. package/dist/controller/layoutPipeline.js +11 -1
  5. package/dist/display-list/build/paragraphPrimitives.js +63 -7
  6. package/dist/display-list/build/textBoxPrimitives.js +1 -1
  7. package/dist/display-list/build/unsupported.d.ts +2 -2
  8. package/dist/display-list/build/unsupported.js +0 -0
  9. package/dist/display-list/dom/renderDisplayListToDom.js +0 -1
  10. package/dist/docx/blockContentParser.js +2 -2
  11. package/dist/docx/bulletMarkers.d.ts +18 -2
  12. package/dist/docx/bulletMarkers.js +27 -2
  13. package/dist/docx/drawingGroupChildren.d.ts +60 -0
  14. package/dist/docx/drawingGroupChildren.js +176 -0
  15. package/dist/docx/drawingUtils.js +3 -0
  16. package/dist/docx/groupDrawingParser.d.ts +7 -1
  17. package/dist/docx/groupDrawingParser.js +123 -32
  18. package/dist/docx/packageParts.d.ts +45 -1
  19. package/dist/docx/packageParts.js +93 -2
  20. package/dist/docx/paraIdAttribute.d.ts +3 -2
  21. package/dist/docx/paragraphParser.js +91 -7
  22. package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
  23. package/dist/docx/runParser.js +10 -0
  24. package/dist/docx/selectiveSaveFlags.d.ts +10 -3
  25. package/dist/docx/selectiveSaveFlags.js +1 -1
  26. package/dist/docx/selectiveXmlPatch.d.ts +56 -34
  27. package/dist/docx/selectiveXmlPatch.js +222 -115
  28. package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
  29. package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
  30. package/dist/docx/serializer/paragraphSerializer.js +5 -4
  31. package/dist/docx/serializer/runSerializer.d.ts +5 -1
  32. package/dist/docx/serializer/runSerializer.js +4 -3
  33. package/dist/docx/shapeAlternateContent.d.ts +25 -0
  34. package/dist/docx/shapeAlternateContent.js +52 -0
  35. package/dist/docx/textBoxParser.d.ts +10 -2
  36. package/dist/docx/textBoxParser.js +9 -3
  37. package/dist/internal/headlessRevisionResolution.js +50 -13
  38. package/dist/layout-bridge/convert/toFlowBlocks.js +26 -4
  39. package/dist/layout-engine/index.d.ts +2 -2
  40. package/dist/layout-engine/index.js +33 -4
  41. package/dist/layout-engine/keep-together.d.ts +5 -2
  42. package/dist/layout-engine/keep-together.js +5 -1
  43. package/dist/layout-engine/measure/advanceComposition.js +49 -3
  44. package/dist/layout-engine/measure/measureBlocks.js +5 -3
  45. package/dist/layout-engine/measure/measureContainer.js +46 -3
  46. package/dist/layout-engine/measure/measureHelpers.js +13 -3
  47. package/dist/layout-engine/measure/measureParagraph.js +44 -81
  48. package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
  49. package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
  50. package/dist/layout-engine/paginator.d.ts +2 -0
  51. package/dist/layout-engine/paginator.js +5 -1
  52. package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
  53. package/dist/layout-engine/paragraphSpacing.js +2 -1
  54. package/dist/layout-engine/tableRowBreak.d.ts +8 -1
  55. package/dist/layout-engine/tableRowBreak.js +22 -1
  56. package/dist/layout-engine/types.d.ts +21 -2
  57. package/dist/layout-painter/renderParagraph.js +41 -4
  58. package/dist/layout-painter/renderTextBox.d.ts +12 -2
  59. package/dist/layout-painter/renderTextBox.js +26 -2
  60. package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
  61. package/dist/prosemirror/alternateContentAttrs.js +35 -0
  62. package/dist/prosemirror/attrs/index.js +90 -0
  63. package/dist/prosemirror/commands/comments.js +51 -41
  64. package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
  65. package/dist/prosemirror/contentControlRevisions.js +84 -0
  66. package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
  67. package/dist/prosemirror/conversion/toProseDoc.js +42 -19
  68. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
  69. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
  70. package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
  71. package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
  72. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
  73. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
  74. package/dist/prosemirror/listMarker.js +2 -2
  75. package/dist/prosemirror/paragraphDirection.d.ts +29 -2
  76. package/dist/prosemirror/paragraphDirection.js +21 -2
  77. package/dist/prosemirror/plugins/suggestionMode.js +37 -8
  78. package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
  79. package/dist/prosemirror/rejoinRunCarriers.js +66 -0
  80. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
  81. package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
  82. package/dist/prosemirror/schema/nodes.d.ts +37 -5
  83. package/dist/types/content.d.ts +2 -2
  84. package/package.json +2 -2
  85. package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
  86. package/dist/layout-engine/justifiedLineFit.js +0 -6
@@ -5,6 +5,7 @@ import { applyComplexScriptFormatting, hasComplexScriptFormatting, resolveComple
5
5
  import { getHyperlinkInstanceIndex } from "../layout-engine/measure/hyperlinkInstance.js";
6
6
  import { getListMarkerInlineWidth, getListMarkerVisualOffset, resolveListMarkerFont } from "../layout-engine/measure/listMarkerWidth.js";
7
7
  import { DEFAULT_FONT_FAMILY, docxScriptFontSize } from "../layout-engine/measure/measureHelpers.js";
8
+ import { SMALL_CAPS_SCALE, smallCapsSegments } from "../layout-engine/measure/smallCapsCasing.js";
8
9
  import { calculateTabWidth } from "../layout-engine/measure/tabCalculator.js";
9
10
  import { FONT_KERNING_MODE, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode, toPaintedText } from "../layout-engine/measure/textMeasurementPolicy.js";
10
11
  import { isFloatingImageRun } from "../layout-engine/types.js";
@@ -201,7 +202,6 @@ function applyRunStyles(element, run) {
201
202
  }
202
203
  if (run.letterSpacing) element.style.letterSpacing = `${run.letterSpacing}px`;
203
204
  if (run.allCaps) element.style.textTransform = "uppercase";
204
- if (run.smallCaps) element.style.fontVariant = "small-caps";
205
205
  if (run.positionPx) element.style.verticalAlign = `${run.positionPx}px`;
206
206
  applyHorizontalScaleTransform(element, run.horizontalScale);
207
207
  element.style.fontKerning = getRunFontKerningMode(run, 11);
@@ -338,7 +338,8 @@ function applyPmPositions(element, pmStart, pmEnd) {
338
338
  }
339
339
  const NON_COMPRESSIBLE_WHITESPACE_PATTERN = /([^\S ]+)/gu;
340
340
  const HAS_NON_COMPRESSIBLE_WHITESPACE_PATTERN = /[^\S ]/u;
341
- function appendPaintedText({ host, text, doc, contractedWordSpacing }) {
341
+ /** Fill `host` with `text`, splitting it into word-spacing segments as needed. */
342
+ function fillPaintedText({ host, text, doc, contractedWordSpacing }) {
342
343
  if (!contractedWordSpacing) {
343
344
  host.textContent = text;
344
345
  return;
@@ -357,6 +358,39 @@ function appendPaintedText({ host, text, doc, contractedWordSpacing }) {
357
358
  host.append(segmentEl);
358
359
  }
359
360
  }
361
+ function appendPaintedText({ host, text, doc, contractedWordSpacing, smallCapsFontSizePx }) {
362
+ const wordSpacing = contractedWordSpacing === void 0 ? {} : { contractedWordSpacing };
363
+ if (smallCapsFontSizePx === void 0) {
364
+ fillPaintedText({
365
+ host,
366
+ text,
367
+ doc,
368
+ ...wordSpacing
369
+ });
370
+ return;
371
+ }
372
+ const segments = smallCapsSegments(text);
373
+ if (!segments.some((segment) => segment.small)) {
374
+ fillPaintedText({
375
+ host,
376
+ text,
377
+ doc,
378
+ ...wordSpacing
379
+ });
380
+ return;
381
+ }
382
+ for (const segment of segments) {
383
+ const segmentEl = doc.createElement("span");
384
+ if (segment.small) segmentEl.style.fontSize = `${smallCapsFontSizePx}px`;
385
+ fillPaintedText({
386
+ host: segmentEl,
387
+ text: segment.text,
388
+ doc,
389
+ ...wordSpacing
390
+ });
391
+ host.append(segmentEl);
392
+ }
393
+ }
360
394
  function renderTextRun(run, doc, options) {
361
395
  const span = doc.createElement("span");
362
396
  span.className = `${PARAGRAPH_CLASS_NAMES.run} ${PARAGRAPH_CLASS_NAMES.text}`;
@@ -374,6 +408,7 @@ function renderTextRun(run, doc, options) {
374
408
  applyRunStyles(span, run);
375
409
  applyPmPositions(span, run.pmStart, run.pmEnd);
376
410
  const paintedText = toPaintedText(run.text);
411
+ const smallCapsFontSizePx = run.smallCaps && !run.allCaps ? fontSizePtToPx(run.fontSize || 11) * SMALL_CAPS_SCALE : void 0;
377
412
  const visualSpaceContractionPx = options?.visualSpaceContractionPx;
378
413
  const effectiveHorizontalScaleFactor = getHorizontalScaleFactor(run.horizontalScale) * (options?.ancestorHorizontalScaleFactor ?? 1);
379
414
  const contractedWordSpacing = visualSpaceContractionPx === void 0 || effectiveHorizontalScaleFactor === 0 ? void 0 : `${-visualSpaceContractionPx / effectiveHorizontalScaleFactor}px`;
@@ -393,7 +428,8 @@ function renderTextRun(run, doc, options) {
393
428
  host: anchor,
394
429
  text: paintedText,
395
430
  doc,
396
- ...contractedWordSpacing !== void 0 ? { contractedWordSpacing } : {}
431
+ ...contractedWordSpacing !== void 0 ? { contractedWordSpacing } : {},
432
+ ...smallCapsFontSizePx !== void 0 ? { smallCapsFontSizePx } : {}
397
433
  });
398
434
  if (!run.hyperlink.noDefaultStyle) {
399
435
  const hyperlinkColor = getHyperlinkTextColor(run, span.style.color);
@@ -406,7 +442,8 @@ function renderTextRun(run, doc, options) {
406
442
  host: span,
407
443
  text: paintedText,
408
444
  doc,
409
- ...contractedWordSpacing !== void 0 ? { contractedWordSpacing } : {}
445
+ ...contractedWordSpacing !== void 0 ? { contractedWordSpacing } : {},
446
+ ...smallCapsFontSizePx !== void 0 ? { smallCapsFontSizePx } : {}
410
447
  });
411
448
  applyWhitespaceUnderline(span, run);
412
449
  return span;
@@ -1,4 +1,4 @@
1
- import { TableBlock, TableMeasure, TextBoxBlock, TextBoxFragment, TextBoxMeasure } from "../layout-engine/types.js";
1
+ import { TableBlock, TableMeasure, TextBoxBlock, TextBoxFragment, TextBoxGradientFill, TextBoxMeasure } from "../layout-engine/types.js";
2
2
  import { RenderContext } from "./renderUtils.js";
3
3
  //#region src/layout-painter/renderTextBox.d.ts
4
4
  /**
@@ -14,9 +14,19 @@ type RenderTextBoxFragmentOptions = {
14
14
  document?: Document;
15
15
  renderTable?: (block: TableBlock, measure: TableMeasure, context: RenderContext, document: Document, frameWidth?: number) => HTMLElement;
16
16
  };
17
+ /**
18
+ * The CSS angle of a DrawingML linear gradient over a `width` x `height` box.
19
+ *
20
+ * DrawingML measures `a:lin@ang` clockwise from the positive x axis; CSS
21
+ * measures clockwise from "to top", a quarter turn earlier. A `scaled` angle is
22
+ * stated in the unit square: stretching that square to the box keeps each
23
+ * isoline through the same corners, so the gradient runs perpendicular to the
24
+ * stretched isoline rather than along the stated angle.
25
+ */
26
+ declare function cssLinearGradientAngle(fill: Pick<TextBoxGradientFill, "angle" | "scaled">, width: number, height: number): number;
17
27
  /**
18
28
  * Render a text box fragment to DOM
19
29
  */
20
30
  declare function renderTextBoxFragment(fragment: TextBoxFragment, block: TextBoxBlock, measure: TextBoxMeasure, context: RenderContext, options?: RenderTextBoxFragmentOptions): HTMLElement;
21
31
  //#endregion
22
- export { RenderTextBoxFragmentOptions, TEXTBOX_CLASS_NAMES, renderTextBoxFragment };
32
+ export { RenderTextBoxFragmentOptions, TEXTBOX_CLASS_NAMES, cssLinearGradientAngle, renderTextBoxFragment };
@@ -10,7 +10,7 @@ import { panic } from "better-result";
10
10
  * Text Box Renderer
11
11
  *
12
12
  * Renders text box fragments to DOM. Handles:
13
- * - Background fill color
13
+ * - Background fill color or linear gradient
14
14
  * - Border/outline
15
15
  * - Internal padding (margins)
16
16
  * - Block content inside the box (using pre-measured data)
@@ -20,6 +20,29 @@ import { panic } from "better-result";
20
20
  */
21
21
  const TEXTBOX_CLASS_NAMES = { textBox: "layout-textbox" };
22
22
  /**
23
+ * The CSS angle of a DrawingML linear gradient over a `width` x `height` box.
24
+ *
25
+ * DrawingML measures `a:lin@ang` clockwise from the positive x axis; CSS
26
+ * measures clockwise from "to top", a quarter turn earlier. A `scaled` angle is
27
+ * stated in the unit square: stretching that square to the box keeps each
28
+ * isoline through the same corners, so the gradient runs perpendicular to the
29
+ * stretched isoline rather than along the stated angle.
30
+ */
31
+ function cssLinearGradientAngle(fill, width, height) {
32
+ const radians = fill.angle * Math.PI / 180;
33
+ return ((90 + (fill.scaled && width > 0 && height > 0 ? Math.atan2(width * Math.sin(radians), height * Math.cos(radians)) : radians) * 180 / Math.PI) % 360 + 360) % 360;
34
+ }
35
+ const formatCssNumber = (value) => String(Math.round(value * 1e3) / 1e3);
36
+ function paintGradientFill(style, fill, width, height) {
37
+ const [first] = fill.stops;
38
+ if (first === void 0) return;
39
+ if (fill.stops.length === 1) {
40
+ setAuthoredBackgroundColor(style, first.color);
41
+ return;
42
+ }
43
+ style.backgroundImage = `linear-gradient(${formatCssNumber(cssLinearGradientAngle(fill, width, height))}deg, ${fill.stops.map((stop) => `${stop.color} ${formatCssNumber(stop.offset * 100)}%`).join(", ")})`;
44
+ }
45
+ /**
23
46
  * Render a text box fragment to DOM
24
47
  */
25
48
  function renderTextBoxFragment(fragment, block, measure, context, options = {}) {
@@ -32,6 +55,7 @@ function renderTextBoxFragment(fragment, block, measure, context, options = {})
32
55
  containerEl.style.overflow = block.textWrap === "none" ? "visible" : "hidden";
33
56
  containerEl.style.boxSizing = "border-box";
34
57
  if (block.fillColor) setAuthoredBackgroundColor(containerEl.style, block.fillColor);
58
+ else if (block.fillGradient) paintGradientFill(containerEl.style, block.fillGradient, fragment.width, fragment.height);
35
59
  const outlineDash = presetDashForOutlineAttr(block.outlineStyle) ?? "solid";
36
60
  if (block.outlineWidth && block.outlineWidth > 0 && outlineDash !== "none") {
37
61
  const color = block.outlineColor || "#000000";
@@ -93,4 +117,4 @@ function renderTextBoxFragment(fragment, block, measure, context, options = {})
93
117
  return containerEl;
94
118
  }
95
119
  //#endregion
96
- export { TEXTBOX_CLASS_NAMES, renderTextBoxFragment };
120
+ export { TEXTBOX_CLASS_NAMES, cssLinearGradientAngle, renderTextBoxFragment };
@@ -0,0 +1,9 @@
1
+ import { AlternateContentAttrs } from "./schema/nodes.js";
2
+ import { Node } from "prosemirror-model";
3
+ //#region src/prosemirror/alternateContentAttrs.d.ts
4
+ /** Attach a captured `mc:AlternateContent` to a freshly projected node. */
5
+ declare const withAlternateContent: (node: Node, xml: string | undefined) => Node;
6
+ /** The captured element, while the node is still the one it was attached to. */
7
+ declare const unchangedAlternateContentXml: (node: Node, captured: AlternateContentAttrs | undefined) => string | undefined;
8
+ //#endregion
9
+ export { unchangedAlternateContentXml, withAlternateContent };
@@ -0,0 +1,35 @@
1
+ import { canonicalJson } from "../utils/canonicalJson.js";
2
+ //#region src/prosemirror/alternateContentAttrs.ts
3
+ const ALTERNATE_CONTENT_ATTR = "_docxAlternateContent";
4
+ /**
5
+ * What the node says about the shape: its attributes and, for a text box, its
6
+ * content. Marks are left out; they carry the run's properties, which sit
7
+ * outside `mc:AlternateContent`.
8
+ *
9
+ * The projection from the model is lossy, so the node is compared with itself
10
+ * as it was projected rather than the rebuilt shape with the parsed one: a
11
+ * round trip that changed nothing would otherwise read as an edit.
12
+ */
13
+ const nodeFingerprint = (node) => {
14
+ const { [ALTERNATE_CONTENT_ATTR]: _captured, ...attrs } = node.attrs;
15
+ return canonicalJson({
16
+ attrs,
17
+ content: node.content.toJSON()
18
+ });
19
+ };
20
+ /** Attach a captured `mc:AlternateContent` to a freshly projected node. */
21
+ const withAlternateContent = (node, xml) => {
22
+ if (xml === void 0) return node;
23
+ const alternateContent = {
24
+ xml,
25
+ fingerprint: nodeFingerprint(node)
26
+ };
27
+ return node.type.create({
28
+ ...node.attrs,
29
+ [ALTERNATE_CONTENT_ATTR]: alternateContent
30
+ }, node.content, node.marks);
31
+ };
32
+ /** The captured element, while the node is still the one it was attached to. */
33
+ const unchangedAlternateContentXml = (node, captured) => captured !== void 0 && captured.fingerprint === nodeFingerprint(node) ? captured.xml : void 0;
34
+ //#endregion
35
+ export { unchangedAlternateContentXml, withAlternateContent };
@@ -540,6 +540,7 @@ const readFieldAttrs = (node) => {
540
540
  requiredOneOf(attrs, "fieldKind", "field.attrs.fieldKind", issues, FIELD_KINDS);
541
541
  optionalBoolean(attrs, "fldLock", "field.attrs.fldLock", issues);
542
542
  optionalBoolean(attrs, "dirty", "field.attrs.dirty", issues);
543
+ optionalBoolean(attrs, "fieldResultIsFallback", "field.attrs.fieldResultIsFallback", issues);
543
544
  return attrsResult(attrs, issues);
544
545
  };
545
546
  const expectFieldAttrs = (node) => expectCachedNodeAttrs(node, fieldAttrsCache, readFieldAttrs, "field attrs");
@@ -637,6 +638,7 @@ const readShapeAttrs = (node) => {
637
638
  optionalNumber(attrs, "shadowOffsetY", "shape.attrs.shadowOffsetY", issues);
638
639
  optionalString(attrs, "glowColor", "shape.attrs.glowColor", issues);
639
640
  optionalNumber(attrs, "glowRadius", "shape.attrs.glowRadius", issues);
641
+ optionalAlternateContent(attrs, "shape.attrs._docxAlternateContent", issues);
640
642
  return attrsResult(attrs, issues);
641
643
  };
642
644
  const expectShapeAttrs = (node) => expectCachedNodeAttrs(node, shapeAttrsCache, readShapeAttrs, "shape attrs");
@@ -654,6 +656,7 @@ const readTextBoxAttrs = (node) => {
654
656
  optionalString(attrs, "alt", "textBox.attrs.alt", issues);
655
657
  optionalString(attrs, "title", "textBox.attrs.title", issues);
656
658
  optionalString(attrs, "fillColor", "textBox.attrs.fillColor", issues);
659
+ optionalGradientFill(attrs, "gradientFill", "textBox.attrs.gradientFill", issues);
657
660
  optionalNumber(attrs, "outlineWidth", "textBox.attrs.outlineWidth", issues);
658
661
  optionalAuthoredEmu(attrs, "_docxAuthoredEmu", "textBox.attrs._docxAuthoredEmu", issues, TEXT_BOX_AUTHORED_EMU_ATTRS);
659
662
  optionalString(attrs, "outlineColor", "textBox.attrs.outlineColor", issues);
@@ -681,8 +684,10 @@ const readTextBoxAttrs = (node) => {
681
684
  optionalEffectExtentSlots(attrs, "wrapEffectExtentSlots", "textBox.attrs.wrapEffectExtentSlots", issues);
682
685
  optionalWrapPolygon(attrs, "wrapPolygon", "textBox.attrs.wrapPolygon", issues);
683
686
  optionalOneOf(attrs, "_docxPlacement", "textBox.attrs._docxPlacement", issues, TEXT_BOX_DOCX_PLACEMENTS);
687
+ optionalAlternateContent(attrs, "textBox.attrs._docxAlternateContent", issues);
684
688
  optionalString(attrs, "_docxGroupId", "textBox.attrs._docxGroupId", issues);
685
689
  optionalString(attrs, "_docxAnchorId", "textBox.attrs._docxAnchorId", issues);
690
+ optionalDrawingGroupChild(attrs, "_docxGroupChild", "textBox.attrs._docxGroupChild", issues);
686
691
  requiredTextBoxBodyContentState(attrs, issues);
687
692
  optionalTextBoxTrackedChange(attrs, issues);
688
693
  optionalTextBoxInlineSdts(attrs, issues);
@@ -1618,6 +1623,20 @@ const optionalTextBoxTrackedChange = (attrs, issues) => {
1618
1623
  requiredString(info, "author", "textBox.attrs._docxTrackedChange.info.author", issues);
1619
1624
  optionalString(info, "date", "textBox.attrs._docxTrackedChange.info.date", issues);
1620
1625
  };
1626
+ /** `_docxAlternateContent`: a captured element and its node fingerprint, or absent. */
1627
+ const optionalAlternateContent = (attrs, path, issues) => {
1628
+ const value = attrs["_docxAlternateContent"];
1629
+ if (value === void 0 || value === null) return;
1630
+ if (!isRecord(value)) {
1631
+ issues.push({
1632
+ path,
1633
+ message: "Expected an object."
1634
+ });
1635
+ return;
1636
+ }
1637
+ requiredString(value, "xml", `${path}.xml`, issues);
1638
+ requiredString(value, "fingerprint", `${path}.fingerprint`, issues);
1639
+ };
1621
1640
  const requiredTextBoxBodyContentState = (attrs, issues) => {
1622
1641
  const value = attrs["_docxTextBodyContentState"];
1623
1642
  if (!isRecord(value)) {
@@ -1667,6 +1686,7 @@ const validateSdtAttrsRecord = (attrs, path, issues) => {
1667
1686
  optionalPreservedMarkup(attrs, "_preserved", `${path}._preserved`, issues);
1668
1687
  optionalString(attrs, "rawEndPropertiesXml", `${path}.rawEndPropertiesXml`, issues);
1669
1688
  optionalSdtEndProperties(attrs, `${path}.endProperties`, issues);
1689
+ optionalNumberArray(attrs, "_docxEnclosingRevisionIds", `${path}._docxEnclosingRevisionIds`, issues);
1670
1690
  };
1671
1691
  /**
1672
1692
  * The attribute remainder an authored element's record carries.
@@ -2555,6 +2575,76 @@ const optionalWrapPolygon = (attrs, key, path, issues) => {
2555
2575
  readWrapPolygonPoint(point, `${path}.lineTo[${index}]`, issues);
2556
2576
  });
2557
2577
  };
2578
+ /** A text box's gradient fill: the model's `ShapeFill`, and only its gradient arm. */
2579
+ const optionalGradientFill = (attrs, key, path, issues) => {
2580
+ const value = attrs[key];
2581
+ if (value === void 0 || value === null) return;
2582
+ if (!isRecord(value)) {
2583
+ issues.push({
2584
+ path,
2585
+ message: "Expected an object."
2586
+ });
2587
+ return;
2588
+ }
2589
+ if (value["type"] !== "gradient") issues.push({
2590
+ path: `${path}.type`,
2591
+ message: "Expected \"gradient\"."
2592
+ });
2593
+ optionalString(value, "rawXml", `${path}.rawXml`, issues);
2594
+ const gradient = value["gradient"];
2595
+ if (!isRecord(gradient)) {
2596
+ issues.push({
2597
+ path: `${path}.gradient`,
2598
+ message: "Expected an object."
2599
+ });
2600
+ return;
2601
+ }
2602
+ optionalOneOf(gradient, "type", `${path}.gradient.type`, issues, SHAPE_GRADIENT_TYPES);
2603
+ optionalNumber(gradient, "angle", `${path}.gradient.angle`, issues);
2604
+ optionalBoolean(gradient, "scaled", `${path}.gradient.scaled`, issues);
2605
+ const stops = gradient["stops"];
2606
+ if (!Array.isArray(stops)) {
2607
+ issues.push({
2608
+ path: `${path}.gradient.stops`,
2609
+ message: "Expected an array."
2610
+ });
2611
+ return;
2612
+ }
2613
+ stops.forEach((stop, index) => {
2614
+ const stopPath = `${path}.gradient.stops[${index}]`;
2615
+ if (!isRecord(stop)) {
2616
+ issues.push({
2617
+ path: stopPath,
2618
+ message: "Expected a gradient stop object."
2619
+ });
2620
+ return;
2621
+ }
2622
+ if (typeof stop["position"] !== "number") issues.push({
2623
+ path: `${stopPath}.position`,
2624
+ message: "Expected a number."
2625
+ });
2626
+ optionalColorValue(stop, "color", `${stopPath}.color`, issues);
2627
+ });
2628
+ };
2629
+ /** Where a text box sits inside a DrawingML group: its element path and two fingerprints. */
2630
+ const optionalDrawingGroupChild = (attrs, key, path, issues) => {
2631
+ const value = attrs[key];
2632
+ if (value === void 0 || value === null) return;
2633
+ if (!isRecord(value)) {
2634
+ issues.push({
2635
+ path,
2636
+ message: "Expected an object."
2637
+ });
2638
+ return;
2639
+ }
2640
+ const childPath = value["path"];
2641
+ if (!Array.isArray(childPath) || childPath.length === 0 || !childPath.every((index) => Number.isInteger(index) && index >= 0)) issues.push({
2642
+ path: `${path}.path`,
2643
+ message: "Expected a non-empty array of indices."
2644
+ });
2645
+ requiredString(value, "group", `${path}.group`, issues);
2646
+ requiredString(value, "content", `${path}.content`, issues);
2647
+ };
2558
2648
  /** `a:hlinkClick` as captured: its own bytes, plus the target it named. */
2559
2649
  const optionalDocPrLink = (attrs, key, path, issues) => {
2560
2650
  const value = attrs[key];
@@ -4,11 +4,13 @@ import { appendHeadlessInlineResolution } from "../../internal/headlessRevisionR
4
4
  import { stateAllowsHeadlessRevisionResolution } from "../../internal/headlessRevisionResolutionGuard.js";
5
5
  import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
6
6
  import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../containerFinalParagraph.js";
7
+ import { enclosingRevisionIds, resolutionRemovesControl, withoutResolvedEnclosures } from "../contentControlRevisions.js";
7
8
  import { resolveParagraphDefaultTextFormatting } from "../conversion/toProseDoc.js";
8
9
  import { markChangedParagraphRanges, markStructuralChange, markTrackedSectionEndpointRemoval } from "../extensions/features/ParagraphChangeTrackerExtension.js";
9
10
  import "../extensions/nodes/SdtExtension.js";
10
11
  import { getDocumentStyleResolver } from "../plugins/documentStyles.js";
11
12
  import { setParagraphAttrsWithRebasedRunFormatting } from "../rebaseParagraphRunFormatting.js";
13
+ import { rejoinRunsAt } from "../rejoinRunCarriers.js";
12
14
  import { getFolioNodeRevisionCarriers, nodePropertyRevisionSites, propertyRevisionMetadata, propertyRevisionRecords } from "../revisionCarriers.js";
13
15
  import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
14
16
  import { reconstructRejectedRunFormattingMarks } from "../runPropertyChangeResolution.js";
@@ -109,6 +111,8 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
109
111
  if (dispatch) {
110
112
  const tr = state.tr;
111
113
  const deleteRanges = [];
114
+ /** Where resolved inline content began and ended, in `state.doc`. */
115
+ const resolvedBoundaries = [];
112
116
  const pPrMarkOps = [];
113
117
  const tableRowStructuralOps = [];
114
118
  const tableCellStructuralOps = [];
@@ -198,15 +202,27 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
198
202
  const nodeEnd = pos + node.nodeSize;
199
203
  const rangeFrom = Math.max(from, pos);
200
204
  const rangeTo = Math.min(to, nodeEnd);
201
- if (node.type.name === "sdt" && removeType !== void 0 && rangeCoversNode(from, to, pos, node) && revisionCoversWholeControl(node, removeType, matchesRevision)) {
202
- deleteRanges.push({
203
- from: pos,
204
- to: nodeEnd
205
- });
206
- return false;
205
+ if (node.type.name === "sdt") {
206
+ if (!rangeCoversNode(from, to, pos, node)) return true;
207
+ const resolvesRevision = (revisionId) => revisionSet === null || revisionSet.has(revisionId);
208
+ if (resolutionRemovesControl({
209
+ control: node,
210
+ removeType,
211
+ resolves: resolvesRevision
212
+ })) {
213
+ deleteRanges.push({
214
+ from: pos,
215
+ to: nodeEnd
216
+ });
217
+ return false;
218
+ }
219
+ const released = withoutResolvedEnclosures(node, resolvesRevision);
220
+ if (released) tr.setNodeMarkup(pos, void 0, released);
221
+ return true;
207
222
  }
208
223
  const runPropertyChangeMark = node.marks.find((mark) => mark.type.name === "runPropertyChange");
209
224
  const removesNode = removeType !== void 0 && node.marks.some((mark) => mark.type === removeType && matchesRevision(mark));
225
+ if (removesNode || runPropertyChangeMark !== void 0 || node.marks.some((mark) => (mark.type === insertionType || mark.type === deletionType) && matchesRevision(mark))) resolvedBoundaries.push(rangeFrom, rangeTo);
210
226
  if (runPropertyChangeMark) resolveRunPropertyChange({
211
227
  tr,
212
228
  node,
@@ -386,6 +402,11 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
386
402
  removedReferences: removedSectionReferences
387
403
  });
388
404
  if (!terminalTableDeletionIsPending(tr)) resolveTerminalTableReviewCarrier(tr, mode);
405
+ rejoinRunsAt({
406
+ tr,
407
+ boundaries: resolvedBoundaries.map((position) => tr.mapping.map(position)),
408
+ styleResolver
409
+ });
389
410
  if (tr.steps.length > 0) dispatch(tr);
390
411
  }
391
412
  return true;
@@ -620,24 +641,6 @@ function rangeCoversNode(from, to, pos, node) {
620
641
  return from <= pos && to >= pos + node.nodeSize;
621
642
  }
622
643
  /**
623
- * Whether a revision covers everything an inline content control holds.
624
- *
625
- * The save leg writes such a revision around the control (`w:ins > w:sdt`),
626
- * so resolving it is an operation over the control itself: a reader who
627
- * rejects an inserted control gets no control, not an empty one standing
628
- * where it was. A revision over part of the content is a revision over that
629
- * part, and a control that holds nothing is covered by nothing.
630
- */
631
- function revisionCoversWholeControl(node, removeType, matchesRevision) {
632
- if (node.childCount === 0) return false;
633
- for (let index = 0; index < node.childCount; index += 1) {
634
- const child = node.child(index);
635
- if (child.marks.some((mark) => mark.type === removeType && matchesRevision(mark))) continue;
636
- if (child.type.name !== "sdt" || !revisionCoversWholeControl(child, removeType, matchesRevision)) return false;
637
- }
638
- return true;
639
- }
640
- /**
641
644
  * The attrs a reject restores for each property revision the shared node-attr
642
645
  * pass owns. Total over those kinds, so a revision the model gains either
643
646
  * declares a different resolution or states here what rejecting it restores.
@@ -826,6 +829,7 @@ function findAIEditRevisionRange(state, revisionIds) {
826
829
  };
827
830
  state.doc.descendants((node, pos) => {
828
831
  for (const carrier of getFolioNodeRevisionCarriers(node, pos)) if (idSet.has(carrier.id)) includeRange(carrier.from, carrier.to);
832
+ if (node.type.name === "sdt" && enclosingRevisionIds(node).some((id) => idSet.has(id))) includeRange(pos, pos + node.nodeSize);
829
833
  if (node.type.name === "tableRow") for (const attrName of ["trIns", "trDel"]) {
830
834
  const marker = node.attrs[attrName];
831
835
  if (isTableRowRevisionAttr(marker) && idSet.has(marker.revisionId)) {
@@ -1724,24 +1728,30 @@ function expandTrackedChangeRange(state, mark, fromHint, toHint) {
1724
1728
  const carriesSameInlineMark = (node) => node?.isInline === true && node.marks.some((m) => m.eq(mark));
1725
1729
  let from = fromHint;
1726
1730
  let to = toHint;
1727
- let $from = state.doc.resolve(from);
1728
- let nodeBefore = $from.nodeBefore;
1729
- while (carriesSameInlineMark(nodeBefore)) {
1730
- from -= nodeBefore.nodeSize;
1731
- $from = state.doc.resolve(from);
1732
- nodeBefore = $from.nodeBefore;
1733
- }
1734
- let $to = state.doc.resolve(to);
1735
- let nodeAfter = $to.nodeAfter;
1736
- while (carriesSameInlineMark(nodeAfter)) {
1737
- to += nodeAfter.nodeSize;
1738
- $to = state.doc.resolve(to);
1739
- nodeAfter = $to.nodeAfter;
1731
+ const revisionId = mark.attrs["revisionId"];
1732
+ for (;;) {
1733
+ let $from = state.doc.resolve(from);
1734
+ let nodeBefore = $from.nodeBefore;
1735
+ while (carriesSameInlineMark(nodeBefore)) {
1736
+ from -= nodeBefore.nodeSize;
1737
+ $from = state.doc.resolve(from);
1738
+ nodeBefore = $from.nodeBefore;
1739
+ }
1740
+ let $to = state.doc.resolve(to);
1741
+ let nodeAfter = $to.nodeAfter;
1742
+ while (carriesSameInlineMark(nodeAfter)) {
1743
+ to += nodeAfter.nodeSize;
1744
+ $to = state.doc.resolve(to);
1745
+ nodeAfter = $to.nodeAfter;
1746
+ }
1747
+ const control = $from.parent;
1748
+ if (control.type.name !== "sdt" || typeof revisionId !== "number" || !enclosingRevisionIds(control).includes(revisionId) || from !== $from.start() || to !== $from.end()) return {
1749
+ from,
1750
+ to
1751
+ };
1752
+ from = $from.before();
1753
+ to = $from.after();
1740
1754
  }
1741
- return {
1742
- from,
1743
- to
1744
- };
1745
1755
  }
1746
1756
  /**
1747
1757
  * Find the next tracked change after the given position. Returns the full
@@ -0,0 +1,43 @@
1
+ import { Transaction } from "prosemirror-state";
2
+ import { MarkType, Node } from "prosemirror-model";
3
+ //#region src/prosemirror/contentControlRevisions.d.ts
4
+ declare const ENCLOSING_REVISION_IDS_ATTR = "_docxEnclosingRevisionIds";
5
+ /** The revisions whose element encloses `control`. */
6
+ declare const enclosingRevisionIds: (control: Node) => readonly number[];
7
+ /** `control`'s attrs with `revisionId` added to its enclosing revisions. */
8
+ declare const withEnclosingRevision: (control: Node, revisionId: number) => Node["attrs"] | null;
9
+ type ResolutionRemovesControlOptions = {
10
+ control: Node;
11
+ /** The revision mark type the resolution removes content under. */
12
+ removeType: MarkType | undefined;
13
+ /** Whether the resolution resolves revision `revisionId`. */
14
+ resolves: (revisionId: number) => boolean;
15
+ };
16
+ /**
17
+ * Whether resolving removes the control itself: a revision that encloses it is
18
+ * resolved in the direction that removes its content, and still covers all of
19
+ * it. Removing only the content leaves the control standing, emptied.
20
+ */
21
+ declare const resolutionRemovesControl: ({ control, removeType, resolves }: ResolutionRemovesControlOptions) => boolean;
22
+ /**
23
+ * `control`'s attrs without the enclosing revisions `resolved` names, or
24
+ * `null` when none of them is: a resolved revision no longer exists, and an id
25
+ * left behind would claim whatever revision is later given that id.
26
+ */
27
+ declare const withoutResolvedEnclosures: (control: Node, resolved: (revisionId: number) => boolean) => Node["attrs"] | null;
28
+ type EncloseWholeControlsOptions = {
29
+ tr: Transaction;
30
+ from: number;
31
+ to: number;
32
+ revisionId: number;
33
+ };
34
+ /**
35
+ * Record `revisionId` as enclosing every inline control `[from, to]` covers
36
+ * whole, opening and closing token included: the revision removes or adds the
37
+ * control, as deleting or inserting the same range directly would. A control
38
+ * the range covers only the content of is left alone, and its revision stays
39
+ * one over that content.
40
+ */
41
+ declare const encloseWholeControls: ({ tr, from, to, revisionId }: EncloseWholeControlsOptions) => void;
42
+ //#endregion
43
+ export { ENCLOSING_REVISION_IDS_ATTR, encloseWholeControls, enclosingRevisionIds, resolutionRemovesControl, withEnclosingRevision, withoutResolvedEnclosures };
@@ -0,0 +1,84 @@
1
+ import { expectTrackedChangeMarkAttrs } from "./attrs/index.js";
2
+ import "./extensions/nodes/SdtExtension.js";
3
+ //#region src/prosemirror/contentControlRevisions.ts
4
+ const ENCLOSING_REVISION_IDS_ATTR = "_docxEnclosingRevisionIds";
5
+ const isInlineControl = (node) => node.type.name === "sdt";
6
+ /** The revisions whose element encloses `control`. */
7
+ const enclosingRevisionIds = (control) => {
8
+ const ids = control.attrs[ENCLOSING_REVISION_IDS_ATTR];
9
+ return Array.isArray(ids) ? ids.filter((id) => typeof id === "number") : [];
10
+ };
11
+ /** `control`'s attrs with `revisionId` added to its enclosing revisions. */
12
+ const withEnclosingRevision = (control, revisionId) => {
13
+ const ids = enclosingRevisionIds(control);
14
+ return ids.includes(revisionId) ? null : {
15
+ ...control.attrs,
16
+ [ENCLOSING_REVISION_IDS_ATTR]: [...ids, revisionId]
17
+ };
18
+ };
19
+ const REMOVED_LAYERS = {
20
+ deletion: /* @__PURE__ */ new Set(["deletion", "moveFrom"]),
21
+ insertion: /* @__PURE__ */ new Set(["insertion", "moveTo"])
22
+ };
23
+ /** Whether `mark`, or a revision it is nested in, is `revisionId` of `removeType`'s kind. */
24
+ const carriesRevision = (mark, removeType, revisionId) => {
25
+ if (mark.type.name !== "insertion" && mark.type.name !== "deletion") return false;
26
+ const attrs = expectTrackedChangeMarkAttrs(mark);
27
+ if (mark.type === removeType && attrs.revisionId === revisionId) return true;
28
+ const removed = removeType.name === "deletion" ? REMOVED_LAYERS.deletion : REMOVED_LAYERS.insertion;
29
+ return (attrs._docxRevisionAncestors ?? []).some((layer) => layer.revisionId === revisionId && removed.has(layer.type));
30
+ };
31
+ const coveredBy = (node, covers) => {
32
+ if (node.childCount === 0) return false;
33
+ for (let index = 0; index < node.childCount; index += 1) {
34
+ const child = node.child(index);
35
+ if (child.marks.some(covers)) continue;
36
+ if (!isInlineControl(child) || !coveredBy(child, covers)) return false;
37
+ }
38
+ return true;
39
+ };
40
+ /**
41
+ * Whether resolving removes the control itself: a revision that encloses it is
42
+ * resolved in the direction that removes its content, and still covers all of
43
+ * it. Removing only the content leaves the control standing, emptied.
44
+ */
45
+ const resolutionRemovesControl = ({ control, removeType, resolves }) => {
46
+ if (removeType === void 0 || !isInlineControl(control)) return false;
47
+ return enclosingRevisionIds(control).some((revisionId) => resolves(revisionId) && coveredBy(control, (mark) => carriesRevision(mark, removeType, revisionId)));
48
+ };
49
+ /**
50
+ * `control`'s attrs without the enclosing revisions `resolved` names, or
51
+ * `null` when none of them is: a resolved revision no longer exists, and an id
52
+ * left behind would claim whatever revision is later given that id.
53
+ */
54
+ const withoutResolvedEnclosures = (control, resolved) => {
55
+ const ids = enclosingRevisionIds(control);
56
+ const remaining = ids.filter((revisionId) => !resolved(revisionId));
57
+ if (remaining.length === ids.length) return null;
58
+ return {
59
+ ...control.attrs,
60
+ [ENCLOSING_REVISION_IDS_ATTR]: remaining.length > 0 ? remaining : null
61
+ };
62
+ };
63
+ /**
64
+ * Record `revisionId` as enclosing every inline control `[from, to]` covers
65
+ * whole, opening and closing token included: the revision removes or adds the
66
+ * control, as deleting or inserting the same range directly would. A control
67
+ * the range covers only the content of is left alone, and its revision stays
68
+ * one over that content.
69
+ */
70
+ const encloseWholeControls = ({ tr, from, to, revisionId }) => {
71
+ const enclosed = [];
72
+ tr.doc.nodesBetween(from, to, (node, pos) => {
73
+ if (!isInlineControl(node) || pos < from || pos + node.nodeSize > to) return true;
74
+ const attrs = withEnclosingRevision(node, revisionId);
75
+ if (attrs) enclosed.push({
76
+ pos,
77
+ attrs
78
+ });
79
+ return true;
80
+ });
81
+ for (const { pos, attrs } of enclosed) tr.setNodeMarkup(pos, void 0, attrs);
82
+ };
83
+ //#endregion
84
+ export { ENCLOSING_REVISION_IDS_ATTR, encloseWholeControls, enclosingRevisionIds, resolutionRemovesControl, withEnclosingRevision, withoutResolvedEnclosures };