@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 { isInlineSdtContent, isSimpleFieldContent } from "../../docx/inlineWrapp
5
5
  import { paragraphNumberingReferenceId as paragraphNumberingReferenceId$1, sameStatedParagraphNumbering as sameStatedParagraphNumbering$1 } from "../../docx/numberingReference.js";
6
6
  import { PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR, ParagraphPropertySourceValidationError, copyDocumentParagraphPropertySourceContract, copyDocumentParagraphPropertySources, copyParagraphPropertyCapture, copyParagraphPropertySource, decodeTableCellParagraphSourcePayload, getDocumentParagraphPropertySourceContract, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceToken, getParagraphPropertySourceTransferId, getProseDocumentParagraphPropertySourceContract, getProseParagraphPropertySourceToken, isParagraphPropertySourceToken, linkParagraphPropertySourceCandidate, paragraphPropertySourceBelongsToDocument, paragraphPropertySourceTokenMatchesContract, recreateProseNodeWithParagraphPropertySource, restoreTableCellsWithParagraphPropertySources, visitDocumentStoryParagraphs, visitTableCellParagraphPropertySourceBindings } from "../../docx/paragraphPropertySource.js";
7
7
  import { BLOCK_TREE_DESCENT, visitBlockTreeRecords, visitDocxParagraphs } from "../../docx/paragraphTraversal.js";
8
+ import { refingerprintShapeAlternateContent } from "../../docx/shapeAlternateContent.js";
8
9
  import { DATE_UTC_ATTRIBUTE } from "../../docx/trackedChangeInfo.js";
9
10
  import { copiedWrapPolygon } from "../../docx/wrapPolygon.js";
10
11
  import { isStyleSourcedParagraphNumbering, modelParagraphFormattingEmission } from "../../internal/paragraphFormattingSerialization.js";
@@ -15,12 +16,14 @@ import { canonicalJson } from "../../utils/canonicalJson.js";
15
16
  import { normalizeHorizontalScalePercent } from "../../utils/horizontalScale.js";
16
17
  import { mergeTextFormatting } from "../../utils/textFormattingMerge.js";
17
18
  import { emuToPixels, emuToStrokePixels, pixelsToEmu } from "../../utils/units.js";
19
+ import { unchangedAlternateContentXml } from "../alternateContentAttrs.js";
18
20
  import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectInlineWrapperMarkAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectPageBreakRunAttrs, expectParagraphAttrs, expectPreservedBlockAttrs, expectPreservedXmlAttrs, expectRunFormattingOverrideMarkAttrs, expectRunIdentityMarkAttrs, expectRunPropertyChangeMarkAttrs, expectRunShadingMarkAttrs, expectSdtAttrs, expectShapeAttrs, expectStrikeMarkAttrs, expectSymbolAttrs, expectTabAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../attrs/index.js";
19
21
  import { readAuthoredTransform } from "../authoredTransformAttrs.js";
20
22
  import { autospacingMatchesBase, hasAutospacingBaseSide } from "../autospacingBase.js";
21
23
  import { bookmarkMarkerFromAttrs, expectBookmarkBoundaryAttrs } from "../bookmarkBoundaryAttrs.js";
22
24
  import { paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, removeParagraphPropertyChanges } from "../commands/propertyChangeScope.js";
23
25
  import { expectCommentReferenceAttrs } from "../commentReferenceAttrs.js";
26
+ import { enclosingRevisionIds } from "../contentControlRevisions.js";
24
27
  import { INLINE_WRAPPER_MARK_NAME } from "../extensions/marks/InlineWrapperExtension.js";
25
28
  import { applyRunFormattingOverrideAttrs, buildRunFormattingOverrideAttrs } from "../extensions/marks/RunFormattingOverrideExtension.js";
26
29
  import { textFormattingToMarks } from "../extensions/marks/markUtils.js";
@@ -31,7 +34,7 @@ import { listRenderingFromAttrs as listRenderingFromAttrs$1 } from "../listRende
31
34
  import { expectMoveRangeBoundaryAttrs } from "../moveRangeBoundaryAttrs.js";
32
35
  import { resolveNumberedRefFields } from "../numberedRefFields.js";
33
36
  import { directParagraphAlignment } from "../paragraphAlignment.js";
34
- import { directionToBidi } from "../paragraphDirection.js";
37
+ import { directionToAuthoredBidi } from "../paragraphDirection.js";
35
38
  import { directParagraphSpacing } from "../paragraphSpacing.js";
36
39
  import { expectRangeAnchorAttrs } from "../rangeAnchorAttrs.js";
37
40
  import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
