@stll/folio-core 0.33.2 → 0.35.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 (49) hide show
  1. package/dist/ai-edits/apply.js +137 -23
  2. package/dist/ai-edits/snapshot.d.ts +3 -1
  3. package/dist/ai-edits/snapshot.js +44 -4
  4. package/dist/ai-edits/types.d.ts +6 -6
  5. package/dist/compare/compare.js +15 -3
  6. package/dist/compare/formatting.d.ts +1 -1
  7. package/dist/compare/formatting.js +38 -9
  8. package/dist/compare/verification.js +18 -1
  9. package/dist/controller/headerFooterEditorManager.js +11 -9
  10. package/dist/controller/layoutPipeline.js +24 -3
  11. package/dist/display-list/build/watermarkPrimitives.js +15 -3
  12. package/dist/document-operations.js +30 -3
  13. package/dist/docx/headerFooterParser.js +8 -14
  14. package/dist/docx/paragraphParser.js +45 -0
  15. package/dist/docx/serializer/headerFooterSerializer.js +21 -2
  16. package/dist/docx/serializer/paragraphSerializer.d.ts +1 -1
  17. package/dist/docx/serializer/paragraphSerializer.js +19 -9
  18. package/dist/docx/settingsParser.js +3 -0
  19. package/dist/docx/watermarkParser.d.ts +2 -4
  20. package/dist/docx/watermarkParser.js +4 -6
  21. package/dist/headless-layout.js +14 -2
  22. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -2
  23. package/dist/layout-bridge/convert/footnoteLayout.js +23 -10
  24. package/dist/layout-bridge/convert/headerFooterLayout.js +13 -2
  25. package/dist/layout-bridge/convert/toFlowBlocks.js +96 -13
  26. package/dist/layout-engine/index.js +11 -4
  27. package/dist/layout-engine/justifiedLineFit.d.ts +4 -4
  28. package/dist/layout-engine/justifiedLineFit.js +4 -4
  29. package/dist/layout-engine/measure/lineBreakProvider.js +1 -0
  30. package/dist/layout-engine/measure/measureBlocks.js +1 -1
  31. package/dist/layout-engine/measure/measureParagraph.js +29 -29
  32. package/dist/layout-painter/renderPage.js +6 -1
  33. package/dist/layout-painter/renderParagraph.js +18 -8
  34. package/dist/layout-painter/renderWatermark.js +11 -4
  35. package/dist/prosemirror/attrs/index.js +11 -0
  36. package/dist/prosemirror/commands/comments.js +22 -2
  37. package/dist/prosemirror/conversion/fromProseDoc.js +32 -5
  38. package/dist/prosemirror/conversion/toProseDoc.js +49 -11
  39. package/dist/prosemirror/extensions/core/DocExtension.js +7 -1
  40. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  41. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +1 -0
  42. package/dist/prosemirror/plugins/templateDirectives.d.ts +14 -7
  43. package/dist/prosemirror/plugins/templateDirectives.js +26 -19
  44. package/dist/prosemirror/plugins/templateSlashMenu.js +2 -2
  45. package/dist/prosemirror/schema/marks.d.ts +2 -8
  46. package/dist/utils/fontResolver.js +51 -0
  47. package/dist/utils/formatToStyle.js +41 -1
  48. package/dist/watermark/index.js +7 -0
  49. package/package.json +3 -3
