@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
@@ -676,6 +676,81 @@ const FIELD_INLINE_WRAPPER_HANDLERS = {
676
676
  pPr: CAPTURE
677
677
  };
678
678
  /**
679
+ * The container children a complex field's result may hold and still be
680
+ * assembled into a `ComplexField`: its runs and the zero-width markers read
681
+ * beside them. Any other child is inline content (a hyperlink, a simple field,
682
+ * a content control, a revision, a wrapper, an equation) that `fieldResult`
683
+ * has no place for.
684
+ */
685
+ const FIELD_TRANSPARENT_CHILDREN = /* @__PURE__ */ new Set([
686
+ "r",
687
+ "pPr",
688
+ "smartTagPr",
689
+ "customXmlPr",
690
+ "bookmarkStart",
691
+ "bookmarkEnd",
692
+ "commentRangeStart",
693
+ "commentRangeEnd",
694
+ "moveFromRangeStart",
695
+ "moveFromRangeEnd",
696
+ "moveToRangeStart",
697
+ "moveToRangeEnd",
698
+ "proofErr",
699
+ "permStart",
700
+ "permEnd",
701
+ "customXmlDelRangeStart",
702
+ "customXmlDelRangeEnd",
703
+ "customXmlInsRangeStart",
704
+ "customXmlInsRangeEnd",
705
+ "customXmlMoveFromRangeStart",
706
+ "customXmlMoveFromRangeEnd",
707
+ "customXmlMoveToRangeStart",
708
+ "customXmlMoveToRangeEnd"
709
+ ]);
710
+ const isWordprocessingChild = (child) => {
711
+ const namespace = getNamespaceUri(child);
712
+ return namespace === void 0 || WORDPROCESSINGML_NAMESPACE_URIS.has(namespace);
713
+ };
714
+ /**
715
+ * The `w:r` children whose `begin` opens a complex field that this container's
716
+ * walk closes.
717
+ *
718
+ * A field is assembled from the runs between its `begin` and its `end`, so it
719
+ * exists only when both are runs of this container with nothing between them
720
+ * that a `ComplexField` cannot hold. A TOC opened here and closed in a later
721
+ * paragraph, a field whose result holds a `w:hyperlink` (a TOC's `\h` entries),
722
+ * and an outer field whose result holds a nested field are none of these; their
723
+ * runs are ordinary content, read in source order alongside the siblings
724
+ * between them. Mirrors the `w:r` handler run by run: a run with a `begin`
725
+ * opens a field, and a run with an `end` closes the one open.
726
+ */
727
+ const assembledFieldBeginsOf = (container) => {
728
+ const assembled = /* @__PURE__ */ new Set();
729
+ let open;
730
+ for (const child of getChildElements(container)) {
731
+ const wordprocessing = isWordprocessingChild(child);
732
+ const localName = getLocalName(child.name);
733
+ if (!wordprocessing || localName !== "r") {
734
+ if (!wordprocessing || !FIELD_TRANSPARENT_CHILDREN.has(localName)) open = void 0;
735
+ continue;
736
+ }
737
+ let hasBegin = false;
738
+ let hasEnd = false;
739
+ for (const runChild of getChildElements(child)) {
740
+ if (getLocalName(runChild.name) !== "fldChar" || !isWordprocessingChild(runChild)) continue;
741
+ const charType = getAttribute(runChild, "w", "fldCharType");
742
+ if (charType === "end") hasEnd = true;
743
+ else if (charType !== "separate") hasBegin = true;
744
+ }
745
+ if (hasBegin) open = child;
746
+ if (hasEnd && open !== void 0) {
747
+ assembled.add(open);
748
+ open = void 0;
749
+ }
750
+ }
751
+ return assembled;
752
+ };
753
+ /**
679
754
  * A transparent wrapper at paragraph level. All four hold ordinary inline
680
755
  * content and say something about it rather than about the text, so the
681
756
  * recursion is this same walk and `inlineWrapperOf` reads what the element
@@ -714,8 +789,12 @@ const PARAGRAPH_CONTENT_HANDLERS = {
714
789
  }
715
790
  else if (content.type === "instrText") instrText += content.text;
716
791
  if (hasFieldBegin) {
717
- if (scan.inComplexField && scan.afterSeparator) {
718
- contents.push(...scan.complexFieldResultRuns);
792
+ scan.assembledFieldBegins ??= assembledFieldBeginsOf(scan.container);
793
+ if (!scan.assembledFieldBegins.has(child)) hasFieldBegin = false;
794
+ }
795
+ if (hasFieldBegin) {
796
+ if (scan.inComplexField) {
797
+ contents.push(...scan.complexFieldOpenRuns);
719
798
  scan.inComplexField = false;
720
799
  }
721
800
  scan.inComplexField = true;
@@ -751,7 +830,11 @@ const PARAGRAPH_CONTENT_HANDLERS = {
751
830
  }
752
831
  if (hasFieldEnd) {
753
832
  let resultRuns = scan.complexFieldResultRuns;
754
- if (resultRuns.length === 0 && scan.complexFieldFallbackDisplay !== void 0 && isLegacyFormCheckboxInstruction(scan.complexFieldInstr)) resultRuns = [createLegacyFormCheckboxResultRun(scan.complexFieldFallbackDisplay, scan.complexFieldFormatting)];
833
+ let resultIsFallback = false;
834
+ if (resultRuns.length === 0 && scan.complexFieldFallbackDisplay !== void 0 && isLegacyFormCheckboxInstruction(scan.complexFieldInstr)) {
835
+ resultRuns = [createLegacyFormCheckboxResultRun(scan.complexFieldFallbackDisplay, scan.complexFieldFormatting)];
836
+ resultIsFallback = true;
837
+ }
755
838
  if (resultRuns.length === 0 && !scan.afterSeparator && endOriginalValue !== void 0) resultRuns = [{
756
839
  type: "run",
757
840
  content: [{
@@ -767,6 +850,7 @@ const PARAGRAPH_CONTENT_HANDLERS = {
767
850
  fieldResult: resultRuns,
768
851
  ...scan.complexFieldState
769
852
  };
853
+ if (resultIsFallback) complexField.fieldResultIsFallback = true;
770
854
  if (scan.complexFieldFormatting) complexField.formatting = scan.complexFieldFormatting;
771
855
  contents.push(complexField);
772
856
  if (commentReferenceId !== null) contents.push({
@@ -922,7 +1006,9 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
922
1006
  afterSeparator: false,
923
1007
  complexFieldState: {},
924
1008
  complexFieldFallbackDisplay: void 0,
925
- complexFieldFormatting: void 0
1009
+ complexFieldFormatting: void 0,
1010
+ container: paraElement,
1011
+ assembledFieldBegins: void 0
926
1012
  };
927
1013
  const preserved = dispatchChildrenWithContext({
928
1014
  element: paraElement,
@@ -1096,9 +1182,7 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
1096
1182
  if (!paragraph.formatting) paragraph.formatting = {};
1097
1183
  const directInd = pPr ? findChild(pPr, "w", "ind") : null;
1098
1184
  const hasDirectLeft = hasAttributeAnySpelling(directInd, "CT_Ind @left");
1099
- const directFirstLine = directInd ? parseNumericAttribute(directInd, "w", "firstLine") : void 0;
1100
- const directHanging = directInd ? parseNumericAttribute(directInd, "w", "hanging") : void 0;
1101
- const hasDirectFirstLineOrHanging = directFirstLine !== void 0 && directFirstLine !== 0 || directHanging !== void 0 && directHanging !== 0;
1185
+ const hasDirectFirstLineOrHanging = parseNumericAttribute(directInd, "w", "firstLine") !== void 0 || parseNumericAttribute(directInd, "w", "hanging") !== void 0;
1102
1186
  if (!hasDirectLeft && !chainInd.left && level.pPr.indentLeft !== void 0) paragraph.formatting.indentLeft = level.pPr.indentLeft;
1103
1187
  if (!hasDirectFirstLineOrHanging && !chainInd.firstLine && numberingLevelHasMarkerSlot(level)) {
1104
1188
  if (level.pPr.indentFirstLine !== void 0) paragraph.formatting.indentFirstLine = level.pPr.indentFirstLine;
@@ -1,9 +1,12 @@
1
1
  import { pixelsToEmu } from "../utils/units.js";
2
+ import { groupTextContentFingerprint, groupXmlFingerprint } from "./drawingGroupChildren.js";
3
+ import { groupPreviewTextBoxes } from "./groupDrawingParser.js";
2
4
  import { parseParagraph } from "./paragraphParser.js";
3
5
  import { consolidateParagraphContent } from "./runConsolidator.js";
4
- import { getTextBoxContentElement, parseTextBox, parseTextBoxContent, scanRunForTextBoxDrawings } from "./textBoxParser.js";
6
+ import { captureShapeAlternateContent } from "./shapeAlternateContent.js";
7
+ import { getTextBoxContentElement, parseTextBox, parseTextBoxContent, parseTextBoxFromShape, scanRunForTextBoxDrawings } from "./textBoxParser.js";
5
8
  import { isVmlPictParsedByRunParser } from "./vmlImageParser.js";
6
- import { findDeep, getAttribute, getChildElements, getLocalName } from "./xmlParser.js";
9
+ import { findChildByLocalName, findDeep, getAttribute, getChildElements, getLocalName } from "./xmlParser.js";
7
10
  //#region src/docx/paragraphTextBoxEnrichment.ts
8
11
  const VML_HORIZONTAL_RELATIVES = /* @__PURE__ */ new Set([
9
12
  "character",
@@ -134,8 +137,120 @@ const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering,
134
137
  previews,
135
138
  context
136
139
  });
