@stll/folio-core 0.28.1 → 0.30.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 (125) hide show
  1. package/dist/controller/folioEditor.d.ts +28 -1
  2. package/dist/controller/folioEditor.js +37 -26
  3. package/dist/controller/fontReadiness.js +28 -24
  4. package/dist/controller/hiddenEditorManager.d.ts +2 -7
  5. package/dist/controller/layoutPipeline.js +33 -3
  6. package/dist/controller/noteEditorManager.d.ts +3 -6
  7. package/dist/docx/metafileSvg.d.ts +12 -0
  8. package/dist/docx/metafileSvg.js +462 -0
  9. package/dist/docx/paragraphTextBoxEnrichment.js +55 -4
  10. package/dist/docx/parser.d.ts +2 -2
  11. package/dist/docx/parser.js +26 -0
  12. package/dist/docx/runParser.js +2 -1
  13. package/dist/docx/serializer/runSerializer.js +14 -2
  14. package/dist/docx/styleParser.js +2 -1
  15. package/dist/docx/textBoxParser.js +14 -0
  16. package/dist/docx/vmlImageParser.js +72 -152
  17. package/dist/docx/vmlPreview.d.ts +31 -0
  18. package/dist/docx/vmlPreview.js +535 -0
  19. package/dist/docx/wrapTypes.d.ts +2 -36
  20. package/dist/docx/wrapTypes.js +1 -39
  21. package/dist/fonts/fontAlternates.d.ts +8 -0
  22. package/dist/fonts/fontAlternates.js +16 -0
  23. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  24. package/dist/layout-bridge/convert/footnoteLayout.js +34 -3
  25. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  26. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -0
  27. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
  28. package/dist/layout-bridge/convert/toFlowBlocks.js +136 -120
  29. package/dist/layout-bridge/dom/noteStoryDom.d.ts +1 -1
  30. package/dist/layout-bridge/engine/selectionRects.js +3 -1
  31. package/dist/layout-engine/index.js +65 -32
  32. package/dist/layout-engine/justifiedLineFit.d.ts +7 -0
  33. package/dist/layout-engine/justifiedLineFit.js +6 -0
  34. package/dist/layout-engine/keep-together.js +15 -10
  35. package/dist/layout-engine/measure/complexScriptFormatting.d.ts +6 -1
  36. package/dist/layout-engine/measure/complexScriptFormatting.js +11 -2
  37. package/dist/layout-engine/measure/floatingTablePosition.d.ts +16 -1
  38. package/dist/layout-engine/measure/floatingTablePosition.js +29 -6
  39. package/dist/layout-engine/measure/lineBreakProvider.js +67 -8
  40. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  41. package/dist/layout-engine/measure/listMarkerWidth.js +23 -10
  42. package/dist/layout-engine/measure/measureBlocks.d.ts +15 -0
  43. package/dist/layout-engine/measure/measureBlocks.js +197 -30
  44. package/dist/layout-engine/measure/measureContainer.js +20 -24
  45. package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
  46. package/dist/layout-engine/measure/measureHelpers.js +14 -8
  47. package/dist/layout-engine/measure/measureParagraph.js +111 -37
  48. package/dist/layout-engine/measure/measureTypes.d.ts +3 -0
  49. package/dist/layout-engine/measure/tabCalculator.d.ts +123 -0
  50. package/dist/layout-engine/measure/tabCalculator.js +144 -0
  51. package/dist/layout-engine/measure/tableCellFlow.d.ts +21 -1
  52. package/dist/layout-engine/measure/tableCellFlow.js +45 -8
  53. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +10 -2
  54. package/dist/layout-engine/measure/tableInlinePlacement.js +17 -15
  55. package/dist/layout-engine/paginator.d.ts +26 -2
  56. package/dist/layout-engine/paginator.js +76 -14
  57. package/dist/layout-engine/paragraphSpacing.d.ts +3 -1
  58. package/dist/layout-engine/paragraphSpacing.js +4 -3
  59. package/dist/layout-engine/renderedBreakReconciliation.d.ts +14 -3
  60. package/dist/layout-engine/renderedBreakReconciliation.js +45 -14
  61. package/dist/layout-engine/section-breaks.d.ts +3 -1
  62. package/dist/layout-engine/section-breaks.js +6 -2
  63. package/dist/layout-engine/types.d.ts +18 -1
  64. package/dist/layout-engine/types.js +1 -1
  65. package/dist/layout-painter/imageLayout.d.ts +1 -1
  66. package/dist/layout-painter/renderPage.js +6 -1
  67. package/dist/layout-painter/renderParagraph.js +169 -82
  68. package/dist/layout-painter/renderTable.d.ts +1 -1
  69. package/dist/layout-painter/renderTable.js +18 -7
  70. package/dist/layout-painter/renderTextBox.d.ts +1 -1
  71. package/dist/layout-painter/renderTextBox.js +6 -1
  72. package/dist/markdown/renderParagraph.js +7 -2
  73. package/dist/model.d.ts +3 -3
  74. package/dist/model.js +2 -2
  75. package/dist/paged-layout/sectionBlockWidths.d.ts +14 -0
  76. package/dist/paged-layout/sectionBlockWidths.js +103 -14
  77. package/dist/prosemirror/attrs/index.js +14 -2
  78. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
  79. package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
  80. package/dist/prosemirror/conversion/fromProseDoc.js +176 -54
  81. package/dist/prosemirror/conversion/toProseDoc.js +179 -64
  82. package/dist/prosemirror/extensions/StarterKit.js +11 -3
  83. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +8 -4
  84. package/dist/prosemirror/extensions/features/PasteCleanupExtension.d.ts +4 -1
  85. package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +9 -5
  86. package/dist/prosemirror/extensions/features/pasteCleanup.d.ts +10 -23
  87. package/dist/prosemirror/extensions/features/pasteCleanup.js +77 -22
  88. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +9 -5
  89. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +9 -0
  90. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +67 -0
  91. package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +10 -4
  92. package/dist/prosemirror/extensions/nodes/FieldExtension.js +77 -59
  93. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -0
  94. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +4 -1
  95. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -3
  96. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +6 -2
  97. package/dist/prosemirror/listMarker.d.ts +58 -0
  98. package/dist/prosemirror/listMarker.js +185 -0
  99. package/dist/prosemirror/numberedRefFields.d.ts +24 -0
  100. package/dist/prosemirror/numberedRefFields.js +276 -0
  101. package/dist/prosemirror/paraText.js +56 -17
  102. package/dist/prosemirror/plugins/anonymizationDecorations.js +1 -1
  103. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -1
  104. package/dist/prosemirror/plugins/pmTextScan.js +28 -7
  105. package/dist/prosemirror/plugins/templateDirectives.js +2 -2
  106. package/dist/prosemirror/schema/index.d.ts +2 -2
  107. package/dist/prosemirror/schema/nodes.d.ts +17 -1
  108. package/dist/prosemirror/utils/tabCalculator.d.ts +2 -123
  109. package/dist/prosemirror/utils/tabCalculator.js +1 -140
  110. package/dist/prosemirror/validation.js +93 -0
  111. package/dist/render-dom/RemoteSelectionOverlay.d.ts +4 -4
  112. package/dist/types/documentEnumValues.d.ts +4 -1
  113. package/dist/types/documentEnumValues.js +12 -1
  114. package/dist/types/editor-story.d.ts +8 -0
  115. package/dist/types/editor-story.js +0 -0
  116. package/dist/types/remote-selection.d.ts +11 -0
  117. package/dist/types/remote-selection.js +0 -0
  118. package/dist/types/wrap.d.ts +36 -0
  119. package/dist/types/wrap.js +40 -0
  120. package/dist/utils/fontResolver.d.ts +1 -1
  121. package/dist/utils/fontResolver.js +42 -2
  122. package/dist/utils/formatToStyle.js +4 -2
  123. package/dist/utils/horizontalScale.d.ts +28 -0
  124. package/dist/utils/horizontalScale.js +42 -0
  125. package/package.json +2 -2