@@ -13,6 +13,7 @@ import { advanceListMarker, advanceVisibleListMarker, cloneListCounterState, for
13
13
  import { resolveNumberedRefFields } from "../../prosemirror/numberedRefFields.js";
14
14
  import { directionToBidi } from "../../prosemirror/paragraphDirection.js";
15
15
  import { cascadeStyleTextFormatting } from "../../prosemirror/styles/styleToggleCascade.js";
16
+ import { expectTextBoxAnchorAttrs } from "../../prosemirror/textBoxAnchorAttrs.js";
16
17
  import { assertValidProseMirrorDocument } from "../../prosemirror/validation.js";
17
18
  import { normalizeShapeTextAnchor } from "../../types/documentEnumValues.js";
18
19
  import { resolveColor, resolveHighlightToCss } from "../../utils/colorResolver.js";
@@ -20,12 +21,22 @@ import { resolveThemeFont } from "../../utils/fontResolver.js";
20
21
  import { resolveShadingFill } from "../../utils/formatToStyle.js";
21
22
  import { normalizeHorizontalScalePercent } from "../../utils/horizontalScale.js";
22
23
  import { decodeOoxmlSymbolCharacter } from "../../utils/ooxmlSymbol.js";
24
+ import { sanitizeImageSrc } from "../../utils/sanitizeImageSrc.js";
23
25
  import { tableOfContentsStyleLevel } from "../../utils/tableOfContentsStyle.js";
24
26
  import { AUTO_PARAGRAPH_SPACING_PX, halfPointsToPixels, halfPointsToPoints, pointsToPixels } from "../../utils/units.js";
25
27
  import { getColumns } from "../sectionColumns.js";
26
28
  import { groupParagraphFrames } from "./paragraphFrames.js";
29
+ import { panic } from "better-result";
27
30
  //#region src/layout-bridge/convert/toFlowBlocks.ts
31
+ const DETACHED_WATERMARK_HOST_ATTR = "_detachedWatermarkHost";
32
+ const expectDetachedWatermarkHostAttr = (attrs) => {
33
+ const value = Reflect.get(attrs, DETACHED_WATERMARK_HOST_ATTR);
34
+ if (value === null || value === void 0) return false;
35
+ if (typeof value !== "boolean") panic("Invalid ProseMirror detached watermark host attrs:\nparagraph.attrs._detachedWatermarkHost: Expected a boolean.");
36
+ return value;
37
+ };
28
38
  const DEFAULT_FONT = "Calibri";
39
+ const TEXT_BOX_ANCHOR_BLOCK_ID = Symbol.for("stll.textBoxAnchorBlockId");
29
40
  const DEFAULT_TABLE_CELL_MARGIN_TWIPS = {
30
41
  top: 0,
31
42
  right: 108,
@@ -454,6 +465,9 @@ function buildImageRun(attrs, constrained, pmStart, pmEnd, trackedChange) {
454
465
  if (trackedChange?.changeRevisionId !== void 0) run.changeRevisionId = trackedChange.changeRevisionId;
455
466
  return run;
456
467
  }
468
+ /** A package image whose bytes cannot paint still owns its authored line box. */
469
+ const hasRelationshipBackedImageBox = (attrs) => typeof attrs.rId === "string" && attrs.rId.trim().length > 0 && typeof attrs.width === "number" && Number.isFinite(attrs.width) && attrs.width >= 0 && typeof attrs.height === "number" && Number.isFinite(attrs.height) && attrs.height >= 0;
470
+ const hasPaintableImageSource = (src) => sanitizeImageSrc(src) !== void 0;
457
471
  /**
458
472
  * In TOC paragraphs, strip the resolved Hyperlink character-style colour and
459
473
  * underline so the painter's link fallback doesn't fire. The PM doc keeps the
@@ -564,7 +578,7 @@ function paragraphToRuns(node, startPos, _options) {
564
578
  }
565
579
  if (child.type.name === "image") {
566
580
  const attrs = expectImageAttrs(child);
567
- if (!attrs.src) return;
581
+ if (!hasPaintableImageSource(attrs.src) && !hasRelationshipBackedImageBox(attrs)) return;
568
582
  const constrained = constrainImageToPage(attrs.width ?? 100, attrs.height ?? 100, _options.pageContentHeight);
569
583
  const trackedFmt = extractRunFormatting(child.marks, theme, fontAlternates);
570
584
  const run = buildImageRun(attrs, constrained, childPos, childPos + child.nodeSize, trackedFmt);
@@ -878,6 +892,9 @@ function mapTabAlignment(align) {
878
892
  function hasOnlyVisuallyEmptyTextRuns(runs) {
879
893
  return runs.length > 0 && runs.every((run) => run.kind === "text" && run.text.replace(/\u00a0/gu, " ").trim().length === 0);
880
894
  }
895
+ function hasOnlyHiddenTextRuns(runs) {
896
+ return runs.length > 0 && runs.every((run) => run.kind === "text" && run.hidden === true);
897
+ }
881
898
  function convertParagraph(node, startPos, options) {
882
899
  const pmAttrs = expectParagraphAttrs(node);
883
900
  const runs = paragraphToRuns(node, startPos, options);
@@ -903,10 +920,10 @@ function convertParagraph(node, startPos, options) {
903
920
  if (alternate) attrs.defaultAlternateFontFamily = alternate;
904
921
  }
905
922
  }
906
- if (runs.length === 0 && defaultTextFormatting?.hidden === true) {
907
- attrs.suppressEmptyParagraphHeight = true;
908
- if (attrs.listMarker !== void 0) attrs.listMarkerHidden = true;
909
- }
923
+ const isFullyHiddenParagraph = defaultTextFormatting?.hidden === true && (runs.length === 0 || hasOnlyHiddenTextRuns(runs));
924
+ if (isFullyHiddenParagraph && attrs.listMarker !== void 0) attrs.listMarkerHidden = true;
925
+ if (isFullyHiddenParagraph) attrs.suppressEmptyParagraphHeight = true;
926
+ if (runs.length === 0 && expectDetachedWatermarkHostAttr(node.attrs)) attrs.suppressEmptyParagraphHeight = false;
910
927
  const hasVisibleParagraphPayload = attrs.listMarker !== void 0 && !attrs.listMarkerHidden || attrs.borders?.top !== void 0 || attrs.borders?.bottom !== void 0 || attrs.borders?.left !== void 0 || attrs.borders?.right !== void 0 || attrs.borders?.between !== void 0 || attrs.borders?.bar !== void 0 || attrs.shading !== void 0;
911
928
  if (runs.length === 0 && pmAttrs._pageBreakCarrier === true && !hasVisibleParagraphPayload) attrs.suppressEmptyParagraphHeight = true;
912
929
  const bookmarkNames = pmAttrs.bookmarks?.map((b) => b.name);
@@ -934,6 +951,11 @@ function convertParagraph(node, startPos, options) {
934
951
  ...frame.yAlign !== void 0 ? { yAlign: frame.yAlign } : {},
935
952
  ...frame.wrap !== void 0 ? { wrap: frame.wrap } : {}
936
953
  });
954
+ node.descendants((child) => {
955
+ if (child.type.name !== "textBoxAnchor") return true;
956
+ options.textBoxAnchorBlockIds.set(expectTextBoxAnchorAttrs(child).anchorId, block.id);
957
+ return false;
958
+ });
937
959
  return block;
938
960
  }
939
961
  /**
@@ -945,6 +967,7 @@ function convertParagraph(node, startPos, options) {
945
967
  function isPaintlessTerminalParagraph(block) {
946
968
  if (block?.kind !== "paragraph" || block.runs.length !== 0) return false;
947
969
  const attrs = block.attrs;
970
+ if (attrs?.suppressEmptyParagraphHeight === false) return false;
948
971
  return !(attrs?.listMarker !== void 0 && !attrs.listMarkerHidden || attrs?.borders?.top || attrs?.borders?.bottom || attrs?.borders?.left || attrs?.borders?.right || attrs?.borders?.between || attrs?.borders?.bar || attrs?.shading || attrs?.spacingExplicit?.before || attrs?.spacingExplicit?.after || attrs?.pageBreakBefore || attrs?.renderedPageBreakBefore);
949
972
  }
950
973
  function suppressFinalEmptyParagraphAfterTable(blocks) {
@@ -1188,6 +1211,7 @@ function convertTable(node, startPos, options) {
1188
1211
  */
1189
1212
  function convertImage(node, startPos, pageContentHeight) {
1190
1213
  const attrs = expectImageAttrs(node);
1214
+ if (!hasPaintableImageSource(attrs.src) && !hasRelationshipBackedImageBox(attrs)) return;
1191
1215
  const wrapType = attrs.wrapType;
1192
1216
  const shouldAnchor = wrapType === "behind" || wrapType === "inFront";
1193
1217
  const constrained = constrainImageToPage(attrs.width ?? 100, attrs.height ?? 100, pageContentHeight);
@@ -1269,6 +1293,8 @@ function convertTextBoxNode(node, startPos, opts) {
1269
1293
  if (attrs.distRight !== void 0) textBox.distRight = attrs.distRight;
1270
1294
  if (attrs.position !== void 0) textBox.position = attrs.position;
1271
1295
  if (attrs._docxGroupId !== void 0) setTextBoxGroupId(textBox, attrs._docxGroupId);
1296
+ const anchorBlockId = attrs._docxAnchorId ? opts.textBoxAnchorBlockIds.get(attrs._docxAnchorId) : void 0;
1297
+ if (anchorBlockId !== void 0) Reflect.set(textBox, TEXT_BOX_ANCHOR_BLOCK_ID, anchorBlockId);
1272
1298
  return textBox;
1273
1299
  }
1274
1300
  function getLastMapKey(map) {
@@ -1277,6 +1303,41 @@ function getLastMapKey(map) {
1277
1303
  return lastKey;
1278
1304
  }
1279
1305
  /**
1306
+ * Translate section-owned start modes into the boundary-owned values consumed
1307
+ * by the paginator. Each flow section break carries the properties for the
1308
+ * section it ends, so its start mode belongs on the preceding boundary.
1309
+ */
1310
+ function applySectionStartsToBoundaries(blocks, finalSectionStart) {
1311
+ const breakIndexes = [];
1312
+ for (let index = 0; index < blocks.length; index += 1) if (blocks[index]?.kind === "sectionBreak") breakIndexes.push(index);
1313
+ if (breakIndexes.length === 0) return [...blocks];
1314
+ const result = [...blocks];
1315
+ for (let index = 0; index < breakIndexes.length; index += 1) {
1316
+ const boundaryIndex = breakIndexes[index];
1317
+ if (boundaryIndex === void 0) continue;
1318
+ const boundary = blocks[boundaryIndex];
1319
+ if (boundary?.kind !== "sectionBreak") continue;
1320
+ const nextBoundaryIndex = breakIndexes[index + 1];
1321
+ const nextBoundary = nextBoundaryIndex === void 0 ? void 0 : blocks[nextBoundaryIndex];
1322
+ const nextStart = nextBoundary?.kind === "sectionBreak" ? nextBoundary.type : finalSectionStart;
1323
+ const translated = { ...boundary };
1324
+ if (nextStart === void 0) delete translated.type;
1325
+ else translated.type = nextStart;
1326
+ result[boundaryIndex] = translated;
1327
+ }
1328
+ return result;
1329
+ }
1330
+ function readFinalSectionStart(doc) {
1331
+ const sectionStart = doc.attrs["_finalSectionStart"];
1332
+ switch (sectionStart) {
1333
+ case "continuous":
1334
+ case "nextPage":
1335
+ case "oddPage":
1336
+ case "evenPage": return sectionStart;
1337
+ default: return;
1338
+ }
1339
+ }
1340
+ /**
1280
1341
  * Convert a ProseMirror document to FlowBlock array.
1281
1342
  *
1282
1343
  * Walks the document tree, converting each node to the appropriate block type.
@@ -1315,6 +1376,7 @@ function toFlowBlocks(doc, options = {}) {
1315
1376
  final: listCounterState,
1316
1377
  original: originalListCounterState
1317
1378
  },
1379
+ textBoxAnchorBlockIds: /* @__PURE__ */ new Map(),
1318
1380
  numberedRefResults: resolveNumberedRefFields(doc, {
1319
1381
  listCounterState: cloneListCounterState(listCounterState),
1320
1382
  originalListCounterState: cloneListCounterState(originalListCounterState)
@@ -1521,9 +1583,11 @@ function toFlowBlocks(doc, options = {}) {
1521
1583
  case "table":
1522
1584
  trackedPush(convertTable(node, pos, opts));
1523
1585
  break;
1524
- case "image":
1525
- trackedPush(convertImage(node, pos, opts.pageContentHeight));
1586
+ case "image": {
1587
+ const image = convertImage(node, pos, opts.pageContentHeight);
1588
+ if (image !== void 0) trackedPush(image);
1526
1589
  break;
1590
+ }
1527
1591
  case "textBox":
1528
1592
  trackedPush(convertTextBoxNode(node, pos, opts));
1529
1593
  break;
@@ -1548,24 +1612,43 @@ function toFlowBlocks(doc, options = {}) {
1548
1612
  reserveLeadingEmptyOutlineHeight(blocks);
1549
1613
  suppressFinalEmptyParagraphAfterTable(blocks);
1550
1614
  suppressFinalParagraphInRepeatedEmptySuffix(blocks);
1551
- return groupParagraphFrames(applySectionDocumentGrid(mergeRunInParagraphs(blocks), opts.finalSectionDocumentGridLinePitchTwips), nextBlockId);
1615
+ const mergedBlocks = mergeRunInParagraphs(blocks);
1616
+ const tableCellLinePitch = doc.attrs["_adjustLineHeightInTable"] === true ? "sectionGrid" : void 0;
1617
+ return groupParagraphFrames(applySectionStartsToBoundaries(applySectionDocumentGrid(mergedBlocks, {
1618
+ finalLinePitchTwips: opts.finalSectionDocumentGridLinePitchTwips,
1619
+ tableCellLinePitch
1620
+ }), readFinalSectionStart(doc)), nextBlockId);
1552
1621
  }
1553
- function applySectionDocumentGrid(blocks, finalLinePitchTwips) {
1622
+ function applySectionDocumentGrid(blocks, { finalLinePitchTwips, tableCellLinePitch }) {
1554
1623
  const result = [...blocks];
1555
1624
  let sectionStart = 0;
1556
1625
  const stampSection = (end, linePitchTwips) => {
1557
1626
  if (linePitchTwips === void 0 || linePitchTwips <= 0) return;
1558
1627
  const linePitch = twipsToPixels(linePitchTwips);
1559
- for (let index = sectionStart; index < end; index += 1) {
1560
- const block = result[index];
1561
- if (block?.kind !== "paragraph") continue;
1562
- result[index] = {
1628
+ const stampBlock = (block) => {
1629
+ if (block.kind === "paragraph") return {
1563
1630
  ...block,
1564
1631
  attrs: {
1565
1632
  ...block.attrs,
1566
1633
  documentGridLinePitch: linePitch
1567
1634
  }
1568
1635
  };
1636
+ if (block.kind !== "table" || tableCellLinePitch !== "sectionGrid") return block;
1637
+ return {
1638
+ ...block,
1639
+ rows: block.rows.map((row) => ({
1640
+ ...row,
1641
+ cells: row.cells.map((cell) => ({
1642
+ ...cell,
1643
+ blocks: cell.blocks.map(stampBlock)
1644
+ }))
1645
+ }))
1646
+ };
1647
+ };
1648
+ for (let index = sectionStart; index < end; index += 1) {
1649
+ const block = result[index];
1650
+ if (!block) continue;
1651
+ result[index] = stampBlock(block);
1569
1652
  }
1570
1653
  };
1571
1654
  for (let index = 0; index < blocks.length; index += 1) {
@@ -500,12 +500,15 @@ function getHeaderRowsHeight(measure, headerRowCount) {
500
500
  return height;
501
501
  }
502
502
  const tableRowStartsWithRenderedPageBreak = (block, rowIndex) => {
503
- const visibleCells = block.rows[rowIndex]?.cells.filter((cell) => cell.blocks.some((cellBlock) => cellBlock.kind !== "paragraph" || !isEmptyParagraph(cellBlock)));
503
+ const row = block.rows[rowIndex];
504
+ const visibleCells = row?.cells.filter((cell) => cell.blocks.some((cellBlock) => cellBlock.kind !== "paragraph" || !isEmptyParagraph(cellBlock)));
504
505
  if (!visibleCells || visibleCells.length === 0) return false;
505
- return visibleCells.every((cell) => {
506
+ const startsWithRenderedPageBreak = (cell) => {
506
507
  const firstVisibleBlock = cell.blocks.find((cellBlock) => cellBlock.kind !== "paragraph" || !isEmptyParagraph(cellBlock));
507
508
  return firstVisibleBlock?.kind === "paragraph" && firstVisibleBlock.attrs?.renderedPageBreakBefore === true;
508
- });
509
+ };
510
+ if (row?.heightRule === "exact") return startsWithRenderedPageBreak(visibleCells[0]);
511
+ return visibleCells.every(startsWithRenderedPageBreak);
509
512
  };
510
513
  const getVerticallyMergedRows = (block) => {
511
514
  const mergedRows = /* @__PURE__ */ new Set();
@@ -866,6 +869,8 @@ function layoutAnchoredImage(block, measure, paginator) {
866
869
  };
867
870
  paginator.addUnflowedFragment(fragment);
868
871
  }
872
+ const TEXT_BOX_ANCHOR_BLOCK_ID = Symbol.for("stll.textBoxAnchorBlockId");
873
+ const readTextBoxAnchorBlockId = (block) => Reflect.get(block, TEXT_BOX_ANCHOR_BLOCK_ID);
869
874
  /**
870
875
  * Layout a text box block onto pages.
871
876
  */
@@ -909,12 +914,14 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
909
914
  boxWidth: measure.width
910
915
  }) : paginator.getColumnX(state.columnIndex);
911
916
  const vertical = block.position.vertical;
917
+ const anchorBlockId = readTextBoxAnchorBlockId(block);
918
+ const anchorParagraph = vertical?.relativeTo === "paragraph" && typeof anchorBlockId === "string" ? state.page.fragments.find((fragment) => fragment.kind === "paragraph" && fragment.blockId === anchorBlockId) : void 0;
912
919
  const y = isPageFrameRelativeAnchor(vertical?.relativeTo) ? state.topMargin + bandTopContentY(vertical, {
913
920
  pageHeight: sectionPageHeight,
914
921
  marginTop: sectionMarginTop,
915
922
  marginBottom: sectionMarginBottom,
916
923
  boxHeight: measure.height
917
- }) : state.cursorY + emuToPixels(vertical?.posOffset ?? 0);
924
+ }) : (anchorParagraph?.y ?? state.cursorY) + emuToPixels(vertical?.posOffset ?? 0);
918
925
  const fragment = {
919
926
  kind: "textBox",
920
927
  blockId: block.id,
@@ -1,7 +1,7 @@
1
1
  import { ParagraphBlock } from "./types.js";
2
2
  //#region src/layout-engine/justifiedLineFit.d.ts
3
- declare const JUSTIFIED_LIST_FINAL_LINE_MAX_SHRINK_RATIO = 0.025;
4
- declare const JUSTIFIED_LIST_SPACE_CONTRACTION_RATIO = 0.32;
5
- declare const supportsJustifiedListFinalLineContraction: (block: ParagraphBlock) => boolean;
3
+ declare const JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO = 0.025;
4
+ declare const JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO = 0.32;
5
+ declare const supportsJustifiedFinalLineContraction: (block: ParagraphBlock) => boolean;
6
6
  //#endregion
7
- export { JUSTIFIED_LIST_FINAL_LINE_MAX_SHRINK_RATIO, JUSTIFIED_LIST_SPACE_CONTRACTION_RATIO, supportsJustifiedListFinalLineContraction };
7
+ export { JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO, JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO, supportsJustifiedFinalLineContraction };
@@ -1,6 +1,6 @@
1
1
  //#region src/layout-engine/justifiedLineFit.ts
2
- const JUSTIFIED_LIST_FINAL_LINE_MAX_SHRINK_RATIO = .025;
3
- const JUSTIFIED_LIST_SPACE_CONTRACTION_RATIO = .32;
4
- const supportsJustifiedListFinalLineContraction = (block) => block.attrs?.justificationCompatibility?.type !== "legacy" && block.attrs?.listMarker !== void 0;
2
+ const JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO = .025;
3
+ const JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO = .32;
4
+ const supportsJustifiedFinalLineContraction = (block) => block.attrs?.alignment === "justify" && block.attrs.justificationCompatibility?.type !== "legacy" && (block.attrs.listMarker !== void 0 || (block.attrs.indent?.left ?? 0) > 0);
5
5
  //#endregion
6
- export { JUSTIFIED_LIST_FINAL_LINE_MAX_SHRINK_RATIO, JUSTIFIED_LIST_SPACE_CONTRACTION_RATIO, supportsJustifiedListFinalLineContraction };
6
+ export { JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO, JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO, supportsJustifiedFinalLineContraction };
@@ -169,6 +169,7 @@ const usesEastAsianLineBreaking = (text, locale) => localeUsesEastAsianLineBreak
169
169
  const allowsBreak = (text, index, policy, usesEastAsianRules, nextLineStart) => {
170
170
  const previous = previousCodePoint(text, index);
171
171
  const next = nextLineStart ?? firstCodePoint(text, index);
172
+ if (!(usesEastAsianRules || policy?.kinsoku === true || policy?.noLineBreaksBefore !== void 0 || policy?.noLineBreaksAfter !== void 0)) return true;
172
173
  if (previous === " " && next === "%" && policy?.kinsoku !== true && policy?.noLineBreaksBefore === void 0 && !usesEastAsianRules) return !isProhibitedLineEnd(previous, policy);
173
174
  return !isProhibitedLineEnd(previous, policy) && !isProhibitedLineStart(next, policy);
174
175
  };
@@ -587,7 +587,7 @@ function measureTextBoxBlock(tb, fieldValues) {
587
587
  return {
588
588
  kind: "textBox",
589
589
  width: totalWidth,
590
- height: tb.autoFit === "shape" ? Math.max(tb.height ?? 0, contentBoxHeight) : tb.height ?? contentBoxHeight,
590
+ height: tb.autoFit === "shape" ? contentBoxHeight : tb.height ?? contentBoxHeight,
591
591
  innerMeasures
592
592
  };
593
593
  }
@@ -1,9 +1,8 @@
1
1
  import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
2
2
  import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
3
- import { isRtlParagraph } from "../../utils/paragraphBaseDirection.js";
4
3
  import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
5
4
  import { hasCjk, hasComplexScript } from "../../utils/scriptSegments.js";
6
- import { JUSTIFIED_LIST_FINAL_LINE_MAX_SHRINK_RATIO, JUSTIFIED_LIST_SPACE_CONTRACTION_RATIO, supportsJustifiedListFinalLineContraction } from "../justifiedLineFit.js";
5
+ import { JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO, JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO, supportsJustifiedFinalLineContraction } from "../justifiedLineFit.js";
7
6
  import { measuredLineAdvance } from "../lineFlow.js";
8
7
  import { isFloatingImageRun } from "../types.js";
9
8
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
@@ -382,7 +381,7 @@ function resolveJustifyFitStrategy(block, isFirstLine, profile) {
382
381
  };
383
382
  return {
384
383
  type: "space",
385
- ratio: JUSTIFIED_LIST_SPACE_CONTRACTION_RATIO,
384
+ ratio: JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO,
386
385
  maxWidthRatio: JUSTIFY_SHRINK_TOLERANCE_RATIO
387
386
  };
388
387
  }
@@ -409,10 +408,10 @@ function resolveJustifyFitStrategy(block, isFirstLine, profile) {
409
408
  };
410
409
  }
411
410
  function resolveFinalLineJustifyFitStrategy(block, profile) {
412
- if (supportsJustifiedListFinalLineContraction(block)) return {
411
+ if (supportsJustifiedFinalLineContraction(block)) return {
413
412
  type: "space",
414
- ratio: JUSTIFIED_LIST_SPACE_CONTRACTION_RATIO,
415
- maxWidthRatio: JUSTIFIED_LIST_FINAL_LINE_MAX_SHRINK_RATIO
413
+ ratio: JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO,
414
+ maxWidthRatio: JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO
416
415
  };
417
416
  return resolveJustifyFitStrategy(block, false, profile);
418
417
  }
@@ -433,11 +432,11 @@ function isFinalTextCandidate(block, runIndex, nextBreak) {
433
432
  return true;
434
433
  }
435
434
  function resolveTextCandidateFit({ block, line, isFirstLine, isFinalCandidate, candidateWidth, candidateSpaceWidth, fallbackTolerancePx, continuationStrategy, finalStrategy }) {
436
- if (isFirstLine || !isFinalCandidate || !supportsJustifiedListFinalLineContraction(block)) return {
435
+ if (!isFinalCandidate || !supportsJustifiedFinalLineContraction(block)) return {
437
436
  type: "ordinary",
438
437
  tolerancePx: fallbackTolerancePx
439
438
  };
440
- const ordinaryTolerancePx = justifyFitTolerance(line, continuationStrategy, candidateSpaceWidth);
439
+ const ordinaryTolerancePx = isFirstLine ? fallbackTolerancePx : justifyFitTolerance(line, continuationStrategy, candidateSpaceWidth);
441
440
  if (candidateWidth <= line.availableWidth + ordinaryTolerancePx) return {
442
441
  type: "ordinary",
443
442
  tolerancePx: ordinaryTolerancePx
@@ -713,7 +712,6 @@ function findClearLineY(startY, lineHeight, zones, contentWidth, minWidth) {
713
712
  function measureParagraph(block, maxWidth, options) {
714
713
  const runs = block.runs;
715
714
  const attrs = block.attrs;
716
- const isRtl = isRtlParagraph(block);
717
715
  const spacing = attrs?.spacing;
718
716
  const isJustifiedParagraph = attrs?.alignment === "justify";
719
717
  const justificationProfile = {
@@ -754,24 +752,26 @@ function measureParagraph(block, maxWidth, options) {
754
752
  const firstLineWidth = Math.max(1, getFloatingAvailableWidth(firstLineFloatingMargins, baseFirstLineWidth));
755
753
  const lines = [];
756
754
  let consecutiveHyphenatedLines = 0;
755
+ if (attrs?.suppressEmptyParagraphHeight) {
756
+ const finalRunIndex = Math.max(0, runs.length - 1);
757
+ const finalRun = runs.at(-1);
758
+ lines.push({
759
+ fromRun: 0,
760
+ fromChar: 0,
761
+ toRun: finalRunIndex,
762
+ toChar: finalRun?.kind === "text" ? finalRun.text.length : 0,
763
+ width: 0,
764
+ ascent: 0,
765
+ descent: 0,
766
+ lineHeight: 0
767
+ });
768
+ return {
769
+ kind: "paragraph",
770
+ lines,
771
+ totalHeight: 0
772
+ };
773
+ }
757
774
  if (runs.length === 0) {
758
- if (attrs?.suppressEmptyParagraphHeight) {
759
- lines.push({
760
- fromRun: 0,
761
- fromChar: 0,
762
- toRun: 0,
763
- toChar: 0,
764
- width: 0,
765
- ascent: 0,
766
- descent: 0,
767
- lineHeight: 0
768
- });
769
- return {
770
- kind: "paragraph",
771
- lines,
772
- totalHeight: 0
773
- };
774
- }
775
775
  const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY, attrs);
776
776
  const outlineLineHeight = attrs?.reserveEmptyOutlineHeight ? emptyMetrics.lineHeight * 2 : emptyMetrics.lineHeight;
777
777
  lines.push({
@@ -987,11 +987,11 @@ function measureParagraph(block, maxWidth, options) {
987
987
  let tabWidth = tabResult.width;
988
988
  const authoredEndpoint = contentX + tabWidth + followingWidth;
989
989
  const activeContentRightEdge = maxWidth - currentLine.rightOffset;
990
- const preservesLogicalRtlEndStop = isRtl && tabResult.alignment === "end" && authoredEndpoint <= activeContentRightEdge + WIDTH_TOLERANCE;
990
+ const preservesAuthoredEndStop = tabResult.alignment === "end" && authoredEndpoint <= activeContentRightEdge + WIDTH_TOLERANCE;
991
991
  const landsOnLeftIndent = tabResult.alignment === "start" && indentLeft > 0 && Math.abs(contentX + tabWidth - indentLeft) <= WIDTH_TOLERANCE;
992
992
  const lineRightEdgeX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + currentLine.availableWidth + currentLine.leftOffset;
993
- if (!preservesLogicalRtlEndStop && !landsOnLeftIndent && !hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width, hasPriorTabOnLine(runs, runIndex), followingWidth, currentLine.availableWidth) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
994
- if (preservesLogicalRtlEndStop) currentLine.availableWidth = Math.max(currentLine.availableWidth, currentLine.width + tabWidth + followingWidth);
993
+ if (!preservesAuthoredEndStop && !landsOnLeftIndent && !hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width, hasPriorTabOnLine(runs, runIndex), followingWidth, currentLine.availableWidth) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
994
+ if (preservesAuthoredEndStop) currentLine.availableWidth = Math.max(currentLine.availableWidth, currentLine.width + tabWidth + followingWidth);
995
995
  if (currentLine.width + tabWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
996
996
  startNewLine(runIndex, 0);
997
997
  updateMaxFont(style);
@@ -24,6 +24,7 @@ import { panic } from "better-result";
24
24
  * Renders a single page from Layout data to DOM elements.
25
25
  * Each page contains positioned fragments within a content area.
26
26
  */
27
+ const TEXT_BOX_ANCHOR_BLOCK_ID = Symbol.for("stll.textBoxAnchorBlockId");
27
28
  const floatingTableReservesBand = ({ contentX, tableWidth, contentWidth, distLeft, distRight }) => {
28
29
  const leftClearance = Math.max(0, contentX - distLeft);
29
30
  const rightClearance = Math.max(0, contentWidth - contentX - tableWidth - distRight);
@@ -442,6 +443,7 @@ function renderHeaderFooterContent(content, context, options, layout) {
442
443
  ...context,
443
444
  positioning: "absolute"
444
445
  };
446
+ const paragraphStartYByBlockId = /* @__PURE__ */ new Map();
445
447
  for (let i = 0; i < content.blocks.length; i++) {
446
448
  const block = content.blocks[i];
447
449
  const measure = content.measures[i];
@@ -450,6 +452,7 @@ function renderHeaderFooterContent(content, context, options, layout) {
450
452
  const paragraphBlock = block;
451
453
  const paragraphMeasure = measure;
452
454
  const paragraphStartY = cursorY;
455
+ paragraphStartYByBlockId.set(paragraphBlock.id, paragraphStartY);
453
456
  const inlineRuns = [];
454
457
  for (const run of paragraphBlock.runs) if (run.kind === "image" && (isFloatingImageRun(run) || run.position)) floatingImages.push({
455
458
  src: run.src,
@@ -530,9 +533,11 @@ function renderHeaderFooterContent(content, context, options, layout) {
530
533
  document: doc,
531
534
  renderTable: renderNestedTable
532
535
  });
536
+ const anchorBlockId = Reflect.get(block, TEXT_BOX_ANCHOR_BLOCK_ID);
537
+ const paragraphY = block.position?.vertical?.relativeTo === "paragraph" && typeof anchorBlockId === "string" ? paragraphStartYByBlockId.get(anchorBlockId) ?? cursorY : cursorY;
533
538
  const textBoxTop = block.position ? resolveHeaderFooterFloatTop({
534
539
  height: measure.height,
535
- paragraphY: cursorY,
540
+ paragraphY,
536
541
  position: block.position
537
542
  }, layout) : cursorY;
538
543
  fragEl.style.top = `${textBoxTop}px`;
@@ -14,7 +14,7 @@ import "../utils/fontWeights.js";
14
14
  import { getHorizontalScaleFactor } from "../utils/horizontalScale.js";
15
15
  import { resolvePhysicalParagraphInlineLayout } from "../utils/paragraphInlineLayout.js";
16
16
  import { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox } from "../utils/rotationBoundingBox.js";
17
- import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
17
+ import { sanitizeImageSrc } from "../utils/sanitizeImageSrc.js";
18
18
  import { SCRIPT_CLASS, hasCjk, hasComplexScript, segmentByScript } from "../utils/scriptSegments.js";
19
19
  import { sanitizeExternalUrl } from "../utils/urlSecurity.js";
20
20
  import { borderStrokeToCss, resolveParagraphBorderHorizontalOutsets } from "./borderStroke.js";
@@ -42,6 +42,14 @@ const PARAGRAPH_CLASS_NAMES = {
42
42
  image: "layout-run-image",
43
43
  lineBreak: "layout-run-linebreak"
44
44
  };
45
+ const applyImageRunSource = (img, src) => {
46
+ const safeSrc = sanitizeImageSrc(src);
47
+ if (safeSrc === void 0) {
48
+ img.style.visibility = "hidden";
49
+ return;
50
+ }
51
+ img.src = safeSrc;
52
+ };
45
53
  const LEFT_TO_RIGHT_DIRECTION = "ltr";
46
54
  const RIGHT_TO_LEFT_DIRECTION = "rtl";
47
55
  const DISPLAYED_URL_PATTERN = /^(?:https?:\/\/|www\.)\S+$/iu;
@@ -488,7 +496,7 @@ function getLeaderChar(leader) {
488
496
  function renderInlineImageRun(run, doc) {
489
497
  const img = doc.createElement("img");
490
498
  img.className = `${PARAGRAPH_CLASS_NAMES.run} ${PARAGRAPH_CLASS_NAMES.image}`;
491
- applySanitizedImageSrc(img, run.src);
499
+ applyImageRunSource(img, run.src);
492
500
  img.width = run.width;
493
501
  img.height = run.height;
494
502
  img.style.width = `${run.width}px`;
@@ -556,7 +564,7 @@ function renderBlockImage(run, doc) {
556
564
  container.style.marginTop = `${run.distTop ?? 6}px`;
557
565
  container.style.marginBottom = `${run.distBottom ?? 6}px`;
558
566
  const img = doc.createElement("img");
559
- applySanitizedImageSrc(img, run.src);
567
+ applyImageRunSource(img, run.src);
560
568
  img.width = run.width;
561
569
  img.height = run.height;
562
570
  if (run.alt) img.alt = run.alt;
@@ -1241,7 +1249,7 @@ function renderLine(block, line, alignment, doc, options) {
1241
1249
  }
1242
1250
  }
1243
1251
  lineEl.style.whiteSpace = "pre";
1244
- lineEl.style.overflow = "visible";
1252
+ lineEl.style.overflow = block.attrs?.suppressEmptyParagraphHeight ? "hidden" : "visible";
1245
1253
  let tabContext;
1246
1254
  const hasScaledTextRun = runsForLine.some((run) => (isTextRun(run) || isFieldRun(run) || isMathRun(run)) && getHorizontalScaleFactor(run.horizontalScale) !== 1);
1247
1255
  const measureText = collapsedSpaceMeasureText ?? (hasTabRuns || hasScaledTextRun ? createTextMeasurer(doc) : void 0);
@@ -1285,7 +1293,11 @@ function renderLine(block, line, alignment, doc, options) {
1285
1293
  break;
1286
1294
  }
1287
1295
  }
1288
- if (lineRightEdgeX !== void 0 && options?.isRtl !== true && tabResult.alignment === "end" && !hasFollowingTab && currentX + tabResult.width + followingWidthForCheck >= lineRightEdgeX - RIGHT_EDGE_EPSILON_PX) {
1296
+ const authoredEndpoint = currentX + tabResult.width + followingWidthForCheck;
1297
+ 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;
1299
+ const preservesAuthoredEndStopPastIndent = lineRightEdgeX !== void 0 && preservesAuthoredEndStop && authoredEndpoint > lineRightEdgeX + RIGHT_EDGE_EPSILON_PX;
1300
+ if (lineRightEdgeX !== void 0 && options?.isRtl !== true && tabResult.alignment === "end" && !hasFollowingTab && !preservesAuthoredEndStopPastIndent && authoredEndpoint >= lineRightEdgeX - RIGHT_EDGE_EPSILON_PX) {
1289
1301
  lineEl.style.display = "flex";
1290
1302
  lineEl.style.alignItems = "baseline";
1291
1303
  lineEl.style.whiteSpace = "pre";
@@ -1320,10 +1332,8 @@ function renderLine(block, line, alignment, doc, options) {
1320
1332
  break;
1321
1333
  }
1322
1334
  let tabWidth = tabResult.width;
1323
- const activeContentRightEdge = options?.contentWidthPx === void 0 ? void 0 : options.contentWidthPx - (options.floatingMargins?.rightMargin ?? 0);
1324
- const preservesLogicalRtlEndStop = options?.isRtl === true && tabResult.alignment === "end" && activeContentRightEdge !== void 0 && currentX + tabWidth + followingWidthForCheck <= activeContentRightEdge + RIGHT_EDGE_EPSILON_PX;
1325
1335
  const landsOnLeftIndent = tabResult.alignment === "start" && tabLeftIndentPx > 0 && Math.abs(currentX + tabWidth - tabLeftIndentPx) <= RIGHT_EDGE_EPSILON_PX;
1326
- if (!preservesLogicalRtlEndStop && !landsOnLeftIndent && lineRightEdgeX !== void 0 && canClampTabToRightEdge(tabResult.alignment, i > 0, runsForLine.slice(0, i).some(isTabRun), options?.isLastLine === true) && currentX + tabWidth + followingWidthForCheck > lineRightEdgeX) tabWidth = Math.max(1, lineRightEdgeX - currentX - followingWidthForCheck);
1336
+ if (!preservesAuthoredEndStop && !landsOnLeftIndent && lineRightEdgeX !== void 0 && canClampTabToRightEdge(tabResult.alignment, i > 0, runsForLine.slice(0, i).some(isTabRun), options?.isLastLine === true) && currentX + tabWidth + followingWidthForCheck > lineRightEdgeX) tabWidth = Math.max(1, lineRightEdgeX - currentX - followingWidthForCheck);
1327
1337
  const tabEl = renderTabRun(run, doc, tabWidth, tabResult.leader);
1328
1338
  lineEl.append(tabEl);
1329
1339
  currentX += tabWidth;
@@ -1,7 +1,9 @@
1
1
  import { resolveFontFamily } from "../utils/fontResolver.js";
2
2
  import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
3
+ import { pointsToPixels } from "../utils/units.js";
3
4
  //#region src/layout-painter/renderWatermark.ts
4
5
  const WATERMARK_CLASS = "layout-page-watermark";
6
+ const PICTURE_WASHOUT_OPACITY = .18;
5
7
  /**
6
8
  * Build the watermark overlay element for a page. Returns `null` when
7
9
  * the watermark is a picture without a resolved `imageSrc` (the
@@ -51,10 +53,15 @@ function renderPictureWatermark(watermark, imageSrc, doc) {
51
53
  applySanitizedImageSrc(img, imageSrc);
52
54
  img.alt = "";
53
55
  img.setAttribute("aria-hidden", "true");
54
- const scalePct = (watermark.scale ?? 1) * 100;
55
- img.style.maxWidth = `${scalePct}%`;
56
- img.style.maxHeight = `${scalePct}%`;
57
- img.style.opacity = watermark.washout === false ? "1" : "0.4";
56
+ if (watermark.widthPt !== void 0 && watermark.heightPt !== void 0) {
57
+ img.style.width = `${pointsToPixels(watermark.widthPt)}px`;
58
+ img.style.height = `${pointsToPixels(watermark.heightPt)}px`;
59
+ } else {
60
+ const scalePct = (watermark.scale ?? 1) * 100;
61
+ img.style.maxWidth = `${scalePct}%`;
62
+ img.style.maxHeight = `${scalePct}%`;
63
+ }
64
+ img.style.opacity = String(watermark.washout === false ? 1 : PICTURE_WASHOUT_OPACITY);
58
65
  img.style.objectFit = "contain";
59
66
  return img;
60
67
  }
@@ -91,6 +91,11 @@ const RUN_FORMATTING_OVERRIDE_BOOLEAN_KEYS = [
91
91
  "outline"
92
92
  ];
93
93
  const RUN_FORMATTING_OVERRIDE_FALSE_KEYS = ["doubleStrike", "rtl"];
94
+ const RUN_FORMATTING_OVERRIDE_DIRECT_FONT_PROPERTIES = [
95
+ "fontFamily",
96
+ "fontSize",
97
+ "color"
98
+ ];
94
99
  const SECTION_ORIENTATIONS = ["portrait", "landscape"];
95
100
  const SECTION_START_TYPES = [
96
101
  "continuous",
@@ -678,6 +683,12 @@ const readRunFormattingOverrideMarkAttrs = (mark) => {
678
683
  optionalNumber(attrs, "fontSizeCs", "runFormattingOverride.attrs.fontSizeCs", issues);
679
684
  optionalBoolean(attrs, "cs", "runFormattingOverride.attrs.cs", issues);
680
685
  optionalOneOf(attrs, "underline", "runFormattingOverride.attrs.underline", issues, ["none"]);
686
+ optionalOneOfArray(attrs, "directFontProperties", "runFormattingOverride.attrs.directFontProperties", issues, RUN_FORMATTING_OVERRIDE_DIRECT_FONT_PROPERTIES);
687
+ const directFontProperties = attrs["directFontProperties"];
688
+ if (Array.isArray(directFontProperties) && new Set(directFontProperties).size !== directFontProperties.length) issues.push({
689
+ path: "runFormattingOverride.attrs.directFontProperties",
690
+ message: "Expected unique direct font properties."
691
+ });
681
692
  return attrsResult(attrs, issues);
682
693
  };
683
694
  const expectRunFormattingOverrideMarkAttrs = (mark) => expectCachedMarkAttrs(mark, runFormattingOverrideAttrsCache, readRunFormattingOverrideMarkAttrs, "run formatting override attrs");
@@ -62,6 +62,7 @@ function resolveChange(from, to, mode, revisionIds) {
62
62
  const keepType = mode === "accept" ? insertionType : deletionType;
63
63
  const removeType = mode === "accept" ? deletionType : insertionType;
64
64
  const revisionSet = revisionIds === void 0 ? null : new Set(revisionIds);
65
+ const removeKeptMarksInBulk = revisionSet === null && keepType !== void 0;
65
66
  const matchesRevision = (mark) => revisionSet === null || typeof mark.attrs["revisionId"] === "number" && revisionSet.has(mark.attrs["revisionId"]);
66
67
  if (dispatch) {
67
68
  const tr = state.tr;
@@ -139,10 +140,29 @@ function resolveChange(from, to, mode, revisionIds) {
139
140
  from: rangeFrom,
140
141
  to: rangeTo
141
142
  });
142
- for (const mark of node.marks) if (keepType && mark.type === keepType && matchesRevision(mark)) tr.removeMark(rangeFrom, rangeTo, mark);
143
+ if (!removeKeptMarksInBulk) {
144
+ for (const mark of node.marks) if (keepType && mark.type === keepType && matchesRevision(mark)) tr.removeMark(rangeFrom, rangeTo, mark);
145
+ }
143
146
  return true;
144
147
  });
145
- for (const range of deleteRanges.toReversed()) tr.delete(range.from, range.to);
148
+ if (removeKeptMarksInBulk) tr.removeMark(from, to, keepType);
149
+ let rangesToDelete = deleteRanges;
150
+ if (revisionSet === null) {
151
+ const coalescedDeleteRanges = [];
152
+ for (const range of deleteRanges) {
153
+ const previous = coalescedDeleteRanges.at(-1);
154
+ if (previous && range.from <= previous.to) {
155
+ previous.to = Math.max(previous.to, range.to);
156
+ continue;
157
+ }
158
+ coalescedDeleteRanges.push({
159
+ from: range.from,
160
+ to: range.to
161
+ });
162
+ }
163
+ rangesToDelete = coalescedDeleteRanges;
164
+ }
165
+ for (const range of rangesToDelete.toReversed()) tr.delete(range.from, range.to);
146
166
  pPrMarkOps.sort((a, b) => b.paragraphPos - a.paragraphPos);
147
167
  for (const op of pPrMarkOps) {
148
168
  const mappedPos = tr.mapping.map(op.paragraphPos);