140
+ liftGroupTextBoxes(paragraph.content, {
141
+ styles,
142
+ theme,
143
+ numbering,
144
+ rels,
145
+ media,
146
+ parseTable,
147
+ previews
148
+ });
137
149
  paragraph.content = consolidateParagraphContent(paragraph.content);
138
150
  };
151
+ /** Previews whose text boxes were already lifted, so a second pass adds none. */
152
+ const liftedGroupPreviews = /* @__PURE__ */ new WeakSet();
153
+ /**
154
+ * Lift the text boxes out of every group preview in the paragraph, each into
155
+ * a text box shape right after the group's drawing in the same run.
156
+ *
157
+ * The group keeps drawing its other children; a lifted text box is laid out
158
+ * and painted like any anchored text box, placed at the group's offset plus
159
+ * the child's frame, and it records where in the group it came from so the
160
+ * writer can put its text back there.
161
+ */
162
+ const liftGroupTextBoxes = (content, parsers) => {
163
+ for (const item of content) switch (item.type) {
164
+ case "run":
165
+ liftGroupTextBoxesInRun(item, parsers);
166
+ break;
167
+ case "hyperlink":
168
+ liftGroupTextBoxes(item.children, parsers);
169
+ break;
170
+ case "inlineSdt":
171
+ case "insertion":
172
+ case "deletion":
173
+ case "moveFrom":
174
+ case "moveTo":
175
+ liftGroupTextBoxes(item.content, parsers);
176
+ break;
177
+ default: break;
178
+ }
179
+ };
180
+ const liftGroupTextBoxesInRun = (run, parsers) => {
181
+ if (!run.content.some((item) => item.type === "drawing")) return;
182
+ const lifted = [];
183
+ for (const item of run.content) {
184
+ lifted.push(item);
185
+ if (item.type !== "drawing" || item.rawXml === void 0) continue;
186
+ const frames = groupPreviewTextBoxes(item.image);
187
+ if (!frames || liftedGroupPreviews.has(item.image)) continue;
188
+ liftedGroupPreviews.add(item.image);
189
+ const group = groupXmlFingerprint(item.rawXml);
190
+ for (const frame of frames) {
191
+ const shape = groupTextBoxShape(item.image, frame, group, parsers);
192
+ if (shape) lifted.push({
193
+ type: "shape",
194
+ shape
195
+ });
196
+ }
197
+ }
198
+ run.content = lifted;
199
+ };
200
+ const groupTextBoxShape = (groupImage, frame, group, { styles, theme, numbering, rels, media, parseTable, previews }) => {
201
+ const position = groupImage.position;
202
+ const horizontalOffset = position?.horizontal.posOffset;
203
+ const verticalOffset = position?.vertical.posOffset;
204
+ if (!position || horizontalOffset === void 0 || verticalOffset === void 0) return;
205
+ const size = {
206
+ width: Math.round(frame.width),
207
+ height: Math.round(frame.height)
208
+ };
209
+ const textBox = parseTextBoxFromShape(frame.wsp, size);
210
+ const content = parseTextBoxContent(findChildByLocalName(findChildByLocalName(frame.wsp, "txbx"), "txbxContent"), parseParagraph, parseTable, styles, theme, numbering, rels, media, previews);
211
+ const transform = frame.rotation !== 0 || frame.flipH || frame.flipV ? {
212
+ ...frame.rotation !== 0 ? { rotation: frame.rotation } : {},
213
+ ...frame.flipH ? { flipH: true } : {},
214
+ ...frame.flipV ? { flipV: true } : {}
215
+ } : void 0;
216
+ const shape = {
217
+ type: "shape",
218
+ shapeType: "textBox",
219
+ size,
220
+ ...textBox?.name !== void 0 ? { name: textBox.name } : {},
221
+ ...textBox?.alt !== void 0 ? { alt: textBox.alt } : {},
222
+ ...textBox?.title !== void 0 ? { title: textBox.title } : {},
223
+ position: {
224
+ horizontal: {
225
+ relativeTo: position.horizontal.relativeTo,
226
+ posOffset: Math.round(horizontalOffset + frame.x)
227
+ },
228
+ vertical: {
229
+ relativeTo: position.vertical.relativeTo,
230
+ posOffset: Math.round(verticalOffset + frame.y)
231
+ }
232
+ },
233
+ wrap: { type: groupImage.wrap.type === "behind" ? "behind" : "inFront" },
234
+ ...groupImage.anchor !== void 0 ? { anchor: { ...groupImage.anchor } } : {},
235
+ ...textBox?.fill !== void 0 ? { fill: textBox.fill } : {},
236
+ ...textBox?.outline !== void 0 ? { outline: textBox.outline } : {},
237
+ ...transform !== void 0 ? { transform } : {},
238
+ textBody: {
239
+ content,
240
+ ...textBox?.autoFit !== void 0 ? { autoFit: textBox.autoFit } : {},
241
+ ...textBox?.textWrap !== void 0 ? { textWrap: textBox.textWrap } : {},
242
+ ...textBox?.verticalAlign !== void 0 ? { anchor: textBox.verticalAlign } : {},
243
+ ...textBox?.margins !== void 0 ? { margins: textBox.margins } : {}
244
+ },
245
+ groupChild: {
246
+ path: frame.path,
247
+ group,
248
+ content: groupTextContentFingerprint(content)
249
+ }
250
+ };
251
+ if (textBox?.id) shape.id = textBox.id;
252
+ return shape;
253
+ };
139
254
  const trackedChangeTypeFromXml = (localName) => {
140
255
  if (localName === "ins") return "insertion";
141
256
  if (localName === "del") return "deletion";
@@ -199,7 +314,7 @@ const enrichTextBoxRuns = ({ content, xmlChildren, styles, theme, numbering, rel
199
314
  const targetRun = parsedRun ?? (hasNonTextBoxContent ? lastConsumedRun : void 0);
200
315
  const targetRunMatchesXml = targetRun !== void 0 && (hasNonTextBoxContent || parsedRun?.content.length === 0);
201
316
  const fillsEmptyCarrier = targetRunMatchesXml && !hasNonTextBoxContent && parsedRun !== void 0;
202
- for (const runEl of textBoxDrawings) {
317
+ for (const { drawing: runEl, alternateContent: source } of textBoxDrawings) {
203
318
  const textBox = parseTextBox(runEl, context);
204
319
  if (!textBox) continue;
205
320
  const wsp = findDeep(runEl, "wps", "wsp");
@@ -229,9 +344,16 @@ const enrichTextBoxRuns = ({ content, xmlChildren, styles, theme, numbering, rel
229
344
  }
230
345
  };
231
346
  if (textBox.id) shape.id = textBox.id;
347
+ const alternateContent = source && captureShapeAlternateContent({
348
+ shape,
349
+ alternateContent: source.element,
350
+ branch: source.branch,
351
+ drawing: runEl
352
+ });
232
353
  const shapeContent = {
233
354
  type: "shape",
234
- shape
355
+ shape,
356
+ ...alternateContent ? { alternateContent } : {}
235
357
  };
236
358
  if (targetRunMatchesXml) targetRun.content.push(shapeContent);
237
359
  else {
@@ -10,6 +10,7 @@ import { EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSche
10
10
  import { preserveRunChild } from "./preservedRunContent.js";
11
11
  import { standalonePreviewLedger } from "./previewBudget.js";
12
12
  import { parseShading } from "./shadingParser.js";
13
+ import { captureShapeAlternateContent } from "./shapeAlternateContent.js";
13
14
  import { parseShapeFromDrawing, shouldPreserveRawShapeDrawing } from "./shapeParser.js";
14
15
  import { isTextBoxDrawing } from "./textBoxParser.js";
15
16
  import { parseThemeColorAttribute } from "./themeColorAttribute.js";
@@ -696,6 +697,15 @@ function parseRunContents(runElement, rels, media, previews, rootXmlns = {}) {
696
697
  const innerDrawing = parseDrawingContent(innerChild, rels, media, previews);
697
698
  if (innerDrawing) {
698
699
  if (innerDrawing.type === "drawing" && (innerDrawing.rawXml !== void 0 || !innerDrawing.image.src)) innerDrawing.rawXml = captureVerbatimXml(child);
700
+ if (innerDrawing.type === "shape") {
701
+ const alternateContent = captureShapeAlternateContent({
702
+ shape: innerDrawing.shape,
703
+ alternateContent: child,
704
+ branch: targetEl,
705
+ drawing: innerChild
706
+ });
707
+ if (alternateContent) innerDrawing.alternateContent = alternateContent;
708
+ }
699
709
  contents.push(innerDrawing);
700
710
  }
701
711
  } else if (innerName === "pict") {
@@ -2,8 +2,11 @@
2
2
  /**
3
3
  * Selective Save Feature Flags
4
4
  *
5
- * Operational controls for the selective save path. Default values keep the
6
- * existing full-repack behavior in place so hosts opt in explicitly.
5
+ * Operational controls for the selective save path. Selective save is on by
6
+ * default: every save attempts it first and falls back to a full repack
7
+ * whenever the patch-safety checks refuse (structural changes, untracked
8
+ * changes, oversized buffers, or an unmodelled part). Hosts that need the
9
+ * previous behavior can opt back into full repacking on every save.
7
10
  *
8
11
  * - `selectiveSave` — gate the selective branch entirely.
9
12
  * - `selectiveSaveTripwire` — run selective + full and compare bytes for CI
@@ -13,7 +16,11 @@
13
16
  * the dominant cost.
14
17
  */
15
18
  type FolioSelectiveSaveFlags = {
16
- /** Enable the selective save path. Default: false (full repack only). */
19
+ /**
20
+ * Enable the selective save path. Default: true. A save always succeeds:
21
+ * when the patch-safety checks refuse, the caller falls back to a full
22
+ * repack automatically. Set to `false` to always full-repack.
23
+ */
17
24
  selectiveSave?: boolean;
18
25
  /**
19
26
  * Run selective save and full repack on every save, compare bytes, and emit
@@ -2,7 +2,7 @@
2
2
  const DEFAULT_SELECTIVE_SAVE_MAX_BYTES = 100 * 1024 * 1024;
3
3
  function resolveSelectiveSaveFlags(flags) {
4
4
  return {
5
- selectiveSave: flags?.selectiveSave ?? false,
5
+ selectiveSave: flags?.selectiveSave ?? true,
6
6
  selectiveSaveTripwire: flags?.selectiveSaveTripwire ?? false,
7
7
  selectiveSaveMaxBytes: flags?.selectiveSaveMaxBytes ?? 104857600
8
8
  };
@@ -54,22 +54,48 @@ type ParagraphOffsets = {
54
54
  * function would return null.
55
55
  */
56
56
  declare function buildParagraphOffsetIndex(xml: string): Map<string, ParagraphOffsets>;
57
- /** One `<w:p>` of a part: its byte range and the paraId its open tag carries. */
57
+ /**
58
+ * The containers a `<w:p>` sits in, as far as a splice has to care.
59
+ *
60
+ * A paragraph is addressed inside one *story*: the main flow of the part (its
61
+ * body, table cells and block-level content controls included), or the text
62
+ * of a text box (`w:txbxContent`, DrawingML or VML). The two are separate
63
+ * ordinal spaces, because the model can legitimately represent a text box
64
+ * differently from the source (a VML box read as DrawingML, a box it cannot
65
+ * read at all) without that saying anything about the main flow.
66
+ *
67
+ * `mc:Fallback` is a copy of its `mc:Choice` for consumers that cannot read
68
+ * the Choice. The model reads the Choice and owns nothing in the Fallback, so
69
+ * a Fallback paragraph is never addressed: it is neither a splice target nor
70
+ * an ordinal, which is also the rule `ensureParaIds` stamps by.
71
+ */
72
+ type ParagraphContainer = {
73
+ /** Inside `mc:Fallback`. */
74
+ inFallback: boolean;
75
+ /** Inside `mc:AlternateContent`, whose Fallback repeats this paragraph. */
76
+ inAlternateContent: boolean;
77
+ /** How many text-box stories (`w:txbxContent`) enclose the paragraph. */
78
+ textBoxDepth: number;
79
+ /** How many table cells (`w:tc`) enclose the paragraph. */
80
+ tableDepth: number;
81
+ };
82
+ /** One `<w:p>` of a part: its byte range, the paraId its open tag carries, and where it sits. */
58
83
  type ScannedParagraph = ParagraphOffsets & {
59
84
  paraId: string | undefined;
85
+ container: ParagraphContainer;
60
86
  };
61
87
  /**
62
88
  * Every `<w:p>` element of `xml`, in the document order of its opening tags,
63
- * with the `w14:paraId` that tag carries.
89
+ * with the `w14:paraId` that tag carries and the containers around it.
64
90
  *
65
- * Document order is what makes the array an ordinal space: the *n*th entry of
66
- * the source part and the *n*th entry of the model's serialization name the
67
- * same paragraph, which is the only way to address a paragraph the producer
68
- * gave no id. A `<w:p>` nested inside another (inside `mc:AlternateContent`,
69
- * a text box) is one entry of its own, exactly as
70
- * {@link countParagraphElements} counts it, so the two never disagree about
71
- * what an ordinal is. An unterminated paragraph keeps `end === start`: it
72
- * occupies its ordinal but no splice can be built from it.
91
+ * Document order is what makes the array an ordinal space: within one story
92
+ * (see {@link ParagraphContainer}), the *n*th paragraph of the source part and
93
+ * the *n*th paragraph of the model's serialization name the same paragraph,
94
+ * which is the only way to address a paragraph the producer gave no id. A
95
+ * `<w:p>` nested inside another (inside `mc:AlternateContent`, a text box) is
96
+ * one entry of its own, exactly as {@link countParagraphElements} counts it.
97
+ * An unterminated paragraph keeps `end === start`: it occupies its ordinal but
98
+ * no splice can be built from it.
73
99
  */
74
100
  declare function scanParagraphs(xml: string): ScannedParagraph[];
75
101
  /**
@@ -85,32 +111,26 @@ type PatchValidationResult = {
85
111
  safe: boolean;
86
112
  reason?: string;
87
113
  };
88
- type PatchSafetyOptions = {
89
- /**
90
- * Require the original and serialized XML to hold the same number of `<w:p>`
91
- * elements. Guards document.xml against structural drift. Notes disable it:
92
- * the model only retains the normal notes, so a serialized note part
93
- * legitimately has fewer paragraphs than the original (which still carries
94
- * the separator notes). See {@link buildPatchedNoteXml}.
95
- */
96
- checkParagraphCount?: boolean;
97
- };
98
114
  /**
99
- * Validate that a selective patch can be safely applied.
115
+ * Validate that a selective patch can be safely applied: every changed
116
+ * paragraph routes to exactly one region of the original part (see
117
+ * {@link routeChangedParagraphs} for what is refused and why).
100
118
  *
101
- * Checks:
102
- * - Every changed paraId routes to one region of the original XML, by its
103
- * authored id or, when the producer wrote none, by its paragraph ordinal
104
- * - All changed paraIds exist in serialized XML (exactly once)
105
- * - Paragraph count matches between original and serialized (unless disabled)
119
+ * The rest of the part does not have to match the model's serialization. The
120
+ * splice keeps every unchanged byte of the source, so a paragraph the model
121
+ * represents differently elsewhere (a text box it re-reads, a Fallback it
122
+ * does not own) is simply kept as the source wrote it.
106
123
  */
107
- declare function validatePatchSafety(originalXml: string, serializedXml: string, changedIds: Set<string>, options?: PatchSafetyOptions): PatchValidationResult;
124
+ declare function validatePatchSafety(originalXml: string, serializedXml: string, changedIds: Set<string>): PatchValidationResult;
108
125
  /**
109
126
  * Build a patched document.xml by splicing new paragraph XML into
110
127
  * the original at the correct offsets. Only changed paragraphs
111
128
  * are replaced; everything else is preserved byte-for-byte.
112
129
  *
113
- * Returns null if any step fails.
130
+ * Returns null when a changed paragraph cannot be routed (see
131
+ * {@link validatePatchSafety}) or {@link spliceXml} refuses the result; the
132
+ * caller then falls back to a full repack, whose parts are all re-serialized
133
+ * from one model.
114
134
  */
115
135
  declare function buildPatchedDocumentXml(originalXml: string, serializedXml: string, changedIds: Set<string>): string | null;
116
136
  /**
@@ -118,11 +138,13 @@ declare function buildPatchedDocumentXml(originalXml: string, serializedXml: str
118
138
  * splicing edited note paragraphs into the original, preserving unchanged
119
139
  * content byte-for-byte.
120
140
  *
121
- * Unlike {@link buildPatchedDocumentXml} this does NOT require the paragraph
122
- * counts to match: the document model only retains the normal notes, so the
123
- * serialized note XML omits the separator / continuationSeparator paragraphs
124
- * the original part still carries. Splicing by `paraId` keeps those separators
125
- * and every unedited note byte-exact while replacing only the edited ones.
141
+ * The routing is the document's: the model only retains the normal notes, so
142
+ * the serialized note XML omits the separator / continuationSeparator
143
+ * paragraphs the original part still carries, and splicing by `paraId` keeps
144
+ * those separators and every unedited note byte-exact while replacing only the
145
+ * edited ones. (An id-less note paragraph does not route here, because the
146
+ * separators misalign its story's ordinals; {@link buildPatchedNotePartXml}
147
+ * addresses it by note instead.)
126
148
  *
127
149
  * Returns null if any changed id is missing or ambiguous in either input, so
128
150
  * the caller can fall back to preserving the original part verbatim.
@@ -241,4 +263,4 @@ declare const patchNumberingDefinitions: ({ originalXml, baselineXml, currentXml
241
263
  */
242
264
  declare function appendNumberingDefs(xml: string, currentXml: string, added: ChangedNumberingDefs): string | null;
243
265
  //#endregion
244
- export { ChangedNumberingDefs, NotePartPatch, NotePartPatchRefusal, ParagraphOffsets, PatchSafetyOptions, PatchValidationResult, ScannedParagraph, XmlSplice, appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, scanParagraphs, spliceXml, validatePatchSafety };
266
+ export { ChangedNumberingDefs, NotePartPatch, NotePartPatchRefusal, ParagraphContainer, ParagraphOffsets, PatchValidationResult, ScannedParagraph, XmlSplice, appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, scanParagraphs, spliceXml, validatePatchSafety };