@@ -1,11 +1,13 @@
1
1
  import { resolveColorValueToHex } from "../../docx/drawingUtils.js";
2
2
  import { createStyleEngine } from "../../style-engine/styleEngine.js";
3
+ import { normalizeHorizontalScalePercent } from "../../utils/horizontalScale.js";
3
4
  import { mergeParagraphFormatting, mergeParagraphTabStops } from "../../utils/paragraphFormattingMerge.js";
4
5
  import { tableOfContentsStyleLevel } from "../../utils/tableOfContentsStyle.js";
5
6
  import { mergeTextFormatting } from "../../utils/textFormattingMerge.js";
6
7
  import { emuToPixels } from "../../utils/units.js";
7
8
  import { setAutospacingBaseValue } from "../autospacingBase.js";
8
9
  import { buildRunFormattingOverrideAttrs } from "../extensions/marks/RunFormattingOverrideExtension.js";
10
+ import { stampNumberedRefFieldBaselines } from "../numberedRefFields.js";
9
11
  import { directionFromBidi } from "../paragraphDirection.js";
10
12
  import { schema } from "../schema/index.js";
11
13
  import { cascadeStyleTextFormatting } from "../styles/styleToggleCascade.js";
@@ -36,6 +38,56 @@ const createHyperlinkInstanceIndexAllocator = () => {
36
38
  return () => index++;
37
39
  };
