@stll/folio-core 0.37.1 → 0.37.3

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 (63) hide show
  1. package/dist/ai-edits/headless.d.ts +24 -1
  2. package/dist/ai-edits/headless.js +108 -71
  3. package/dist/ai-edits/read.d.ts +9 -1
  4. package/dist/ai-edits/read.js +34 -16
  5. package/dist/ai-edits/snapshot.d.ts +7 -5
  6. package/dist/ai-edits/snapshot.js +39 -12
  7. package/dist/ai-edits/table-cell-mutations.js +10 -5
  8. package/dist/ai-edits/table-template.js +19 -5
  9. package/dist/compare/compare.d.ts +2 -3
  10. package/dist/compare/compare.js +41 -64
  11. package/dist/compare/content-alignment.js +0 -1
  12. package/dist/compare/scenario.d.ts +11 -2
  13. package/dist/compare/scenario.js +6 -2
  14. package/dist/controller/hiddenEditorManager.js +11 -0
  15. package/dist/controller/layoutPipeline.js +1 -1
  16. package/dist/display-list/build/textBoxPrimitives.js +34 -1
  17. package/dist/display-list/dom/renderDisplayListToDom.js +5 -2
  18. package/dist/display-list/primitives.d.ts +1 -1
  19. package/dist/display-list/types.d.ts +8 -4
  20. package/dist/docx/paragraphPropertySource.d.ts +82 -2
  21. package/dist/docx/paragraphPropertySource.js +569 -3
  22. package/dist/docx/parser.js +9 -0
  23. package/dist/docx/server/materializeYjsDocx.d.ts +1 -1
  24. package/dist/docx/server/materializeYjsDocx.js +21 -2
  25. package/dist/headless-layout.js +1 -1
  26. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +7 -1
  27. package/dist/layout-bridge/convert/headerFooterLayout.js +20 -3
  28. package/dist/layout-bridge/convert/toFlowBlocks.js +43 -12
  29. package/dist/layout-engine/index.js +20 -10
  30. package/dist/layout-engine/measure/measureBlocks.d.ts +7 -3
  31. package/dist/layout-engine/measure/measureBlocks.js +10 -7
  32. package/dist/layout-engine/measure/measureParagraph.d.ts +2 -0
  33. package/dist/layout-engine/measure/measureParagraph.js +1 -1
  34. package/dist/layout-engine/paginator.d.ts +1 -1
  35. package/dist/layout-engine/paginator.js +41 -4
  36. package/dist/layout-engine/textBoxFlow.d.ts +2 -0
  37. package/dist/layout-engine/textBoxFlow.js +9 -5
  38. package/dist/layout-engine/types.d.ts +6 -0
  39. package/dist/layout-painter/documentColors.d.ts +10 -1
  40. package/dist/layout-painter/documentColors.js +16 -1
  41. package/dist/layout-painter/renderParagraph.js +9 -24
  42. package/dist/layout-painter/renderTable.js +3 -3
  43. package/dist/layout-painter/renderTextBox.js +2 -1
  44. package/dist/pdf/pageSpace.d.ts +12 -1
  45. package/dist/pdf/pageSpace.js +24 -1
  46. package/dist/pdf/paint.js +2 -2
  47. package/dist/prosemirror/attrs/index.js +19 -8
  48. package/dist/prosemirror/commands/comments.js +12 -3
  49. package/dist/prosemirror/commands/tableCellMergeResolution.js +18 -11
  50. package/dist/prosemirror/conversion/fromProseDoc.js +94 -21
  51. package/dist/prosemirror/conversion/toProseDoc.js +100 -8
  52. package/dist/prosemirror/extensions/core/DocExtension.js +2 -0
  53. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  54. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +5 -1
  55. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +132 -41
  56. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -0
  57. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  58. package/dist/prosemirror/schema/nodes.d.ts +3 -1
  59. package/dist/prosemirror/yjsParagraphSourceContract.d.ts +9 -0
  60. package/dist/prosemirror/yjsParagraphSourceContract.js +26 -0
  61. package/dist/utils/rotationBoundingBox.d.ts +5 -1
  62. package/dist/utils/rotationBoundingBox.js +9 -1
  63. package/package.json +2 -2
@@ -604,6 +604,8 @@ type TableRow = {
604
604
  isHeader?: boolean;
605
605
  /** `w:cantSplit`: keep this row in one flow region. */
606
606
  cantSplit?: boolean;
607
+ /** A leading authored page break in a cell advances the whole row. */
608
+ breakBefore?: "page";
607
609
  hidden?: boolean;
608
610
  };
609
611
  /**
@@ -802,6 +804,8 @@ type TextBoxBlock = {
802
804
  outlineColor?: string;
803
805
  /** Outline dash style, or `"none"` for an explicit no-outline. */
804
806
  outlineStyle?: OutlineStyleAttr;
807
+ /** DrawingML rotation and/or flips, serialized as CSS transform functions. */
808
+ transform?: string;
805
809
  /** Internal padding */