@@ -1050,7 +1053,7 @@ function paragraphAttrsToFormatting(attrs) {
1050
1053
  assignBooleanToggle(result, attrs, orig, "suppressAutoHyphens");
1051
1054
  if (attrs.spacingExplicit !== orig.spacingExplicit) if (attrs.spacingExplicit) result.spacingExplicit = attrs.spacingExplicit;
1052
1055
  else delete result.spacingExplicit;
1053
- const bidi = authoredParagraphValue("bidi", directionToBidi(attrs.direction), orig, attrs._resolvedFormatting);
1056
+ const bidi = authoredParagraphValue("bidi", directionToAuthoredBidi(attrs.direction), orig, attrs._resolvedFormatting);
1054
1057
  if (bidi !== (orig.bidi ?? void 0)) if (bidi === void 0) delete result.bidi;
1055
1058
  else result.bidi = bidi;
1056
1059
  return result;
@@ -1058,7 +1061,7 @@ function paragraphAttrsToFormatting(attrs) {
1058
1061
  const resolved = attrs._resolvedFormatting;
1059
1062
  const authored = (field, value) => authoredParagraphValue(field, value ?? void 0, void 0, resolved);
1060
1063
  const outlineLevel = authored("outlineLevel", Reflect.get(attrs, "outlineLevel"));
1061
- const bidi = authored("bidi", directionToBidi(attrs.direction));
1064
+ const bidi = authored("bidi", directionToAuthoredBidi(attrs.direction));
1062
1065
  const snapToGrid = authored("snapToGrid", attrs.snapToGrid);
1063
1066
  const indentLeft = authored("indentLeft", attrs.indentLeft);
1064
1067
  const indentRight = authored("indentRight", attrs.indentRight);
@@ -1350,6 +1353,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1350
1353
  }
1351
1354
  };
1352
1355
  const appendDirectRun = (node, run, coalescePlainText = false) => {
1356
+ if (currentHyperlink) flushCurrentInline();
1353
1357
  const marksKey = getMarksKey(node.marks);
1354
1358
  const ownerId = runIdentityId(node);
1355
1359
  const currentOwnerId = currentRun ? sourceRunOwners.get(currentRun) : void 0;
@@ -2262,6 +2266,7 @@ function createFieldFromNode(node, { baseParagraphFormatting, inheritedFormattin
2262
2266
  };
2263
2267
  if (attrs.fldLock !== void 0) complex.fldLock = attrs.fldLock;
2264
2268
  if (attrs.dirty !== void 0) complex.dirty = attrs.dirty;
2269
+ if (attrs.fieldResultIsFallback !== void 0) complex.fieldResultIsFallback = attrs.fieldResultIsFallback;
2265
2270
  return complex;
2266
2271
  }
2267
2272
  const simple = {
@@ -2324,21 +2329,21 @@ function createMathFromNode(node) {
2324
2329
  return math;
2325
2330
  }
2326
2331
  /**
2327
- * A revision covering every child of a control, written around the control.
2332
+ * A revision that encloses a control, written around the control.
2328
2333
  *
2329
2334
  * `w:ins > w:sdt` and `w:sdt > w:ins` reach the editor as the same revision
2330
- * mark on the same leaves, so the save leg has to pick one and writes the
2331
- * canonical order: the revision outermost, as it already is around a hyperlink
2332
- * and around a transparent wrapper. Outermost is also the only form in which
2333
- * accepting or rejecting the change is an operation over the whole control —
2334
- * the reader who inserted a bound field inserted the field, not its text.
2335
+ * mark on the same leaves; the control's enclosing-revision record says which
2336
+ * one it was (`contentControlRevisions.ts`). A revision it names that still
2337
+ * covers every child goes outermost, the only form in which accepting or
2338
+ * rejecting the change is an operation over the whole control: the reader who
2339
+ * inserted a bound field inserted the field, not its text.
2335
2340
  *
2336
- * A revision that covers only part of the content has no such form and stays
2337
- * where the editor holds it, per child.
2341
+ * Any other revision, including one over all of the content, revises the
2342
+ * content and stays where the editor holds it, per child.
2338
2343
  */
2339
- const hoistUniformRevision = (sdt) => {
2344
+ const hoistUniformRevision = (sdt, enclosingIds) => {
2340
2345
  const only = sdt.content.length === 1 ? sdt.content.at(0) : void 0;
2341
- if (only === void 0 || !isRevisionWrapper(only)) return sdt;
2346
+ if (only === void 0 || !isRevisionWrapper(only) || !enclosingIds.includes(only.info.id)) return sdt;
2342
2347
  const admitted = only.content.filter(isInlineSdtContent);
2343
2348
  if (admitted.length !== only.content.length) return sdt;
2344
2349
  return {
@@ -2359,7 +2364,7 @@ function createInlineSdtFromNode(node, textBoxAnchorMarkers, formattingContext)
2359
2364
  type: "inlineSdt",
2360
2365
  properties,
2361
2366
  content
2362
- });
2367
+ }, enclosingRevisionIds(node));
2363
2368
  }
2364
2369
  /**
2365
2370
  * Create a Run containing an image
@@ -2488,6 +2493,17 @@ const drawingFromImageAttrs = (image, attrs) => {
2488
2493
  default: return mode;
2489
2494
  }
2490
2495
  };
2496
+ const shapeContentFromNode = ({ node, shape, captured }) => {
2497
+ const xml = unchangedAlternateContentXml(node, captured);
2498
+ return xml === void 0 ? {
2499
+ type: "shape",
2500
+ shape
2501
+ } : {
2502
+ type: "shape",
2503
+ shape,
2504
+ alternateContent: refingerprintShapeAlternateContent(shape, xml)
2505
+ };
2506
+ };
2491
2507
  /**
2492
2508
  * Create a Run from a ProseMirror shape node
2493
2509
  */