38
40
  /**
41
+ * Find bookmark pairs across every editable inline wrapper in a document story.
42
+ * Each endpoint is converted at its own structural position, so a range may
43
+ * start outside a hyperlink and end inside it (or the inverse).
44
+ */
45
+ const collectPairedBookmarkIds = (blocks) => {
46
+ const counts = /* @__PURE__ */ new Map();
47
+ let position = 0;
48
+ const countBoundary = (id, type) => {
49
+ const count = counts.get(id) ?? {
50
+ starts: 0,
51
+ ends: 0
52
+ };
53
+ if (type === "start") {
54
+ count.starts += 1;
55
+ count.firstStart ??= position;
56
+ } else {
57
+ count.ends += 1;
58
+ count.firstEnd ??= position;
59
+ }
60
+ position += 1;
61
+ counts.set(id, count);
62
+ };
63
+ const visitRun = (run) => {
64
+ for (const content of run.content) if (content.type === "shape" && content.shape.textBody) visitBlocks(content.shape.textBody.content);
65
+ };
66
+ const visitHyperlink = (hyperlink) => {
67
+ for (const child of hyperlink.children) if (child.type === "bookmarkStart") countBoundary(child.id, "start");
68
+ else if (child.type === "bookmarkEnd") countBoundary(child.id, "end");
69
+ else visitRun(child);
70
+ };
71
+ const visitParagraphContent = (content) => {
72
+ if (content.type === "bookmarkStart") countBoundary(content.id, "start");
73
+ else if (content.type === "bookmarkEnd") countBoundary(content.id, "end");
74
+ else if (content.type === "run") visitRun(content);
75
+ else if (content.type === "hyperlink") visitHyperlink(content);
76
+ else if (content.type === "simpleField") for (const child of content.content) if (child.type === "hyperlink") visitHyperlink(child);
77
+ else visitRun(child);
78
+ else if (content.type === "complexField") for (const run of [...content.fieldCode ?? [], ...content.fieldResult]) visitRun(run);
79
+ else if (content.type === "inlineSdt") for (const child of content.content) visitParagraphContent(child);
80
+ else if (content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo") for (const child of content.content) visitParagraphContent(child);
81
+ };
82
+ const visitBlocks = (nestedBlocks) => {
83
+ for (const block of nestedBlocks) if (block.type === "paragraph") for (const content of block.content) visitParagraphContent(content);
84
+ else if (block.type === "table") for (const row of block.rows) for (const cell of row.cells) visitBlocks(cell.content);
85
+ else visitBlocks(block.content);
86
+ };
87
+ visitBlocks(blocks);
88
+ return new Set([...counts].flatMap(([id, count]) => count.starts === 1 && count.ends === 1 && count.firstStart !== void 0 && count.firstEnd !== void 0 && count.firstStart < count.firstEnd ? [id] : []));
89
+ };
90
+ /**
39
91
  * Convert a Document to a ProseMirror document
40
92
  *
41
93
  * @param document - The Document to convert
@@ -50,7 +102,8 @@ function toProseDoc(document, options) {
50
102
  const conversionContext = {
51
103
  theme,
52
104
  nextTextBoxGroupId,
53
- nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator()
105
+ nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator(),
106
+ pairedBookmarkIds: collectPairedBookmarkIds(paragraphs)
54
107
  };
55
108
  const convertBodyBlocks = (blocks) => {
56
109
  const out = [];
@@ -85,7 +138,7 @@ function toProseDoc(document, options) {
85
138
  };
86
139
  nodes.push(...convertBodyBlocks(paragraphs));
87
140
  if (nodes.length === 0) nodes.push(schema.node("paragraph", {}, []));
88
- const pmDoc = schema.node("doc", null, nodes);
141
+ const pmDoc = stampNumberedRefFieldBaselines(schema.node("doc", null, nodes));
89
142
  assertValidProseMirrorDocument(pmDoc, "Document conversion produced an invalid ProseMirror document");
90
143
  return pmDoc;
91
144
  }
@@ -126,7 +179,7 @@ function convertBlockSdt(blockSdt, convertBlocks) {
126
179
  * Resolves style-based text formatting and passes it to runs so that
127
180
  * paragraph styles (like Heading1) apply their font size, color, etc.
128
181
  */
129
- function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex, activeCommentIds, extraRunFormatting, tableParagraphOverlay, textBoxAnchors) {
182
+ function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex, pairedBookmarkIds, activeCommentIds, extraRunFormatting, tableParagraphOverlay, textBoxAnchors) {
130
183
  const attrs = paragraphFormattingToAttrs(paragraph, styleResolver, tableParagraphOverlay);
131
184
  const isTocParagraph = attrs._tableOfContentsLevel !== void 0;
132
185
  const inlineNodes = [];
@@ -209,42 +262,56 @@ function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex,
209
262
  const emitTrackedChange = (change, markType, moveKind) => {
210
263
  emitInlineNodes(convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, moveKind, textBoxAnchors));
211
264
  };