806
810
  margins?: {
807
811
  top: number;
@@ -1032,6 +1036,8 @@ type FragmentBase = {
1032
1036
  */
1033
1037
  type ParagraphFragment = FragmentBase & {
1034
1038
  kind: "paragraph";
1039
+ /** Structural PM carrier that must not make an otherwise blank page visible. */
1040
+ paginationRole?: "empty-carrier";
1035
1041
  /** First line index (inclusive) from the measure. */
1036
1042
  fromLine: number;
1037
1043
  /** Last line index (exclusive) from the measure. */
@@ -1,4 +1,13 @@
1
1
  //#region src/layout-painter/documentColors.d.ts
2
+ declare const AUTHORED_BACKGROUND_COLOR_VAR = "--doc-authored-background-color";
3
+ declare const AUTHORED_TEXT_COLOR_VAR = "--doc-run-color";
4
+ /** Paint a document text color and expose it to the dark-mode color transform. */
5
+ declare const setAuthoredTextColor: (style: CSSStyleDeclaration, color: string) => void;
6
+ /**
7
+ * Paint an OOXML background and retain its authored color for dark-mode adaptation.
8
+ * The custom property is presentation-only; serialization still uses the model value.
9
+ */
10
+ declare const setAuthoredBackgroundColor: (style: CSSStyleDeclaration, color: string) => void;
2
11
  declare function getAutomaticTextColorForBackground(backgroundColor: string | undefined): string | undefined;
3
12
  //#endregion
4
- export { getAutomaticTextColorForBackground };
13
+ export { AUTHORED_BACKGROUND_COLOR_VAR, AUTHORED_TEXT_COLOR_VAR, getAutomaticTextColorForBackground, setAuthoredBackgroundColor, setAuthoredTextColor };
@@ -5,6 +5,21 @@ const WHITE_TEXT_COLOR = "#FFFFFF";
5
5
  const BLACK_LUMINANCE = 0;
6
6
  const WHITE_LUMINANCE = 1;
7
7
  const CONTRAST_OFFSET = .05;
8
+ const AUTHORED_BACKGROUND_COLOR_VAR = "--doc-authored-background-color";
9
+ const AUTHORED_TEXT_COLOR_VAR = "--doc-run-color";
10
+ /** Paint a document text color and expose it to the dark-mode color transform. */
11
+ const setAuthoredTextColor = (style, color) => {
12
+ style.color = color;
13
+ style.setProperty(AUTHORED_TEXT_COLOR_VAR, color);
14
+ };
15
+ /**
16
+ * Paint an OOXML background and retain its authored color for dark-mode adaptation.
17
+ * The custom property is presentation-only; serialization still uses the model value.
18
+ */
19
+ const setAuthoredBackgroundColor = (style, color) => {
20
+ style.backgroundColor = color;
21
+ style.setProperty(AUTHORED_BACKGROUND_COLOR_VAR, color);
22
+ };
8
23
  function normalizeHexColor(color) {
9
24
  const trimmed = color.trim();
10
25
  if (!HEX_COLOR_RE.test(trimmed)) return null;
@@ -37,4 +52,4 @@ function getAutomaticTextColorForBackground(backgroundColor) {
37
52
  return contrastRatio(luminance, BLACK_LUMINANCE) >= contrastRatio(luminance, WHITE_LUMINANCE) ? BLACK_TEXT_COLOR : WHITE_TEXT_COLOR;
38
53
  }
39
54
  //#endregion
40
- export { getAutomaticTextColorForBackground };
55
+ export { AUTHORED_BACKGROUND_COLOR_VAR, AUTHORED_TEXT_COLOR_VAR, getAutomaticTextColorForBackground, setAuthoredBackgroundColor, setAuthoredTextColor };
@@ -19,7 +19,7 @@ import { SCRIPT_CLASS, hasCjk, hasComplexScript, segmentByScript } from "../util
19
19
  import { sanitizeExternalUrl } from "../utils/urlSecurity.js";
20
20
  import { borderStrokeToCss, resolveParagraphBorderHorizontalOutsets } from "./borderStroke.js";
21
21
  import { planCursiveJoiners, withCursiveJoiners } from "./cursiveJoiners.js";
22
- import { getAutomaticTextColorForBackground } from "./documentColors.js";
22
+ import { getAutomaticTextColorForBackground, setAuthoredBackgroundColor, setAuthoredTextColor } from "./documentColors.js";
23
23
  import { applyImageBorder, applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs, wrapImageWithCrop } from "./renderImage.js";
24
24
  import { resolveImageLineAlign } from "./renderUtils.js";
25
25
  import { applySdtDataAttrs } from "./sdtBoundary.js";
@@ -124,12 +124,6 @@ const DEFAULT_BLACK_TEXT_COLOR_VALUES = /* @__PURE__ */ new Set(["000000", "000"
124
124
  const SUGGESTION_COLOR_CSS = "var(--suggestion-color, #6d3bd6)";
125
125
  const SUGGESTION_TINT_CSS = "var(--suggestion-bg, color-mix(in oklch, #6d3bd6 12%, transparent))";
126
126
  const SUGGESTION_TINT_LAYER_CSS = `linear-gradient(${SUGGESTION_TINT_CSS}, ${SUGGESTION_TINT_CSS})`;
127
- const RUN_BACKGROUND_TEXT_COLOR_VAR = "--doc-run-background-text-color";
128
- const setRunBackgroundTextColor = (element, color) => {
129
- element.classList.add("docx-run-background-text");
130
- element.style.setProperty(RUN_BACKGROUND_TEXT_COLOR_VAR, color);
131
- };
132
- const hasRunBackgroundTextSurface = (run) => Boolean(run.highlight ?? run.shading) && !run.isInsertion && !run.isDeletion && !(run.commentIds !== void 0 && run.commentIds.length > 0);
133
127
  function normalizeTextColorValue(color) {
134
128
  return color.trim().toLowerCase().replace(/^#/u, "");
135
129
  }
@@ -180,8 +174,7 @@ function applyRunStyles(element, run) {
180
174
  let hasExplicitTextColor = false;
181
175
  const textColor = getRenderableTextColor(run);
182
176
  if (textColor) {
183
- element.style.color = textColor;
184
- element.style.setProperty("--doc-run-color", textColor);
177
+ setAuthoredTextColor(element.style, textColor);
185
178
  hasExplicitTextColor = true;
186
179
  }
187
180
  if (run.letterSpacing) element.style.letterSpacing = `${run.letterSpacing}px`;
@@ -219,13 +212,11 @@ function applyRunStyles(element, run) {
219
212
  }
220
213
  const runBackground = run.highlight ?? run.shading;
221
214
  if (runBackground) {
222
- element.style.backgroundColor = runBackground;
215
+ setAuthoredBackgroundColor(element.style, runBackground);
223
216
  const hasTrackedChangeColor = run.isInsertion || run.isDeletion;
224
217
  const hasCommentHighlight = run.commentIds !== void 0 && run.commentIds.length > 0;
225
218
  const automaticTextColor = hasExplicitTextColor || hasTrackedChangeColor || hasCommentHighlight ? void 0 : getAutomaticTextColorForBackground(runBackground);
226
- if (automaticTextColor) element.style.color = automaticTextColor;
227
- const backgroundTextColor = hasExplicitTextColor ? textColor : automaticTextColor;
228
- if (backgroundTextColor && hasRunBackgroundTextSurface(run)) setRunBackgroundTextColor(element, backgroundTextColor);
219
+ if (automaticTextColor) setAuthoredTextColor(element.style, automaticTextColor);
229
220
  }
230
221
  const decorations = [];
231
222
  let explicitDecorationStyle = false;
@@ -378,15 +369,9 @@ function renderTextRun(run, doc, options) {
378
369
  });
379
370
  if (!run.hyperlink.noDefaultStyle) {
380
371
  const hyperlinkColor = getHyperlinkTextColor(run, span.style.color);
381
- anchor.style.color = hyperlinkColor;
372
+ setAuthoredTextColor(anchor.style, hyperlinkColor);
382
373
  anchor.style.textDecoration = "underline";
383
- span.style.color = hyperlinkColor;
384
- anchor.style.setProperty("--doc-run-color", hyperlinkColor);
385
- span.style.setProperty("--doc-run-color", hyperlinkColor);
386
- if (hasRunBackgroundTextSurface(run)) {
387
- setRunBackgroundTextColor(span, hyperlinkColor);
388
- setRunBackgroundTextColor(anchor, hyperlinkColor);
389
- }
374
+ setAuthoredTextColor(span.style, hyperlinkColor);
390
375
  }
391
376
  span.append(anchor);
392
377
  } else appendPaintedText({
@@ -1295,7 +1280,7 @@ function renderLine(block, line, alignment, doc, options) {
1295
1280
  }
1296
1281
  const authoredEndpoint = currentX + tabResult.width + followingWidthForCheck;
1297
1282
  const activeContentRightEdge = options?.contentWidthPx === void 0 ? void 0 : options.contentWidthPx - (options.floatingMargins?.rightMargin ?? 0);
1298
- const preservesAuthoredEndStop = activeContentRightEdge !== void 0 && tabResult.alignment === "end" && authoredEndpoint <= activeContentRightEdge + RIGHT_EDGE_EPSILON_PX;
1283
+ const preservesAuthoredEndStop = activeContentRightEdge !== void 0 && tabResult.alignment === "end" && (options?.context?.section === "header" || options?.context?.section === "footer" || authoredEndpoint <= activeContentRightEdge + RIGHT_EDGE_EPSILON_PX);
1299
1284
  const preservesAuthoredEndStopPastIndent = lineRightEdgeX !== void 0 && preservesAuthoredEndStop && authoredEndpoint > lineRightEdgeX + RIGHT_EDGE_EPSILON_PX;
1300
1285
  if (lineRightEdgeX !== void 0 && options?.isRtl !== true && tabResult.alignment === "end" && !hasFollowingTab && !preservesAuthoredEndStopPastIndent && authoredEndpoint >= lineRightEdgeX - RIGHT_EDGE_EPSILON_PX) {
1301
1286
  lineEl.style.display = "flex";
@@ -1534,9 +1519,9 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1534
1519
  }
1535
1520
  }
1536
1521
  if (block.attrs?.shading) {
1537
- fragmentEl.style.backgroundColor = block.attrs.shading;
1522
+ setAuthoredBackgroundColor(fragmentEl.style, block.attrs.shading);
1538
1523
  const automaticTextColor = getAutomaticTextColorForBackground(block.attrs.shading);
1539
- if (automaticTextColor) fragmentEl.style.color = automaticTextColor;
1524
+ if (automaticTextColor) setAuthoredTextColor(fragmentEl.style, automaticTextColor);
1540
1525
  }
1541
1526
  const availableWidth = fragment.width - indentLeft - indentRight;
1542
1527
  const paragraphEndsWithLineBreak = block.runs.at(-1)?.kind === "lineBreak";
@@ -9,7 +9,7 @@ import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
9
9
  import { emuToPixels } from "../utils/units.js";
10
10
  import { resolveAnchoredImagePosition } from "./anchoredImagePosition.js";
11
11
  import { borderStrokeToCss, resolveCssBorderStroke } from "./borderStroke.js";
12
- import { getAutomaticTextColorForBackground } from "./documentColors.js";
12
+ import { getAutomaticTextColorForBackground, setAuthoredBackgroundColor, setAuthoredTextColor } from "./documentColors.js";
13
13
  import { applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs } from "./renderImage.js";
14
14
  import { renderParagraphFragment } from "./renderParagraph.js";
15
15
  import { renderTextBoxFragment } from "./renderTextBox.js";
@@ -394,9 +394,9 @@ function renderTableCell({ cell, cellMeasure, x, width, rowHeight, borderFlags,
394
394
  if (borderFlags.drawLeft) applyBorder(cellEl, "left", cell.borders.left);
395
395
  }
396
396
  if (cell.background) {
397
- cellEl.style.backgroundColor = cell.background;
397
+ setAuthoredBackgroundColor(cellEl.style, cell.background);
398
398
  const automaticTextColor = getAutomaticTextColorForBackground(cell.background);
399
- if (automaticTextColor) cellEl.style.color = automaticTextColor;
399
+ if (automaticTextColor) setAuthoredTextColor(cellEl.style, automaticTextColor);
400
400
  }
401
401
  if (cell.noWrap && !columnsPinned) cellEl.style.whiteSpace = "nowrap";
402
402
  if (cell.verticalAlign) {
@@ -1,5 +1,6 @@
1
1
  import { layoutTextBoxContent } from "../layout-engine/measure/textBoxParagraphLayout.js";
2
2
  import { DEFAULT_TEXTBOX_MARGINS } from "../layout-engine/types.js";
3
+ import { setAuthoredBackgroundColor } from "./documentColors.js";
3
4
  import { renderParagraphFragment } from "./renderParagraph.js";
4
5
  import { panic } from "better-result";
5
6
  //#region src/layout-painter/renderTextBox.ts
@@ -28,7 +29,7 @@ function renderTextBoxFragment(fragment, block, measure, context, options = {})
28
29
  containerEl.style.height = `${fragment.height}px`;
29
30
  containerEl.style.overflow = block.textWrap === "none" ? "visible" : "hidden";
30
31
  containerEl.style.boxSizing = "border-box";
31
- if (block.fillColor) containerEl.style.backgroundColor = block.fillColor;
32
+ if (block.fillColor) setAuthoredBackgroundColor(containerEl.style, block.fillColor);
32
33
  if (block.outlineWidth && block.outlineWidth > 0) {
33
34
  const style = block.outlineStyle || "solid";
34
35
  const color = block.outlineColor || "#000000";
@@ -39,5 +39,16 @@ declare const displayPointToPdf: (pageHeightPx: number, xPx: number, yPx: number
39
39
  * screen, which is what a positive CSS/OOXML angle means.
40
40
  */
41
41
  declare const rotationMatrix: (degrees: number, originXPx: number, originYPx: number) => PdfMatrix;
42
+ /**
43
+ * A DrawingML flip and rotation about one display-list point. CSS applies the
44
+ * rightmost scale first, then the rotation; this matrix does the same.
45
+ */
46
+ declare const transformMatrix: ({ degrees, originXPx, originYPx, scaleX, scaleY }: {
47
+ readonly degrees: number;
48
+ readonly originXPx: number;
49
+ readonly originYPx: number;
50
+ readonly scaleX?: -1;
51
+ readonly scaleY?: -1;
52
+ }) => PdfMatrix;
42
53
  //#endregion
43
- export { POINTS_PER_PIXEL, PdfMatrix, applyMatrix, basePageMatrix, displayPointToPdf, pxToPt, rotationMatrix };
54
+ export { POINTS_PER_PIXEL, PdfMatrix, applyMatrix, basePageMatrix, displayPointToPdf, pxToPt, rotationMatrix, transformMatrix };
@@ -53,5 +53,28 @@ const rotationMatrix = (degrees, originXPx, originYPx) => {
53
53
  originYPx - originXPx * sin - originYPx * cos
54
54
  ];
55
55
  };
56
+ /**
57
+ * A DrawingML flip and rotation about one display-list point. CSS applies the
58
+ * rightmost scale first, then the rotation; this matrix does the same.
59
+ */
60
+ const transformMatrix = ({ degrees, originXPx, originYPx, scaleX, scaleY }) => {
61
+ const radians = degrees * Math.PI / 180;
62
+ const cos = Math.cos(radians);
63
+ const sin = Math.sin(radians);
64
+ const horizontalScale = scaleX ?? 1;
65
+ const verticalScale = scaleY ?? 1;
66
+ const a = cos * horizontalScale;
67
+ const b = sin * horizontalScale;
68
+ const c = -sin * verticalScale;
69
+ const d = cos * verticalScale;
70
+ return [
71
+ a,
72
+ b,
73
+ c,
74
+ d,
75
+ originXPx - originXPx * a - originYPx * c,
76
+ originYPx - originXPx * b - originYPx * d
77
+ ];
78
+ };
56
79
  //#endregion
57
- export { POINTS_PER_PIXEL, applyMatrix, basePageMatrix, displayPointToPdf, pxToPt, rotationMatrix };
80
+ export { POINTS_PER_PIXEL, applyMatrix, basePageMatrix, displayPointToPdf, pxToPt, rotationMatrix, transformMatrix };
package/dist/pdf/paint.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { STROKE_DASH_FACTORS, glyphCellOffsetsPx } from "../display-list/primitives.js";
2
2
  import { needsShaping } from "../shaping/placeRun.js";
3
3
  import { TEXT_RENDER_MODE, createContentStream } from "./contentStream.js";
4
- import { basePageMatrix, rotationMatrix } from "./pageSpace.js";
4
+ import { basePageMatrix, transformMatrix } from "./pageSpace.js";
5
5
  import { Result, TaggedError, panic } from "better-result";
6
6
  //#region src/pdf/paint.ts
7
7
  /**
@@ -358,7 +358,7 @@ const paintPrimitive = (context, primitive) => {
358
358
  return;
359
359
  case "rotateGroup":
360
360
  context.stream.save();
361
- context.stream.concat(rotationMatrix(primitive.degrees, primitive.originXPx, primitive.originYPx));
361
+ context.stream.concat(transformMatrix(primitive));
362
362
  for (const child of primitive.children) paintPrimitive(context, child);
363
363
  context.stream.restore();
364
364
  return;
@@ -27,6 +27,8 @@ const TEXT_BOX_AUTO_FIT_VALUES = [
27
27
  "shape"
28
28
  ];
29
29
  const TEXT_BOX_TEXT_WRAP_VALUES = ["square", "none"];
30
+ /** The complete DrawingML transform subset carried by text boxes. */
31
+ const TEXT_BOX_TRANSFORM_PATTERN = /^(?=rotate|scaleX|scaleY)(?:rotate\(-?\d+(?:\.\d+)?deg\)(?: )?)?(?:scaleX\(-1\)(?: )?)?(?:scaleY\(-1\))?$/u;
30
32
  const FIELD_KINDS = ["simple", "complex"];
31
33
  const MATH_DISPLAYS = ["inline", "block"];
32
34
  const SHAPE_FILL_TYPES = [
@@ -168,6 +170,7 @@ const readParagraphAttrs = (node) => {
168
170
  const issues = [];
169
171
  expectNodeType(node, "paragraph", issues);
170
172
  optionalString(attrs, "paraId", "paragraph.attrs.paraId", issues);
173
+ optionalString(attrs, "_docxParagraphSourceToken", "paragraph.attrs._docxParagraphSourceToken", issues);
171
174
  optionalString(attrs, "textId", "paragraph.attrs.textId", issues);
172
175
  optionalOneOf(attrs, "alignment", "paragraph.attrs.alignment", issues, PARAGRAPH_ALIGNMENT_VALUES);
173
176
  optionalOneOf(attrs, "alignmentFromStyle", "paragraph.attrs.alignmentFromStyle", issues, PARAGRAPH_ALIGNMENT_VALUES);
@@ -367,7 +370,6 @@ const readTableCellAttrs = (node) => {
367
370
  optionalRecord(attrs, "_originalFormatting", "tableCell.attrs._originalFormatting", issues);
368
371
  optionalTableCellRevision(attrs, issues);
369
372
  optionalBoolean(attrs, "_preserveVMergeRestart", "tableCell.attrs._preserveVMergeRestart", issues);
370
- optionalArray(attrs, "_docxVMergeContinuationCells", "tableCell.attrs._docxVMergeContinuationCells", issues);
371
373
  return attrsResult(attrs, issues);
372
374
  };
373
375
  const expectTableCellAttrs = (node) => expectCachedNodeAttrs(node, tableCellAttrsCache, readTableCellAttrs, "table cell attrs");
@@ -520,6 +522,7 @@ const readTextBoxAttrs = (node) => {
520
522
  optionalNumber(attrs, "outlineWidth", "textBox.attrs.outlineWidth", issues);
521
523
  optionalString(attrs, "outlineColor", "textBox.attrs.outlineColor", issues);
522
524
  optionalOneOf(attrs, "outlineStyle", "textBox.attrs.outlineStyle", issues, OUTLINE_STYLE_ATTR_VALUES);
525
+ optionalTextBoxTransform(attrs, "transform", "textBox.attrs.transform", issues);
523
526
  optionalNumber(attrs, "marginTop", "textBox.attrs.marginTop", issues);
524
527
  optionalNumber(attrs, "marginBottom", "textBox.attrs.marginBottom", issues);
525
528
  optionalNumber(attrs, "marginLeft", "textBox.attrs.marginLeft", issues);
@@ -905,6 +908,21 @@ const optionalString = (attrs, key, path, issues) => {
905
908
  message: "Expected a string."
906
909
  });
907
910
  };
911
+ const optionalTextBoxTransform = (attrs, key, path, issues) => {
912
+ const value = attrs[key];
913
+ if (value === void 0 || value === null) return;
914
+ if (typeof value !== "string") {
915
+ issues.push({
916
+ path,
917
+ message: "Expected a string."
918
+ });
919
+ return;
920
+ }
921
+ if (!TEXT_BOX_TRANSFORM_PATTERN.test(value)) issues.push({
922
+ path,
923
+ message: "Expected DrawingML rotation and/or horizontal or vertical flips."
924
+ });
925
+ };
908
926
  const optionalSdtListItems = (attrs, key, path, issues) => {
909
927
  const value = attrs[key];
910
928
  if (value === void 0 || value === null) return;
@@ -1236,13 +1254,6 @@ const optionalAutospacingBase = (attrs, path, issues) => {
1236
1254
  optionalNumber(value, "before", `${path}.before`, issues);
1237
1255
  optionalNumber(value, "after", `${path}.after`, issues);
1238
1256
  };
1239
- const optionalArray = (attrs, key, path, issues) => {
1240
- const value = attrs[key];
1241
- if (value !== void 0 && value !== null && !Array.isArray(value)) issues.push({
1242
- path,
1243
- message: "Expected an array."
1244
- });
1245
- };
1246
1257
  const optionalBorderMap = (attrs, key, path, issues, sides) => {
1247
1258
  const value = attrs[key];
1248
1259
  if (value === void 0 || value === null) return;
@@ -1,3 +1,4 @@
1
+ import { joinProseParagraphsWithRightPropertySource } from "../../docx/paragraphPropertySource.js";
1
2
  import { appendHeadlessInlineResolution } from "../../internal/headlessRevisionResolution.js";
2
3
  import { stateAllowsHeadlessRevisionResolution } from "../../internal/headlessRevisionResolutionGuard.js";
3
4
  import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../attrs/index.js";
@@ -232,15 +233,23 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
232
233
  tr.setNodeAttribute(mappedPos, "pPrMark", null);
233
234
  continue;
234
235
  }
236
+ const emptyFirstParagraph = holdsNoContent(paragraph);
235
237
  const joinedAttrs = {
236
- ...(holdsNoContent(paragraph) ? nextNode : paragraph).attrs,
238
+ ...(emptyFirstParagraph ? nextNode : paragraph).attrs,
237
239
  pPrMark: nextNode.attrs["pPrMark"],
238
240
  sectionBreakType: nextNode.attrs["sectionBreakType"],
239
241
  _sectionProperties: nextNode.attrs["_sectionProperties"]
240
242
  };
241
243
  try {
242
- tr.join(joinPos);
243
- tr.setNodeMarkup(mappedPos, void 0, joinedAttrs);
244
+ if (emptyFirstParagraph) joinProseParagraphsWithRightPropertySource({
245
+ attrs: joinedAttrs,
246
+ pos: joinPos,
247
+ transaction: tr
248
+ });
249
+ else {
250
+ tr.join(joinPos);
251
+ tr.setNodeMarkup(mappedPos, void 0, joinedAttrs);
252
+ }
244
253
  if (ownsSectionEndpoint(paragraph)) {
245
254
  removedSectionEndpointCount++;
246
255
  removedSectionReferences.push(...sectionReferencesOf(paragraph));
@@ -1,13 +1,17 @@
1
+ import { decodeTableCellParagraphSourcePayload, restoreTableCellsWithParagraphPropertySources, transportTableCellsWithParagraphPropertySources } from "../../docx/paragraphPropertySource.js";
1
2
  import { standaloneTableCellFromProseMirror } from "../conversion/fromProseDoc.js";
2
3
  import { standaloneTableCellToProseMirror } from "../conversion/toProseDoc.js";
3
4
  import { getTableCellMergeChange } from "../tableCellMergeRevision.js";
4
5
  import { Result } from "better-result";
5
6
  import { TableMap } from "prosemirror-tables";
6
7
  //#region src/prosemirror/commands/tableCellMergeResolution.ts
8
+ const tableCellContinuationPayload = (node) => {
9
+ const value = node.attrs["_docxVMergeContinuationCells"];
10
+ return value === void 0 || value === null ? null : decodeTableCellParagraphSourcePayload(value);
11
+ };
7
12
  const hasMatchingCollapsedTableCellMerge = (node, revisionSet) => {
8
- const continuationCells = node.attrs["_docxVMergeContinuationCells"];
9
- if (!Array.isArray(continuationCells)) return false;
10
- return continuationCells.some((cell) => {
13
+ const payload = tableCellContinuationPayload(node);
14
+ return payload !== null && payload.cells.some((cell) => {
11
15
  const change = getTableCellMergeChange(cell);
12
16
  return change !== null && (revisionSet === null || revisionSet.has(change.info.id));
13
17
  });
@@ -71,19 +75,19 @@ const mergeTableCellWithCellAbove = (tr, cellPos) => {
71
75
  if (!aboveCell || typeof aboveRowspan !== "number" || typeof cellRowspan !== "number" || aboveRowspan < 1 || cellRowspan < 1) return false;
72
76
  const continuationCells = tableCellContinuationCells(aboveCell, aboveRowspan);
73
77
  continuationCells.push(tableCellContinuationFromNode(cell));
74
- const nestedContinuations = cell.attrs["_docxVMergeContinuationCells"];
75
- if (Array.isArray(nestedContinuations)) continuationCells.push(...nestedContinuations);
78
+ const nestedPayload = tableCellContinuationPayload(cell);
79
+ if (nestedPayload) continuationCells.push(...nestedPayload.cells);
76
80
  tr.delete(cellPos, cellPos + cell.nodeSize);
77
81
  tr.setNodeMarkup(abovePos, void 0, {
78
82
  ...aboveCell.attrs,
79
83
  rowspan: aboveRowspan + cellRowspan,
80
- _docxVMergeContinuationCells: continuationCells
84
+ _docxVMergeContinuationCells: transportTableCellsWithParagraphPropertySources(continuationCells)
81
85
  });
82
86
  return true;
83
87
  };
84
88
  const tableCellContinuationCells = (cell, rowspan) => {
85
- const stored = cell.attrs["_docxVMergeContinuationCells"];
86
- const cells = Array.isArray(stored) ? [...stored] : [];
89
+ const payload = tableCellContinuationPayload(cell);
90
+ const cells = payload ? [...payload.cells] : [];
87
91
  while (cells.length < rowspan - 1) cells.push(emptyVerticalMergeContinuation());
88
92
  return cells;
89
93
  };
@@ -109,8 +113,10 @@ const emptyVerticalMergeContinuation = () => ({
109
113
  });
110
114
  const resolveCollapsedTableCellMerge = (tr, cellPos, mode, revisionSet) => {
111
115
  const cell = tr.doc.nodeAt(cellPos);
112
- const stored = cell?.attrs["_docxVMergeContinuationCells"];
113
- if (!cell || !Array.isArray(stored)) return false;
116
+ if (!cell) return false;
117
+ const payload = tableCellContinuationPayload(cell);
118
+ if (!payload) return false;
119
+ const stored = payload.cells;
114
120
  const matchingIndices = [];
115
121
  const nextCells = stored.map((continuationCell, index) => {
116
122
  const change = getTableCellMergeChange(continuationCell);
@@ -142,8 +148,9 @@ const resolveCollapsedTableCellMerge = (tr, cellPos, mode, revisionSet) => {
142
148
  const rowspan = cell.attrs["rowspan"];
143
149
  if (typeof rowspan !== "number" || rowspan !== stored.length + 1 || rectangle.bottom - rectangle.top !== rowspan) return false;
144
150
  const restorations = [];
151
+ const restoredCells = restoreTableCellsWithParagraphPropertySources(payload);
145
152
  for (const index of splitIndices) {
146
- const source = nextCells[index];
153
+ const source = restoredCells[index];
147
154
  if (!source) return false;
148
155
  const restoredCell = createRestoredTableCell(cell, source);
149
156
  if (!restoredCell) return false;
@@ -1,5 +1,5 @@
1
1
  import { numPrEqual } from "../../docx/numberingParser.js";
2
- import { copyParagraphPropertySource, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceTransferId, linkParagraphPropertySourceCandidate, recreateProseNodeWithParagraphPropertySource } from "../../docx/paragraphPropertySource.js";
2
+ 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";
3
3
  import { visitDocxParagraphs } from "../../docx/paragraphTraversal.js";
4
4
  import { ShapeOutlineStyleSchema, narrowEnum } from "../../docx/parserEnums.js";
5
5
  import { DATE_UTC_ATTRIBUTE } from "../../docx/trackedChangeInfo.js";
@@ -103,23 +103,84 @@ const uniqueParagraphsById = (content, includeTransferIds = false) => {
103
103
  });
104
104
  return paragraphs;
105
105
  };
106
+ const restoreParagraphPropertySource = (paragraph, baseParagraph) => {
107
+ copyParagraphPropertySource(paragraph, baseParagraph);
108
+ const baseFormatting = baseParagraph.formatting;
109
+ if (!baseFormatting?.numPr || !baseFormatting.numPrFromStyle || !numPrEqual(baseFormatting.numPr, baseFormatting.numPrFromStyle)) return;
110
+ const { numPr, numPrFromStyle, ...authoredFormatting } = baseFormatting;
111
+ if (canonicalJson(paragraph.formatting ?? {}) !== canonicalJson(authoredFormatting)) return;
112
+ paragraph.formatting = {
113
+ ...paragraph.formatting,
114
+ numPr,
115
+ numPrFromStyle
116
+ };
117
+ };
106
118
  const restoreParagraphPropertySources = (content, baseContent, linkedTargets, linkedSources) => {
107
119
  const baseParagraphs = uniqueParagraphsById(baseContent, true);
108
120
  for (const [paraId, paragraph] of uniqueParagraphsById(content)) {
109
121
  const baseParagraph = baseParagraphs.get(paraId);
110
122
  if (!paragraph || !baseParagraph || linkedTargets.has(paragraph) || linkedSources.has(baseParagraph) || !getParagraphPropertySource(baseParagraph)) continue;
111
- copyParagraphPropertySource(paragraph, baseParagraph);
112
- const baseFormatting = baseParagraph.formatting;
113
- if (!baseFormatting?.numPr || !baseFormatting.numPrFromStyle || !numPrEqual(baseFormatting.numPr, baseFormatting.numPrFromStyle)) continue;
114
- const { numPr, numPrFromStyle, ...authoredFormatting } = baseFormatting;
115
- if (canonicalJson(paragraph.formatting ?? {}) !== canonicalJson(authoredFormatting)) continue;
116
- paragraph.formatting = {
117
- ...paragraph.formatting,
118
- numPr,
119
- numPrFromStyle
120
- };
123
+ restoreParagraphPropertySource(paragraph, baseParagraph);
121
124
  }
122
125
  };
126
+ const sourceValidationError = (code, message) => new ParagraphPropertySourceValidationError({
127
+ code,
128
+ message
129
+ });
130
+ const validateParagraphPropertySourceTokens = (pmDoc, baseDocument, contract) => {
131
+ const sourceParagraphs = copyDocumentParagraphPropertySources(baseDocument);
132
+ if (!sourceParagraphs) panic("A paragraph-property source contract lost its source registry");
133
+ const currentTokens = /* @__PURE__ */ new Set();
134
+ visitDocumentStoryParagraphs(baseDocument.package.document.content, (paragraph) => {
135
+ const token = getParagraphPropertySourceToken(paragraph);
136
+ if (!token) {
137
+ if (paragraphPropertySourceBelongsToDocument(paragraph, baseDocument)) throw sourceValidationError("invalid_token", "A source-bound paragraph is missing its paragraph-property token.");
138
+ return;
139
+ }
140
+ if (!paragraphPropertySourceTokenMatchesContract(token, contract)) throw sourceValidationError("invalid_token", "The source document contains an invalid paragraph-property token.");
141
+ if (!sourceParagraphs.has(token)) throw sourceValidationError("unknown_token", "The source document contains an unknown paragraph-property token.");
142
+ if (currentTokens.has(token)) throw sourceValidationError("duplicate_token", "The source document contains a duplicate paragraph-property token.");
143
+ currentTokens.add(token);
144
+ });
145
+ const seen = /* @__PURE__ */ new Set();
146
+ const validateToken = (token) => {
147
+ if (token === null || token === void 0) return;
148
+ if (!isParagraphPropertySourceToken(token)) throw sourceValidationError("invalid_token", "A paragraph contains a malformed paragraph-property token.");
149
+ if (!paragraphPropertySourceTokenMatchesContract(token, contract)) throw sourceValidationError("unknown_token", "A paragraph-property token belongs to a different source document.");
150
+ if (seen.has(token)) throw sourceValidationError("duplicate_token", "A paragraph-property token is attached to more than one paragraph.");
151
+ if (!sourceParagraphs.has(token)) throw sourceValidationError("unknown_token", "A paragraph-property token is not present in the source document.");
152
+ seen.add(token);
153
+ };
154
+ const validateTableCellBinding = (binding) => {
155
+ switch (binding.type) {
156
+ case "authored": return;
157
+ case "source":
158
+ validateToken(binding.token);
159
+ return;
160
+ default: return binding;
161
+ }
162
+ };
163
+ pmDoc.descendants((node) => {
164
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
165
+ const continuationCells = expectTableCellAttrs(node)._docxVMergeContinuationCells;
166
+ if (continuationCells !== void 0 && continuationCells !== null) visitTableCellParagraphPropertySourceBindings(decodeTableCellParagraphSourcePayload(continuationCells), (binding) => validateTableCellBinding(binding));
167
+ return true;
168
+ }
169
+ if (node.type.name !== "paragraph") return true;
170
+ validateToken(getProseParagraphPropertySourceToken(node));
171
+ return false;
172
+ });
173
+ return sourceParagraphs;
174
+ };
175
+ const restoreParagraphPropertySourcesByToken = (content, baseParagraphs) => {
176
+ visitDocumentStoryParagraphs(content, (paragraph) => {
177
+ const token = getParagraphPropertySourceToken(paragraph);
178
+ if (!token) return;
179
+ const baseParagraph = baseParagraphs.get(token);
180
+ if (!baseParagraph) panic("Validated paragraph-property token lost its source owner");
181
+ restoreParagraphPropertySource(paragraph, baseParagraph);
182
+ });
183
+ };
123
184
  const restoreLinkedParagraphPropertySources = (content) => {
124
185
  const targetsBySource = /* @__PURE__ */ new Map();
125
186
  const linkedTargets = /* @__PURE__ */ new Set();
@@ -133,7 +194,7 @@ const restoreLinkedParagraphPropertySources = (content) => {
133
194
  });
134
195
  for (const [source, targets] of targetsBySource) if (targets.length === 1) {
135
196
  const target = targets.at(0);
136
- if (target) copyParagraphPropertySource(target, source);
197
+ if (target) copyParagraphPropertyCapture(target, source);
137
198
  }
138
199
  return {
139
200
  targets: linkedTargets,
@@ -143,20 +204,30 @@ const restoreLinkedParagraphPropertySources = (content) => {
143
204
  /** Convert a ProseMirror document to the document model. */
144
205
  function fromProseDoc(pmDoc, baseDocument) {
145
206
  assertValidProseMirrorDocument(pmDoc, "Cannot convert invalid ProseMirror document to DOCX model");
207
+ const baseContract = baseDocument ? getDocumentParagraphPropertySourceContract(baseDocument) : void 0;
208
+ const proseContractAttribute = pmDoc.attrs[PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR];
209
+ const proseContract = getProseDocumentParagraphPropertySourceContract(pmDoc);
210
+ if (proseContractAttribute !== null && proseContractAttribute !== void 0 && proseContract === null || (baseContract ?? null) !== proseContract) throw sourceValidationError("contract_mismatch", "The ProseMirror document does not match its paragraph-property source document.");
211
+ const tokenSources = baseContract && proseContract && baseDocument ? validateParagraphPropertySourceTokens(pmDoc, baseDocument, baseContract) : null;
146
212
  const blocks = extractBlocks(pmDoc, "resolve", baseDocument?.package.styles ? createStyleEngine(baseDocument.package.styles) : null);
147
213
  const linkedSources = restoreLinkedParagraphPropertySources(blocks);
148
- if (baseDocument) restoreParagraphPropertySources(blocks, baseDocument.package.document.content, linkedSources.targets, linkedSources.sources);
214
+ if (tokenSources) restoreParagraphPropertySourcesByToken(blocks, tokenSources);
215
+ else if (baseDocument) restoreParagraphPropertySources(blocks, baseDocument.package.document.content, linkedSources.targets, linkedSources.sources);
149
216
  const documentBody = { content: blocks };
150
217
  if (baseDocument?.package.document.finalSectionProperties) documentBody.finalSectionProperties = baseDocument.package.document.finalSectionProperties;
151
218
  if (baseDocument?.package.document.sections) documentBody.sections = baseDocument.package.document.sections;
152
219
  if (baseDocument?.package.document.comments) documentBody.comments = baseDocument.package.document.comments;
153
- if (baseDocument) return {
154
- ...baseDocument,
155
- package: {
156
- ...baseDocument.package,
157
- document: documentBody
158
- }
159
- };
220
+ if (baseDocument) {
221
+ const updatedDocument = {
222
+ ...baseDocument,
223
+ package: {
224
+ ...baseDocument.package,
225
+ document: documentBody
226
+ }
227
+ };
228
+ copyDocumentParagraphPropertySourceContract(updatedDocument, baseDocument);
229
+ return updatedDocument;
230
+ }
160
231
  return { package: { document: documentBody } };
161
232
  }
162
233
  const isSuggestedMark = (mark) => mark.attrs["provenance"] === "suggested";
@@ -2606,7 +2677,7 @@ function convertPMTableRow(node, documentCounts, activeVerticalMerges, styleReso
2606
2677
  const colspan = Math.max(cellAttrs.colspan, 1);
2607
2678
  cells.push(convertPMTableCell(cellNode, documentCounts, styleResolver));
2608
2679
  if (cellAttrs.rowspan > 1) {
2609
- const continuationCells = cellAttrs._docxVMergeContinuationCells;
2680
+ const continuationCells = cellAttrs._docxVMergeContinuationCells !== void 0 && cellAttrs._docxVMergeContinuationCells !== null ? restoreTableCellsWithParagraphPropertySources(decodeTableCellParagraphSourcePayload(cellAttrs._docxVMergeContinuationCells)) : void 0;
2610
2681
  activeVerticalMerges?.set(gridColumn, {
2611
2682
  remainingRows: cellAttrs.rowspan - 1,
2612
2683
  colspan,
@@ -2829,6 +2900,8 @@ function convertPMTextBox(node, styleResolver = null) {
2829
2900
  }
2830
2901
  };
2831
2902
  if (attrs.textBoxId) shape.id = attrs.textBoxId;
2903
+ const transform = parseTransformAttr(attrs.transform);
2904
+ if (transform) shape.transform = transform;
2832
2905
  if (attrs.fillColor) shape.fill = {
2833
2906
  type: "solid",
2834
2907
  color: { rgb: attrs.fillColor.replace("#", "") }