@@ -2557,10 +2573,11 @@ function createShapeRun(node) {
2557
2573
  if (attrs.outlineColor) shapeOutline.color = attrs.outlineColorValue ?? { rgb: attrs.outlineColor.replace("#", "") };
2558
2574
  shape.outline = shapeOutline;
2559
2575
  }
2560
- const shapeContent = {
2561
- type: "shape",
2562
- shape
2563
- };
2576
+ const shapeContent = shapeContentFromNode({
2577
+ node,
2578
+ shape,
2579
+ captured: attrs._docxAlternateContent
2580
+ });
2564
2581
  const run = {
2565
2582
  type: "run",
2566
2583
  ...carriedRunFormatting(attrs._docxRunFormatting),
@@ -3583,6 +3600,7 @@ function convertPMTextBox(node, styleResolver = null) {
3583
3600
  type: "solid",
3584
3601
  color: { rgb: attrs.fillColor.replace("#", "") }
3585
3602
  };
3603
+ else if (attrs.gradientFill?.gradient) shape.fill = structuredClone(attrs.gradientFill);
3586
3604
  if (attrs.outlineStyle !== "none" && attrs.outlineWidth && attrs.outlineWidth > 0) {
3587
3605
  const tbOutline = {
3588
3606
  width: emuFromPixels(attrs.outlineWidth, "outlineWidth", authoredEmu, emuToStrokePixels),
@@ -3597,12 +3615,18 @@ function convertPMTextBox(node, styleResolver = null) {
3597
3615
  if (position) shape.position = position;
3598
3616
  const anchor = restoredDrawingAnchor(attrs.anchor);
3599
3617
  if (anchor) shape.anchor = anchor;
3618
+ if (attrs._docxGroupChild) shape.groupChild = {
3619
+ path: [...attrs._docxGroupChild.path],
3620
+ group: attrs._docxGroupChild.group,
3621
+ content: attrs._docxGroupChild.content
3622
+ };
3600
3623
  const run = {
3601
3624
  type: "run",
3602
- content: [{
3603
- type: "shape",
3604
- shape
3605
- }]
3625
+ content: [shapeContentFromNode({
3626
+ node,
3627
+ shape,
3628
+ captured: attrs._docxAlternateContent
3629
+ })]
3606
3630
  };
3607
3631
  const trackedChange = attrs._docxTrackedChange;
3608
3632
  const inlineSdts = attrs._docxInlineSdts ?? [];
@@ -4,6 +4,7 @@ import { isInlineSdtContent, isTrackedChangeWrapperChild } from "../../docx/inli
4
4
  import { mergeParagraphNumbering as mergeParagraphNumbering$1, paragraphNumberingReferenceId as paragraphNumberingReferenceId$1 } from "../../docx/numberingReference.js";
5
5
  import { PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR, createProseParagraphWithPropertySource, getDocumentParagraphPropertySourceContract, proseParagraphAttrsWithoutPropertySource, recreateProseNodeWithParagraphPropertySource, transportTableCellsWithParagraphPropertySources } from "../../docx/paragraphPropertySource.js";
6
6
  import { isBaselineVertAlign } from "../../docx/runParser.js";
7
+ import { replayableShapeAlternateContent } from "../../docx/shapeAlternateContent.js";
7
8
  import { resolveTableLook } from "../../docx/tableLook.js";
8
9
  import { isCellMergeContinuation } from "../../docx/tableParser.js";
9
10
  import { copiedWrapPolygon } from "../../docx/wrapPolygon.js";
@@ -16,9 +17,11 @@ import { mergeParagraphFormatting, mergeParagraphTabStops } from "../../utils/pa
16
17
  import { tableOfContentsStyleLevel } from "../../utils/tableOfContentsStyle.js";
17
18
  import { mergeTextFormatting } from "../../utils/textFormattingMerge.js";
18
19
  import { emuToPixels, emuToStrokePixels } from "../../utils/units.js";
20
+ import { withAlternateContent } from "../alternateContentAttrs.js";
19
21
  import { expectInlineWrapperMarkAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
20
22
  import { authoredTransformAttrs } from "../authoredTransformAttrs.js";
21
23
  import { setAutospacingBaseValue } from "../autospacingBase.js";
24
+ import { withEnclosingRevision } from "../contentControlRevisions.js";
22
25
  import { planEmptyRanges } from "../emptyRangeAnchor.js";
23
26
  import { INLINE_WRAPPER_MARK_NAME } from "../extensions/marks/InlineWrapperExtension.js";
24
27
  import { textFormattingToMarks } from "../extensions/marks/markUtils.js";
@@ -34,6 +37,7 @@ import { directionFromBidi } from "../paragraphDirection.js";
34
37
  import { styleResolvedParagraphFormatting } from "../paragraphFormattingProvenance.js";
35
38
  import { lineSpacingProvenanceFromSpacing } from "../paragraphSpacing.js";
36
39
  import { RUN_IDENTITY_MARK_NAME, hasRunIdentityPayload, runIdentityAttrs } from "../runIdentity.js";
40
+ import { continueRunIdentitiesAcrossRevisions } from "../runIdentityAcrossRevisions.js";
37
41
  import { getParagraphMarkSuppressionOverrides, hasDirectRunFormatting, resolveParagraphBodyRunFormatting, stripParagraphMarkFormattingForBodyRuns, stripParagraphMarkOnlyFormatting, suppressParagraphMarkFormatting } from "../runStyleFormatting.js";
38
42
  import { schema } from "../schema/index.js";
39
43
  import { PRESERVED_XML_LEVELS } from "../schema/nodes.js";
@@ -86,7 +90,7 @@ const runIdentityMark = (run, nextRunIdentityId) => {
86
90
  const payload = {
87
91
  preservedAttributes: run.preservedAttributes,
88
92
  preserved: run.formatting?.preserved,
89
- emptyFormatting: run.formatting !== void 0 && Object.keys(run.formatting).length === 0 ? true : void 0
93
+ emptyFormatting: run.formatting !== void 0 && Object.keys(run.formatting).length === 0 && (run.propertyChanges?.length ?? 0) === 0 ? true : void 0
90
94
  };
91
95
  if (!runHasPageBreakContent(run) && !runHasMixedContent(run) && !hasRunIdentityPayload(payload)) return null;
92
96
  return schema.mark(RUN_IDENTITY_MARK_NAME, runIdentityAttrs(nextRunIdentityId(), payload));
@@ -577,7 +581,7 @@ function convertParagraph(paragraph, styleResolver, context, extraRunFormatting,
577
581
  if (emptyHyperlinks) attrs._emptyHyperlinks = emptyHyperlinks;
578
582
  return createProseParagraphWithPropertySource(schema.nodes["paragraph"], paragraph, {
579
583
  attrs,
580
- content: inlineNodes
584
+ content: continueRunIdentitiesAcrossRevisions(inlineNodes)
581
585
  });
582
586
  }
583
587
  /**
@@ -609,16 +613,20 @@ function anchorPointComment(nodes, commentId) {
609
613
  * One node of a revision's content with the revision recorded on it.
610
614
  *
611
615
  * An inline content control is an `inline*` node rather than an atom, so it is
612
- * not a run carrier: the revision goes on the leaves it holds. That is what
613
- * makes `w:ins > w:sdt` and `w:sdt > w:ins` the same marks on the same leaves,
614
- * and it is what lets the save leg hoist a revision covering all of them back
615
- * around the control instead of losing it.
616
+ * not a run carrier: the revision goes on the leaves it holds, and the control
617
+ * records that the revision encloses it. The leaves alone would read the same
618
+ * for `w:ins > w:sdt` and `w:sdt > w:ins`; the record is what lets the save leg
619
+ * write the revision back around the control, and resolving it remove the
620
+ * control, only for the first.
616
621
  */
617
622
  const withTrackedRunMark = (node, mark, ancestor) => {
618
623
  if (node.type.name === "sdt") {
619
624
  const marked = [];
620
625
  for (let index = 0; index < node.childCount; index += 1) marked.push(withTrackedRunMark(node.child(index), mark, ancestor));
621
- return recreateProseNodeWithParagraphPropertySource(node, { content: marked });
626
+ return recreateProseNodeWithParagraphPropertySource(node, {
627
+ attrs: withEnclosingRevision(node, ancestor.revisionId) ?? node.attrs,
628
+ content: marked
629
+ });
622
630
  }
623
631
  const nestedMark = node.marks.find(({ type }) => type.name === "insertion" || type.name === "deletion");
624
632
  if (nestedMark) {
@@ -1547,7 +1555,8 @@ function convertField(field, { getInheritedRunFormatting, styleResolver, nextHyp
1547
1555
  displayText,
1548
1556
  fieldKind: field.type === "simpleField" ? "simple" : "complex",
1549
1557
  fldLock: field.fldLock ?? null,
1550
- dirty: field.dirty ?? null
1558
+ dirty: field.dirty ?? null,
1559
+ fieldResultIsFallback: field.type === "complexField" ? field.fieldResultIsFallback ?? null : null
1551
1560
  }, createStructuredField ? inlineNodes : void 0, marks);
1552
1561
  }
1553
1562
  /**
@@ -2163,7 +2172,7 @@ function convertRunContent(content, marks, formatting, textBoxAnchors, authoredF
2163
2172
  const anchorId = textBoxAnchors?.get(shp);
2164
2173
  return anchorId ? [schema.node("textBoxAnchor", { anchorId }).mark(marks)] : [];
2165
2174
  }
2166
- return [withRunBoundaryMarks(convertShape(shp, carriedRunFormatting(authoredFormatting)), marks)];
2175
+ return [withRunBoundaryMarks(withAlternateContent(convertShape(shp, carriedRunFormatting(authoredFormatting)), replayableShapeAlternateContent(content)), marks)];
2167
2176
  }
2168
2177
  case "footnoteRef": {
2169
2178
  const footnoteMark = schema.mark("footnoteRef", {
@@ -2214,6 +2223,12 @@ function withRunBoundaryMarks(node, marks) {
2214
2223
  */
2215
2224
  const authoredEmuAttrs = (values) => Object.values(values).some((value) => value !== void 0) ? values : void 0;
2216
2225
  /**
2226
+ * A text box's gradient fill, copied for the reason {@link copiedDrawingAnchor}
2227
+ * is: ProseMirror keeps object-valued attrs by reference. Any other fill is
2228
+ * stated by `fillColor`, or not at all.
2229
+ */
2230
+ const copiedGradientFill = (fill) => fill?.type === "gradient" && fill.gradient !== void 0 ? structuredClone(fill) : void 0;
2231
+ /**
2217
2232
  * A copy of the anchor record: ProseMirror keeps object-valued attrs by
2218
2233
  * reference, so sharing it with the source would let a mutation of either reach
2219
2234
  * the other outside a transaction.
@@ -2535,16 +2550,19 @@ function convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId, c
2535
2550
  const keepWrapperParagraph = isEmptyAfterExtraction && (preserveEmptyWrapper === true || hasParagraphBoundaryPayload(block, pmParagraph));
2536
2551
  if (!isEmptyAfterExtraction || keepWrapperParagraph) nodes.push(pmParagraph);
2537
2552
  const standalone = isEmptyAfterExtraction && !keepWrapperParagraph;
2538
- for (const [index, { textBox, anchorId, trackedChange, inlineSdts }] of textBoxes.entries()) nodes.push(convertTextBox(textBox, styleResolver, {
2539
- placement: standalone ? "standalone" : "inlineWithPrevious",
2540
- groupId: textBoxGroupId,
2541
- anchorId,
2542
- context,
2543
- trackedChange,
2544
- inlineSdts,
2545
- ...standalone && index === 0 && block.preservedAttributes ? { hostPreservedAttributes: block.preservedAttributes } : {},
2546
- ...standalone && index === 0 ? { hostParagraph: hostParagraphAttrs(pmParagraph.attrs) } : {}
2547
- }));
2553
+ for (const [index, { textBox, alternateContentXml, anchorId, trackedChange, inlineSdts }] of textBoxes.entries()) {
2554
+ const textBoxNode = convertTextBox(textBox, styleResolver, {
2555
+ placement: standalone ? "standalone" : "inlineWithPrevious",
2556
+ groupId: textBoxGroupId,
2557
+ anchorId,
2558
+ context,
2559
+ trackedChange,
2560
+ inlineSdts,
2561
+ ...standalone && index === 0 && block.preservedAttributes ? { hostPreservedAttributes: block.preservedAttributes } : {},
2562
+ ...standalone && index === 0 ? { hostParagraph: hostParagraphAttrs(pmParagraph.attrs) } : {}
2563
+ });
2564
+ nodes.push(withAlternateContent(textBoxNode, alternateContentXml));
2565
+ }
2548
2566
  return nodes;
2549
2567
  }
2550
2568
  /**
@@ -2602,6 +2620,7 @@ function extractTextBoxes(paragraph, textBoxGroupId) {
2602
2620
  textBoxAnchors.set(runContent.shape, anchorId);
2603
2621
  textBoxes.push({
2604
2622
  textBox: textBoxFromShape(runContent.shape, runContent.shape.textBody),
2623
+ alternateContentXml: replayableShapeAlternateContent(runContent),
2605
2624
  anchorId,
2606
2625
  trackedChange: context.trackedChange,
2607
2626
  inlineSdts: context.inlineSdts
@@ -2694,6 +2713,7 @@ function textBoxFromShape(shape, textBody) {
2694
2713
  if (textBody.textWrap) textBox.textWrap = textBody.textWrap;
2695
2714
  if (textBody.anchor) textBox.verticalAlign = textBody.anchor;
2696
2715
  if (textBody.wordArt) textBox.wordArt = textBody.wordArt;
2716
+ if (shape.groupChild) textBox.groupChild = shape.groupChild;
2697
2717
  return textBox;
2698
2718
  }
2699
2719
  /**
@@ -2706,6 +2726,7 @@ function convertTextBox(textBox, styleResolver, options) {
2706
2726
  const heightPx = textBoxSize?.height ? emuToPixels(textBoxSize.height) : void 0;
2707
2727
  let fillColor;
2708
2728
  if (textBox.fill?.color?.rgb) fillColor = `#${textBox.fill.color.rgb}`;
2729
+ const gradientFill = copiedGradientFill(textBox.fill);
2709
2730
  let outlineWidth;
2710
2731
  let outlineColor;
2711
2732
  let outlineStyle;
@@ -2796,6 +2817,7 @@ function convertTextBox(textBox, styleResolver, options) {
2796
2817
  alt: textBox.alt,
2797
2818
  title: textBox.title,
2798
2819
  fillColor,
2820
+ gradientFill,
2799
2821
  outlineWidth,
2800
2822
  outlineColor,
2801
2823
  outlineStyle,
@@ -2816,6 +2838,7 @@ function convertTextBox(textBox, styleResolver, options) {
2816
2838
  _docxPlacement: options.placement,
2817
2839
  _docxGroupId: options.groupId,
2818
2840
  _docxAnchorId: options.anchorId,
2841
+ _docxGroupChild: textBox.groupChild,
2819
2842
  _docxTextBodyContentState: textBox.content.length === 0 ? { type: "source-empty" } : { type: "authored" },
2820
2843
  _docxTrackedChange: options.trackedChange,
2821
2844
  _docxInlineSdts: options.inlineSdts.length > 0 ? options.inlineSdts : void 0,
@@ -8,13 +8,13 @@ declare const autoBidiDetectionKey: PluginKey<any>;
8
8
  * fire for the initial document, so seeded content needs this pass.
9
9
  *
10
10
  * Like the paraId allocator this is `ignoreTrackedChanges`, so a normalized
11
- * paragraph the user never edits is not in `changedParaIds`. Full save (the
12
- * default; and the only path for Markdown/template/new docs, which have no
13
- * original buffer) serializes the whole PM doc, so the flag is written. The
14
- * selective-save path (dark `selectiveSave` flag) intentionally preserves
15
- * untouched original paragraphs byte-for-byte, so an imported DOCX whose Arabic
16
- * arrived without `w:bidi` and is saved without any edit keeps the original
17
- * bytes; editing the paragraph tracks it and the patch then includes `w:bidi`.
11
+ * paragraph the user never edits is not in `changedParaIds`. An imported DOCX
12
+ * whose Arabic arrived without `w:bidi` gets `{ source: "auto" }` here purely
13
+ * for rendering; because `directionToAuthoredBidi` treats `"auto"` the same
14
+ * as undecided, neither a full-repack save nor the selective-save path (dark
15
+ * `selectiveSave` flag) writes `w:bidi` for it when the paragraph is not
16
+ * otherwise edited. A manual toggle, or a paragraph whose `w:bidi` the source
17
+ * document already carried, is unaffected — it always round-trips.
18
18
  */
19
19
  declare const ensureBaseDirectionInState: (state: EditorState) => EditorState;
20
20
  declare const AutoBidiDetectionExtension: (options?: Partial<{}> | undefined) => Extension;
@@ -80,13 +80,13 @@ const applyBidiUpdates = (state, updates) => {
80
80
  * fire for the initial document, so seeded content needs this pass.
81
81
  *
82
82
  * Like the paraId allocator this is `ignoreTrackedChanges`, so a normalized
83
- * paragraph the user never edits is not in `changedParaIds`. Full save (the
84
- * default; and the only path for Markdown/template/new docs, which have no
85
- * original buffer) serializes the whole PM doc, so the flag is written. The
86
- * selective-save path (dark `selectiveSave` flag) intentionally preserves
87
- * untouched original paragraphs byte-for-byte, so an imported DOCX whose Arabic
88
- * arrived without `w:bidi` and is saved without any edit keeps the original
89
- * bytes; editing the paragraph tracks it and the patch then includes `w:bidi`.
83
+ * paragraph the user never edits is not in `changedParaIds`. An imported DOCX
84
+ * whose Arabic arrived without `w:bidi` gets `{ source: "auto" }` here purely
85
+ * for rendering; because `directionToAuthoredBidi` treats `"auto"` the same
86
+ * as undecided, neither a full-repack save nor the selective-save path (dark
87
+ * `selectiveSave` flag) writes `w:bidi` for it when the paragraph is not
88
+ * otherwise edited. A manual toggle, or a paragraph whose `w:bidi` the source
89
+ * document already carried, is unaffected — it always round-trips.
90
90
  */
91
91
  const ensureBaseDirectionInState = (state) => {
92
92
  if (!autoBidiDetectionKey.get(state)) return state;
@@ -9,7 +9,8 @@ const createFieldAttrs = () => ({
9
9
  _numberedRefBaseline: { default: void 0 },
10
10
  fieldKind: { default: "simple" },
11
11
  fldLock: { default: null },
12
- dirty: { default: null }
12
+ dirty: { default: null },
13
+ fieldResultIsFallback: { default: null }
13
14
  });
14
15
  /** A `data-` flag a pasted field carried: absent states nothing. */
15
16
  const statedFlag = (value) => value === void 0 ? null : value === "true";
@@ -19,10 +20,11 @@ const readFieldDomAttrs = (dom) => ({
19
20
  displayText: dom.textContent ?? "",
20
21
  fieldKind: dom.dataset["fieldKind"] ?? "simple",
21
22
  fldLock: statedFlag(dom.dataset["fldLock"]),
22
- dirty: statedFlag(dom.dataset["dirty"])
23
+ dirty: statedFlag(dom.dataset["dirty"]),
24
+ fieldResultIsFallback: statedFlag(dom.dataset["fieldResultIsFallback"])
23
25
  });
24
26
  const getFieldDomAttrs = (node) => {
25
- const { fieldType, instruction, fieldKind, fldLock, dirty } = expectFieldAttrs(node);
27
+ const { fieldType, instruction, fieldKind, fldLock, dirty, fieldResultIsFallback } = expectFieldAttrs(node);
26
28
  return {
27
29
  class: `docx-field docx-field-${fieldType.toLowerCase()}`,
28
30
  "data-field-type": fieldType,
@@ -30,6 +32,7 @@ const getFieldDomAttrs = (node) => {
30
32
  "data-field-kind": fieldKind,
31
33
  ...fldLock === void 0 ? {} : { "data-fld-lock": String(fldLock) },
32
34
  ...dirty === void 0 ? {} : { "data-dirty": String(dirty) },
35
+ ...fieldResultIsFallback === void 0 ? {} : { "data-field-result-is-fallback": String(fieldResultIsFallback) },
33
36
  style: "outline: 1px solid var(--doc-field-outline, rgba(200,200,200,0.4)); padding: 0 1px; border-radius: 2px;"
34
37
  };
35
38
  };
@@ -54,7 +54,13 @@ const SdtExtension = createNodeExtension({
54
54
  * `w:sdtEndPr` as a record, so a control the editor rebuilds still
55
55
  * writes its end mark once the captured bytes are gone.
56
56
  */
57
- endProperties: { default: null }
57
+ endProperties: { default: null },
58
+ /**
59
+ * The revisions whose element encloses the control (`w:ins > w:sdt`),
60
+ * as opposed to revising the content inside it (`w:sdt > w:ins`). Both
61
+ * put the same marks on the leaves; see `contentControlRevisions.ts`.
62
+ */
63
+ _docxEnclosingRevisionIds: { default: null }
58
64
  },
59
65
  parseDOM: [{
60
66
  tag: "span.docx-sdt",
@@ -78,7 +84,8 @@ const SdtExtension = createNodeExtension({
78
84
  checked: onOffFromDataset(el.dataset["checked"]),
79
85
  _preserved: null,
80
86
  rawEndPropertiesXml: null,
81
- endProperties: null
87
+ endProperties: null,
88
+ _docxEnclosingRevisionIds: null
82
89
  };
83
90
  }
84
91
  }],
@@ -370,6 +370,7 @@ const ShapeExtension = createNodeExtension({
370
370
  shapeType: { default: "rect" },
371
371
  geometryAdjustments: { default: null },
372
372
  _docxRunFormatting: { default: null },
373
+ _docxAlternateContent: { default: null },
373
374
  shapeId: { default: null },
374
375
  shapeName: { default: null },
375
376
  alt: { default: null },
@@ -86,6 +86,7 @@ const TextBoxExtension = createNodeExtension({
86
86
  alt: { default: null },
87
87
  title: { default: null },
88
88
  fillColor: { default: null },
89
+ gradientFill: { default: null },
89
90
  outlineWidth: { default: null },
90
91
  outlineColor: { default: null },
91
92
  outlineStyle: { default: null },
@@ -111,9 +112,11 @@ const TextBoxExtension = createNodeExtension({
111
112
  wrapDistanceSlots: { default: null },
112
113
  wrapEffectExtentSlots: { default: null },
113
114
  wrapPolygon: { default: null },
115
+ _docxAlternateContent: { default: null },
114
116
  _docxPlacement: { default: null },
115
117
  _docxGroupId: { default: null },
116
118
  _docxAnchorId: { default: null },
119
+ _docxGroupChild: { default: null },
117
120
  _docxTextBodyContentState: { default: { type: "authored" } },
118
121
  _docxTrackedChange: { default: null },
119
122
  _docxInlineSdts: { default: null },
@@ -1,4 +1,4 @@
1
- import { convertBulletToUnicode } from "../docx/bulletMarkers.js";
1
+ import { bulletMarkerFontName, convertBulletToUnicode } from "../docx/bulletMarkers.js";
2
2
  import { paragraphNumberingLevel, paragraphNumberingReferenceId, sameStatedParagraphNumbering } from "../docx/numberingReference.js";
3
3
  import { formatOoxmlCounter } from "../docx/ooxmlCounterFormatter.js";
4
4
  import { isListNumPr } from "../layout-engine/types.js";
@@ -205,7 +205,7 @@ function advanceListMarker(attrs, state) {
205
205
  fromStyle: false,
206
206
  numId: null
207
207
  };
208
- return convertBulletToUnicode(attrs.listMarker ?? markerTemplate ?? "");
208
+ return convertBulletToUnicode(attrs.listMarker ?? markerTemplate ?? "", bulletMarkerFontName(attrs.listMarkerFormatting));
209
209
  }
210
210
  const firstInstanceEncounter = !state.counters.has(numId);
211
211
  let counters = state.counters.get(numId) ?? Array.from({ length: 9 }, () => NaN);
@@ -15,6 +15,17 @@
15
15
  * The persisted/serialized model keeps the flat OOXML tri-state
16
16
  * (`ParagraphFormatting.bidi: boolean | undefined`); `directionToBidi` /
17
17
  * `directionFromBidi` bridge the two at the conversion boundary.
18
+ *
19
+ * `source: "auto"` is a *rendering* decision only — it makes the layout
20
+ * painter and the editor's DOM shape the paragraph RTL, so newly typed or
21
+ * pasted directional text displays correctly before the user ever makes an
22
+ * explicit choice. It must never reach the saved package on its own: a
23
+ * paragraph that arrived without `w:bidi` and was never touched by the user
24
+ * must save without `w:bidi` too, no matter how many auto-detection passes
25
+ * ran over it on load. Only `source: "manual"` (an explicit user toggle, or
26
+ * a `w:bidi` the source document already carried) is authored content and
27
+ * belongs in `w:pPr`. Use `directionToBidi` for layout/rendering and
28
+ * `directionToAuthoredBidi` for anything that gets serialized.
18
29
  */
19
30
  type ParagraphDirection = {
20
31
  source: "auto";
@@ -29,8 +40,24 @@ declare const directionIsRtl: (direction: ParagraphDirection | null | undefined)
29
40
  * AutoBidiDetection may (re-)evaluate. A manual decision is left untouched.
30
41
  */
31
42
  declare const directionIsAutoManaged: (direction: ParagraphDirection | null | undefined) => boolean;
32
- /** Map a direction to the serialized OOXML `w:bidi` tri-state. */
43
+ /**
44
+ * Map a direction to the OOXML `w:bidi` tri-state for layout/rendering: an
45
+ * auto-detected direction lays out RTL exactly like a manual one. Do not use
46
+ * this to decide what gets written to the saved package — see
47
+ * `directionToAuthoredBidi`.
48
+ */
33
49
  declare const directionToBidi: (direction: ParagraphDirection | null | undefined) => boolean | undefined;
50
+ /**
51
+ * Map a direction to the OOXML `w:bidi` tri-state as *authored* content, for
52
+ * serialization. Only a manual decision (explicit user toggle, or a `w:bidi`
53
+ * the source document already carried) is ever written: `undefined` here
54
+ * means "the paragraph does not state its own direction," which keeps
55
+ * `w:pPr` free of a flag the user never asked for. An auto-detected
56
+ * direction (`source: "auto"`) resolves to `undefined` — same as undecided —
57
+ * regardless of which way detection currently leans, so it stays a view-only
58
+ * aid until the user makes it explicit.
59
+ */
60
+ declare const directionToAuthoredBidi: (direction: ParagraphDirection | null | undefined) => boolean | undefined;
34
61
  /**
35
62
  * Reconstruct a direction from a model `bidi` value (DOCX import / load): an
36
63
  * explicit `true`/`false` is a manual decision, absence is undecided.
@@ -39,4 +66,4 @@ declare const directionFromBidi: (bidi: boolean | null | undefined) => Paragraph
39
66
  /** Runtime guard for the PM attribute validator. */
40
67
  declare const isParagraphDirection: (value: unknown) => value is ParagraphDirection;
41
68
  //#endregion
42
- export { ParagraphDirection, directionFromBidi, directionIsAutoManaged, directionIsRtl, directionToBidi, isParagraphDirection };
69
+ export { ParagraphDirection, directionFromBidi, directionIsAutoManaged, directionIsRtl, directionToAuthoredBidi, directionToBidi, isParagraphDirection };
@@ -6,13 +6,32 @@ const directionIsRtl = (direction) => direction?.source === "auto" || direction?
6
6
  * AutoBidiDetection may (re-)evaluate. A manual decision is left untouched.
7
7
  */
8
8
  const directionIsAutoManaged = (direction) => direction == null || direction.source === "auto";
9
- /** Map a direction to the serialized OOXML `w:bidi` tri-state. */
9
+ /**
10
+ * Map a direction to the OOXML `w:bidi` tri-state for layout/rendering: an
11
+ * auto-detected direction lays out RTL exactly like a manual one. Do not use
12
+ * this to decide what gets written to the saved package — see
13
+ * `directionToAuthoredBidi`.
14
+ */
10
15
  const directionToBidi = (direction) => {
11
16
  if (direction == null) return;
12
17
  if (direction.source === "auto") return true;
13
18
  return direction.value === "rtl";
14
19
  };
15
20
  /**
21
+ * Map a direction to the OOXML `w:bidi` tri-state as *authored* content, for
22
+ * serialization. Only a manual decision (explicit user toggle, or a `w:bidi`
23
+ * the source document already carried) is ever written: `undefined` here
24
+ * means "the paragraph does not state its own direction," which keeps
25
+ * `w:pPr` free of a flag the user never asked for. An auto-detected
26
+ * direction (`source: "auto"`) resolves to `undefined` — same as undecided —
27
+ * regardless of which way detection currently leans, so it stays a view-only
28
+ * aid until the user makes it explicit.
29
+ */
30
+ const directionToAuthoredBidi = (direction) => {
31
+ if (direction == null || direction.source === "auto") return;
32
+ return direction.value === "rtl";
33
+ };
34
+ /**
16
35
  * Reconstruct a direction from a model `bidi` value (DOCX import / load): an
17
36
  * explicit `true`/`false` is a manual decision, absence is undecided.
18
37
  */
@@ -30,4 +49,4 @@ const isParagraphDirection = (value) => {
30
49
  return value.source === "manual" && "value" in value && (value.value === "rtl" || value.value === "ltr");
31
50
  };
32
51
  //#endregion
33
- export { directionFromBidi, directionIsAutoManaged, directionIsRtl, directionToBidi, isParagraphDirection };
52
+ export { directionFromBidi, directionIsAutoManaged, directionIsRtl, directionToAuthoredBidi, directionToBidi, isParagraphDirection };