212
- for (const content of paragraph.content) {
213
- if (content.type === "commentRangeStart") commentIds.add(content.id);
214
- else if (content.type === "commentRangeEnd") commentIds.delete(content.id);
215
- else if (content.type === "commentReference") anchorPointComment(inlineNodes, content.id);
216
- else if (content.type === "run") emitInlineNodes(convertRun(content, getInheritedRunFormatting(content.formatting), styleResolver, textBoxAnchors));
217
- else if (content.type === "hyperlink") {
218
- const linkNodes = convertHyperlink(content, {
219
- getInheritedRunFormatting,
220
- styleResolver,
221
- hyperlinkIndex: nextHyperlinkInstanceIndex(),
222
- textBoxAnchors
223
- });
224
- if (linkNodes.length === 0) {
225
- emptyHyperlinks ??= [];
226
- emptyHyperlinks.push({
227
- offset: inlineOffset,
228
- ...content.href !== void 0 ? { href: content.href } : {},
229
- ...content.anchor !== void 0 ? { anchor: content.anchor } : {},
230
- ...content.tooltip !== void 0 ? { tooltip: content.tooltip } : {},
231
- ...content.rId !== void 0 ? { rId: content.rId } : {}
232
- });
233
- continue;
234
- }
235
- emitInlineNodes(linkNodes);
236
- } else if (content.type === "simpleField" || content.type === "complexField") emitInlineNode(convertField(content, getInheritedRunFormatting, styleResolver));
237
- else if (content.type === "inlineSdt") emitInlineNode(convertInlineSdt(content, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, textBoxAnchors));
238
- else if (content.type === "insertion" || content.type === "moveTo") emitTrackedChange(content, "insertion", content.type === "moveTo" ? "moveTo" : null);
239
- else if (content.type === "deletion" || content.type === "moveFrom") emitTrackedChange(content, "deletion", content.type === "moveFrom" ? "moveFrom" : null);
240
- else if (content.type === "mathEquation") emitInlineNode(convertMathEquation(content));
241
- if (content.type === "bookmarkStart") {
242
- if (!bookmarksArr) bookmarksArr = [];
243
- bookmarksArr.push({
244
- id: content.id,
245
- name: content.name
265
+ for (const content of paragraph.content) if (content.type === "commentRangeStart") commentIds.add(content.id);
266
+ else if (content.type === "commentRangeEnd") commentIds.delete(content.id);
267
+ else if (content.type === "commentReference") anchorPointComment(inlineNodes, content.id);
268
+ else if (content.type === "run") emitInlineNodes(convertRun(content, getInheritedRunFormatting(content.formatting), styleResolver, textBoxAnchors));
269
+ else if (content.type === "hyperlink") {
270
+ const linkNodes = convertHyperlink(content, {
271
+ getInheritedRunFormatting,
272
+ styleResolver,
273
+ hyperlinkIndex: nextHyperlinkInstanceIndex(),
274
+ textBoxAnchors
275
+ });
276
+ if (linkNodes.length === 0) {
277
+ emptyHyperlinks ??= [];
278
+ emptyHyperlinks.push({
279
+ offset: inlineOffset,
280
+ ...content.href !== void 0 ? { href: content.href } : {},
281
+ ...content.anchor !== void 0 ? { anchor: content.anchor } : {},
282
+ ...content.tooltip !== void 0 ? { tooltip: content.tooltip } : {},
283
+ ...content.rId !== void 0 ? { rId: content.rId } : {}
246
284
  });
285
+ continue;
247
286
  }
287
+ emitInlineNodes(linkNodes);
288
+ } else if (content.type === "simpleField" || content.type === "complexField") emitInlineNode(convertField(content, {
289
+ getInheritedRunFormatting,
290
+ styleResolver,
291
+ nextHyperlinkInstanceIndex,
292
+ textBoxAnchors
293
+ }));
294
+ else if (content.type === "inlineSdt") emitInlineNode(convertInlineSdt(content, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, textBoxAnchors));
295
+ else if (content.type === "insertion" || content.type === "moveTo") emitTrackedChange(content, "insertion", content.type === "moveTo" ? "moveTo" : null);
296
+ else if (content.type === "deletion" || content.type === "moveFrom") emitTrackedChange(content, "deletion", content.type === "moveFrom" ? "moveFrom" : null);
297
+ else if (content.type === "mathEquation") emitInlineNode(convertMathEquation(content));
298
+ else if (content.type === "bookmarkStart" && pairedBookmarkIds.has(content.id)) emitInlineNode(schema.node("bookmarkBoundary", {
299
+ type: "start",
300
+ id: content.id,
301
+ name: content.name,
302
+ colFirst: content.colFirst,
303
+ colLast: content.colLast
304
+ }));
305
+ else if (content.type === "bookmarkEnd" && pairedBookmarkIds.has(content.id)) emitInlineNode(schema.node("bookmarkBoundary", {
306
+ type: "end",
307
+ id: content.id
308
+ }));
309
+ else if (content.type === "bookmarkStart") {
310
+ if (!bookmarksArr) bookmarksArr = [];
311
+ bookmarksArr.push({
312
+ id: content.id,
313
+ name: content.name
314
+ });
248
315
  }
249
316
  if (bookmarksArr) attrs.bookmarks = bookmarksArr;
250
317
  if (emptyHyperlinks) attrs._emptyHyperlinks = emptyHyperlinks;
@@ -313,7 +380,7 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, getI
313
380
  });
314
381
  }
315
382
  function canCarryTrackedRunMark(node, markType) {
316
- return node.isText || node.isInline && node.type.allowsMarkType(markType) && (node.type.name === "image" || node.type.name === "shape" || node.type.name === "hardBreak" || node.type.name === "tab" || node.type.name === "symbol" || node.type.name === "textBoxAnchor");
383
+ return node.isText || node.isInline && node.type.allowsMarkType(markType) && (node.type.name === "image" || node.type.name === "shape" || node.type.name === "hardBreak" || node.type.name === "tab" || node.type.name === "symbol" || node.type.name === "bookmarkBoundary" || node.type.name === "textBoxAnchor");
317
384
  }
318
385
  /**
319
386
  * Convert ParagraphFormatting to ProseMirror paragraph attrs
@@ -736,6 +803,7 @@ function convertTable(table, styleResolver, context) {
736
803
  const resolvedTableBorders = table.formatting?.borders ?? tableStyle?.tblPr?.borders ?? fallbackTableStyle?.tblPr?.borders;
737
804
  const fallbackTableIndent = fallbackTableStyle?.tblPr?.indent;
738
805
  const resolvedTableIndent = table.formatting?.indent ?? tableStyle?.tblPr?.indent ?? (fallbackTableIndent?.value === 0 ? void 0 : fallbackTableIndent);
806
+ const resolvedTableJustification = tableStyle?.tblPr?.justification ?? fallbackTableStyle?.tblPr?.justification;
739
807
  const resolvedTableBidi = table.formatting?.bidi ?? tableStyle?.tblPr?.bidi ?? fallbackTableStyle?.tblPr?.bidi;
740
808
  const tableCellMargins = table.formatting?.cellMargins ?? tableStyle?.tblPr?.cellMargins ?? fallbackTableStyle?.tblPr?.cellMargins;
741
809
  let cellMarginsAttr;
@@ -758,6 +826,7 @@ function convertTable(table, styleResolver, context) {
758
826
  if (table.formatting?.look) attrs.look = table.formatting.look;
759
827
  if (table.formatting?.borders) attrs.borders = table.formatting.borders;
760
828
  if (resolvedTableIndent) attrs._resolvedIndent = resolvedTableIndent;
829
+ if (resolvedTableJustification) attrs._resolvedJustification = resolvedTableJustification;
761
830
  if (resolvedTableBidi !== void 0) attrs._resolvedBidi = resolvedTableBidi;
762
831
  if (table.formatting) attrs._originalFormatting = table.formatting;
763
832
  if (table.propertyChanges && table.propertyChanges.length > 0) attrs.tblPrChange = [...table.propertyChanges];
@@ -797,7 +866,8 @@ function convertTable(table, styleResolver, context) {
797
866
  })();
798
867
  })();
799
868
  if (bandingEnabledH && !isFirstRowStyled && !isLastRow) dataRowIndex++;
800
- return convertTableRow(row, styleResolver, context, isFirstRowStyled, columnWidths, totalWidth, conditionalStyles, rowBandStyle, bandingEnabledV, look, resolvedTableBorders, rowIndex, totalRows, totalColumns, rowSpanMap, cellMarginsAttr);
869
+ const resolvedRowJustification = tableStyle?.trPr?.justification ?? fallbackTableStyle?.trPr?.justification;
870
+ return convertTableRow(row, styleResolver, context, isFirstRowStyled, columnWidths, totalWidth, conditionalStyles, rowBandStyle, bandingEnabledV, look, resolvedTableBorders, rowIndex, totalRows, totalColumns, rowSpanMap, cellMarginsAttr, resolvedRowJustification);
801
871
  });
802
872
  return schema.node("table", attrs, rows);
803
873
  }
@@ -814,11 +884,12 @@ function countTableColumns(rows) {
814
884
  /**
815
885
  * Convert a TableRow to a ProseMirror table row node
816
886
  */
817
- function convertTableRow(row, styleResolver, context, isHeaderRow, columnWidths, totalWidth, conditionalStyles, rowBandStyle, bandingEnabledV, tableLook, tableBorders, rowIndex, totalRows, totalColumns, rowSpanMap, defaultCellMargins) {
887
+ function convertTableRow(row, styleResolver, context, isHeaderRow, columnWidths, totalWidth, conditionalStyles, rowBandStyle, bandingEnabledV, tableLook, tableBorders, rowIndex, totalRows, totalColumns, rowSpanMap, defaultCellMargins, resolvedJustification) {
818
888
  const attrsWithoutStructuralChange = { isHeader: !!row.formatting?.header };
819
889
  if (row.formatting?.height?.value !== void 0) attrsWithoutStructuralChange.height = row.formatting.height.value;
820
890
  if (row.formatting?.heightRule) attrsWithoutStructuralChange.heightRule = row.formatting.heightRule;
821
891
  if (row.formatting?.hidden) attrsWithoutStructuralChange.hidden = true;
892
+ if (resolvedJustification) attrsWithoutStructuralChange._resolvedJustification = resolvedJustification;
822
893
  if (row.formatting) attrsWithoutStructuralChange._originalFormatting = row.formatting;
823
894
  if (row.propertyChanges && row.propertyChanges.length > 0) attrsWithoutStructuralChange.trPrChange = [...row.propertyChanges];
824
895
  let attrs = attrsWithoutStructuralChange;
@@ -1011,7 +1082,8 @@ function standaloneTableCellToProseMirror(cell, nodeType) {
1011
1082
  context: {
1012
1083
  theme: null,
1013
1084
  nextTextBoxGroupId: createTextBoxGroupIdFactory(),
1014
- nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator()
1085
+ nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator(),
1086
+ pairedBookmarkIds: collectPairedBookmarkIds(cell.content)
1015
1087
  },
1016
1088
  isHeader: nodeType === "tableHeader",
1017
1089
  gridWidthPercent: void 0,
@@ -1024,31 +1096,47 @@ function standaloneTableCellToProseMirror(cell, nodeType) {
1024
1096
  defaultCellMargins: void 0
1025
1097
  });
1026
1098
  }
1027
- /**
1028
- * Convert a SimpleField or ComplexField to a ProseMirror field node.
1029
- * Preserves run formatting (bold, fontSize, color, etc.) as PM marks.
1030
- * Accepts a run formatting resolver so fields inherit paragraph-level
1031
- * formatting the same way regular text runs do.
1032
- */
1033
- function convertField(field, getInheritedRunFormatting, styleResolver) {
1099
+ function convertField(field, { getInheritedRunFormatting, styleResolver, nextHyperlinkInstanceIndex, textBoxAnchors }) {
1034
1100
  let displayText = "";
1035
1101
  let fieldFormatting;
1036
- const runs = field.type === "simpleField" ? field.content : field.fieldResult;
1037
- for (const r of runs) if (r.type === "run") {
1038
- for (const c of r.content) if (c.type === "text") displayText += c.text;
1039
- if (!fieldFormatting && r.formatting) fieldFormatting = r.formatting;
1102
+ const inlineNodes = [];
1103
+ const hasStructuredSourceContent = field.type === "simpleField" && field.content.some((content) => content.type === "hyperlink");
1104
+ const appendRun = (run) => {
1105
+ for (const content of run.content) if (content.type === "text") displayText += content.text;
1106
+ fieldFormatting ??= run.formatting;
1107
+ if (!hasStructuredSourceContent) return;
1108
+ inlineNodes.push(...convertRun(run, getInheritedRunFormatting(run.formatting, field.fieldType), styleResolver, textBoxAnchors));
1109
+ };
1110
+ if (field.type === "simpleField") for (const content of field.content) {
1111
+ if (content.type === "run") {
1112
+ appendRun(content);
1113
+ continue;
1114
+ }
1115
+ for (const child of content.children) if (child.type === "run") {
1116
+ for (const runContent of child.content) if (runContent.type === "text") displayText += runContent.text;
1117
+ fieldFormatting ??= child.formatting;
1118
+ }
1119
+ inlineNodes.push(...convertHyperlink(content, {
1120
+ getInheritedRunFormatting: (formatting) => getInheritedRunFormatting(formatting, field.fieldType),
1121
+ styleResolver,
1122
+ hyperlinkIndex: nextHyperlinkInstanceIndex(),
1123
+ textBoxAnchors
1124
+ }));
1040
1125
  }
1126
+ else for (const run of field.fieldResult) appendRun(run);
1041
1127
  if (!fieldFormatting && field.type === "complexField" && field.fieldResult.length === 0) fieldFormatting = field.formatting;
1042
1128
  const inheritedFormatting = getInheritedRunFormatting(fieldFormatting, field.fieldType);
1043
1129
  const { marks } = buildRunMarks(fieldFormatting, inheritedFormatting, styleResolver);
1044
- return schema.node("field", {
1130
+ const hasConvertedHyperlinkContent = inlineNodes.some((node) => node.marks.some((mark) => mark.type.name === "hyperlink"));
1131
+ const createStructuredField = hasStructuredSourceContent && hasConvertedHyperlinkContent;
1132
+ return schema.node(createStructuredField ? "structuredField" : "field", {
1045
1133
  fieldType: field.fieldType,
1046
1134
  instruction: field.instruction,
1047
1135
  displayText,
1048
1136
  fieldKind: field.type === "simpleField" ? "simple" : "complex",
1049
1137
  fldLock: field.fldLock ?? false,
1050
1138
  dirty: field.dirty ?? false
1051
- }, void 0, marks);
1139
+ }, createStructuredField ? inlineNodes : void 0, marks);
1052
1140
  }
1053
1141
  /**
1054
1142
  * Convert a MathEquation to a ProseMirror math node.
@@ -1078,7 +1166,12 @@ function convertInlineSdt(sdt, nextHyperlinkInstanceIndex, getInheritedRunFormat
1078
1166
  });
1079
1167
  inlineNodes.push(...linkNodes);
1080
1168
  } else if (content.type === "simpleField" || content.type === "complexField") {
1081
- const fieldNode = convertField(content, getInheritedRunFormatting, styleResolver);
1169
+ const fieldNode = convertField(content, {
1170
+ getInheritedRunFormatting,
1171
+ styleResolver,
1172
+ nextHyperlinkInstanceIndex,
1173
+ textBoxAnchors
1174
+ });
1082
1175
  if (fieldNode) inlineNodes.push(fieldNode);
1083
1176
  } else if (content.type === "inlineSdt") {
1084
1177
  const nestedSdt = convertInlineSdt(content, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, textBoxAnchors);
@@ -1338,11 +1431,30 @@ function convertHyperlink(hyperlink, { getInheritedRunFormatting, styleResolver,
1338
1431
  rId: hyperlink.rId,
1339
1432
  _docxHyperlinkIndex: hyperlinkIndex
1340
1433
  });
1341
- for (const child of hyperlink.children) if (child.type === "run") {
1342
- const inheritedFormatting = getInheritedRunFormatting(child.formatting);
1343
- const { marks: runMarks, mergedFormatting } = buildRunMarks(child.formatting, inheritedFormatting, styleResolver);
1344
- const allMarks = [...runMarks, linkMark];
1345
- for (const content of child.content) nodes.push(...convertRunContent(content, allMarks, mergedFormatting, textBoxAnchors));
1434
+ for (const child of hyperlink.children) {
1435
+ if (child.type === "bookmarkStart") {
1436
+ nodes.push(schema.node("bookmarkBoundary", {
1437
+ type: "start",
1438
+ id: child.id,
1439
+ name: child.name,
1440
+ colFirst: child.colFirst,
1441
+ colLast: child.colLast
1442
+ }, void 0, [linkMark]));
1443
+ continue;
1444
+ }
1445
+ if (child.type === "bookmarkEnd") {
1446
+ nodes.push(schema.node("bookmarkBoundary", {
1447
+ type: "end",
1448
+ id: child.id
1449
+ }, void 0, [linkMark]));
1450
+ continue;
1451
+ }
1452
+ if (child.type === "run") {
1453
+ const inheritedFormatting = getInheritedRunFormatting(child.formatting);
1454
+ const { marks: runMarks, mergedFormatting } = buildRunMarks(child.formatting, inheritedFormatting, styleResolver);
1455
+ const allMarks = [...runMarks, linkMark];
1456
+ for (const content of child.content) nodes.push(...convertRunContent(content, allMarks, mergedFormatting, textBoxAnchors));
1457
+ }
1346
1458
  }
1347
1459
  return nodes;
1348
1460
  }
@@ -1386,7 +1498,7 @@ function textFormattingToMarks(formatting, options) {
1386
1498
  if (formatting.smallCaps) marks.push(schema.mark("smallCaps"));
1387
1499
  const spacing = typeof formatting.spacing === "number" ? formatting.spacing : null;
1388
1500
  const position = typeof formatting.position === "number" ? formatting.position : null;
1389
- const scale = typeof formatting.scale === "number" ? formatting.scale : null;
1501
+ const scale = normalizeHorizontalScalePercent(formatting.scale) ?? null;
1390
1502
  const kerning = typeof formatting.kerning === "number" ? formatting.kerning : null;
1391
1503
  if (spacing !== null || position !== null || scale !== null || kerning !== null) marks.push(schema.mark("characterSpacing", {
1392
1504
  spacing,
@@ -1511,7 +1623,7 @@ function convertShape(shape) {
1511
1623
  }
1512
1624
  function convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId, context, extraRunFormatting, tableParagraphOverlay }) {
1513
1625
  const { textBoxes, textBoxAnchors } = extractTextBoxesFromParagraph(block, textBoxGroupId);
1514
- const pmParagraph = convertParagraph(block, styleResolver, context.nextHyperlinkInstanceIndex, void 0, extraRunFormatting, tableParagraphOverlay, textBoxAnchors);
1626
+ const pmParagraph = convertParagraph(block, styleResolver, context.nextHyperlinkInstanceIndex, context.pairedBookmarkIds, void 0, extraRunFormatting, tableParagraphOverlay, textBoxAnchors);
1515
1627
  const nodes = [];
1516
1628
  const isEmptyAfterExtraction = textBoxes.length > 0 && !hasContentBesidesTextBoxAnchors(pmParagraph);
1517
1629
  const keepWrapperParagraph = isEmptyAfterExtraction && hasParagraphBoundaryPayload(block, pmParagraph);
@@ -1657,6 +1769,7 @@ function textBoxFromShape(shape, textBody) {
1657
1769
  if (textBody.margins) textBox.margins = textBody.margins;
1658
1770
  if (textBody.autoFit) textBox.autoFit = textBody.autoFit;
1659
1771
  if (textBody.textWrap) textBox.textWrap = textBody.textWrap;
1772
+ if (textBody.anchor) textBox.verticalAlign = textBody.anchor;
1660
1773
  return textBox;
1661
1774
  }
1662
1775
  /**
@@ -1732,6 +1845,7 @@ function convertTextBox(textBox, styleResolver, options) {
1732
1845
  height: heightPx,
1733
1846
  autoFit: textBox.autoFit,
1734
1847
  textWrap: textBox.textWrap,
1848
+ verticalAlign: textBox.verticalAlign,
1735
1849
  textBoxId: textBox.id,
1736
1850
  fillColor,
1737
1851
  outlineWidth,
@@ -1773,7 +1887,8 @@ function headerFooterToProseDoc(content, options) {
1773
1887
  const conversionContext = {
1774
1888
  theme,
1775
1889
  nextTextBoxGroupId,
1776
- nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator()
1890
+ nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator(),
1891
+ pairedBookmarkIds: collectPairedBookmarkIds(content)
1777
1892
  };
1778
1893
  const convertBlocks = (blocks) => {
1779
1894
  const out = [];
@@ -1787,7 +1902,7 @@ function headerFooterToProseDoc(content, options) {
1787
1902
  };
1788
1903
  nodes.push(...convertBlocks(content));
1789
1904
  if (nodes.length === 0) nodes.push(schema.node("paragraph", {}, []));
1790
- const pmDoc = schema.node("doc", null, nodes);
1905
+ const pmDoc = stampNumberedRefFieldBaselines(schema.node("doc", null, nodes));
1791
1906
  assertValidProseMirrorDocument(pmDoc, "Header/footer conversion produced an invalid ProseMirror document");
1792
1907
  return pmDoc;
1793
1908
  }
@@ -43,7 +43,8 @@ import { EmbossExtension, EmphasisMarkExtension, ImprintExtension, TextOutlineEx
43
43
  import { DeletionExtension, InsertionExtension, RunPropertyChangeExtension } from "./marks/TrackedChangeExtensions.js";
44
44
  import { UnderlineExtension } from "./marks/UnderlineExtension.js";
45
45
  import { BlockSdtExtension } from "./nodes/BlockSdtExtension.js";
46
- import { FieldExtension } from "./nodes/FieldExtension.js";
46
+ import { BookmarkBoundaryExtension } from "./nodes/BookmarkBoundaryExtension.js";
47
+ import { FieldExtension, StructuredFieldExtension } from "./nodes/FieldExtension.js";
47
48
  import { HardBreakExtension } from "./nodes/HardBreakExtension.js";
48
49
  import { HorizontalRuleExtension } from "./nodes/HorizontalRuleExtension.js";
49
50
  import { ImageExtension } from "./nodes/ImageExtension.js";
@@ -64,6 +65,11 @@ import { TextBoxExtension } from "./nodes/TextBoxExtension.js";
64
65
  function createStarterKit(options = {}) {
65
66
  const disabled = options.disable ? new Set(options.disable) : /* @__PURE__ */ new Set();
66
67
  const extensions = [];
68
+ let internalClipboardToken;
69
+ const getInternalClipboardToken = () => {
70
+ internalClipboardToken ??= globalThis.crypto.randomUUID();
71
+ return internalClipboardToken;
72
+ };
67
73
  function add(name, ext) {
68
74
  if (!disabled.has(name)) extensions.push(ext);
69
75
  }
@@ -105,12 +111,13 @@ function createStarterKit(options = {}) {
105
111
  add("insertion", InsertionExtension());
106
112
  add("deletion", DeletionExtension());
107
113
  add("runPropertyChange", RunPropertyChangeExtension());
114
+ add("bookmarkBoundary", BookmarkBoundaryExtension({ getInternalClipboardToken }));
108
115
  add("hardBreak", HardBreakExtension());
109
116
  add("tab", TabExtension());
110
117
  add("symbol", SymbolExtension());
111
118
  add("image", ImageExtension());
112
119
  add("textBox", TextBoxExtension());
113
- add("textBoxAnchor", TextBoxAnchorExtension());
120
+ add("textBoxAnchor", TextBoxAnchorExtension({ getInternalClipboardToken }));
114
121
  add("shape", ShapeExtension());
115
122
  add("imageDrag", ImageDragExtension());
116
123
  add("imagePaste", ImagePasteExtension());
@@ -120,11 +127,12 @@ function createStarterKit(options = {}) {
120
127
  add("pageBreak", PageBreakExtension());
121
128
  add("renderedPageBreak", RenderedPageBreakExtension());
122
129
  add("field", FieldExtension());
130
+ add("field", StructuredFieldExtension({ getInternalClipboardToken }));
123
131
  add("sdt", SdtExtension());
124
132
  add("blockSdt", BlockSdtExtension());
125
133
  add("math", MathExtension());
126
134
  if (!disabled.has("table")) extensions.push(...createTableExtensions());
127
- add("pasteCleanup", PasteCleanupExtension());
135
+ add("pasteCleanup", PasteCleanupExtension({ getInternalClipboardToken }));
128
136
  add("pasteStyleInliner", PasteStyleInlinerExtension());
129
137
  add("list", ListExtension());
130
138
  add("baseKeymap", BaseKeymapExtension());
@@ -9,12 +9,16 @@ const autoBidiDetectionKey = new PluginKey("autoBidiDetection");
9
9
  const paragraphDirectionalText = (node) => {
10
10
  let text = "";
11
11
  node.descendants((child) => {
12
+ if (child.marks.some((mark) => mark.type.name === "deletion")) return false;
12
13
  if (child.isText) {
13
- if (!child.marks.some((mark) => mark.type.name === "deletion")) text += child.text ?? "";
14
- } else if (child.type.name === "field") {
15
- const display = child.attrs["displayText"];
16
- if (typeof display === "string") text += display;
14
+ text += child.text ?? "";
15
+ return false;
17
16
  }
17
+ if (child.isLeaf && child.textContent) {
18
+ text += child.textContent;
19
+ return false;
20
+ }
21
+ return true;
18
22
  });
19
23
  return text;
20
24
  };
@@ -14,6 +14,9 @@ import { Extension } from "../types.js";
14
14
  * 2. `Mod-Alt-v` — "paste without formatting", inserting clipboard text with
15
15
  * the source formatting stripped (see {@link pasteWithoutFormatting}).
16
16
  */
17
- declare const PasteCleanupExtension: (options?: Partial<Record<string, unknown>> | undefined) => Extension;
17
+ type PasteCleanupOptions = {
18
+ getInternalClipboardToken?: () => string;
19
+ };
20
+ declare const PasteCleanupExtension: (options?: Partial<PasteCleanupOptions> | undefined) => Extension;
18
21
  //#endregion
19
22
  export { PasteCleanupExtension };
@@ -1,7 +1,7 @@
1
1
  import { pasteWithoutFormatting } from "../../commands/pastePlainText.js";
2
2
  import { createExtension } from "../create.js";
3
3
  import { Priority } from "../types.js";
4
- import { cleanPastedHtml } from "./pasteCleanup.js";
4
+ import { cleanPastedHtml, removeUnpairedBookmarkBoundaries } from "./pasteCleanup.js";
5
5
  import { Plugin } from "prosemirror-state";
6
6
  //#region src/prosemirror/extensions/features/PasteCleanupExtension.ts
7
7
  /**
@@ -21,11 +21,15 @@ import { Plugin } from "prosemirror-state";
21
21
  const PasteCleanupExtension = createExtension({
22
22
  name: "pasteCleanup",
23
23
  priority: Priority.Highest,
24
- onSchemaReady() {
24
+ onSchemaReady(_context, options) {
25
25
  return {
26
- plugins: [new Plugin({ props: { transformPastedHTML(html) {
27
- return cleanPastedHtml(html);
28
- } } })],
26
+ plugins: [new Plugin({ props: {
27
+ transformPastedHTML(html) {
28
+ const internalClipboardToken = options.getInternalClipboardToken?.();
29
+ return cleanPastedHtml(html, { ...internalClipboardToken ? { internalClipboardToken } : {} });
30
+ },
31
+ transformPasted: removeUnpairedBookmarkBoundaries
32
+ } })],
29
33
  keyboardShortcuts: { "Mod-Alt-v": pasteWithoutFormatting }
30
34
  };
31
35
  }
@@ -1,31 +1,18 @@
1
+ import { Slice } from "prosemirror-model";
1
2
  //#region src/prosemirror/extensions/features/pasteCleanup.d.ts
2
- /**
3
- * Office / web paste cleanup
4
- *
5
- * Content pasted from word processors and web pages arrives wrapped in a large
6
- * amount of producer-specific cruft: conditional comments, XML processing
7
- * instructions, namespaced markup (`<o:p>`, `<w:sdt>`, smart tags), `mso-*`
8
- * style declarations, and empty spans. None of it maps to the editor schema,
9
- * and some of it confuses the browser HTML parser that ProseMirror's clipboard
10
- * pipeline relies on. Producer class names (`MsoNormal`, ...) are deliberately
11
- * kept so the downstream style inliner can match `<style>` rules against them.
12
- *
13
- * `cleanPastedHtml` normalizes the raw clipboard HTML string into something the
14
- * schema's `parseDOM` rules can read cleanly. It is a pure string transform so
15
- * it runs in the editor and in tests without a DOM, and it is deliberately
16
- * conservative: it strips producer metadata but never rewrites visible text
17
- * (curly quotes, non-Latin scripts, and whitespace between words are left
18
- * untouched, since this editor targets an international, typography-sensitive
19
- * audience).
20
- */
21
3
  /**
22
4
  * Strip Office/web producer cruft from a raw clipboard HTML string.
23
5
  *
24
- * Best-effort and non-throwing: any unexpected failure returns the original
25
- * markup so a paste degrades to the browser default rather than losing content.
6
+ * Best-effort and non-throwing: any unexpected failure returns empty markup so
7
+ * reconstruction capabilities and other untrusted attributes fail closed.
26
8
  * `<style>` blocks are intentionally left in place for the downstream style
27
9
  * inliner, which resolves class-based CSS before the schema parser runs.
28
10
  */
29
- declare function cleanPastedHtml(html: string): string;
11
+ type CleanPastedHtmlOptions = {
12
+ internalClipboardToken?: string;
13
+ };
14
+ declare function cleanPastedHtml(html: string, options?: CleanPastedHtmlOptions): string;
15
+ /** Remove incomplete or duplicate bookmark pairs at copied slice edges. */
16
+ declare function removeUnpairedBookmarkBoundaries(slice: Slice): Slice;
30
17
  //#endregion
31
- export { cleanPastedHtml };
18
+ export { cleanPastedHtml, removeUnpairedBookmarkBoundaries };