@stll/folio-core 0.33.2 → 0.34.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 (44) hide show
  1. package/dist/ai-edits/apply.js +137 -23
  2. package/dist/ai-edits/snapshot.js +24 -2
  3. package/dist/ai-edits/types.d.ts +6 -6
  4. package/dist/compare/formatting.d.ts +1 -1
  5. package/dist/compare/formatting.js +38 -9
  6. package/dist/compare/verification.js +18 -1
  7. package/dist/controller/headerFooterEditorManager.js +11 -9
  8. package/dist/controller/layoutPipeline.js +24 -3
  9. package/dist/display-list/build/watermarkPrimitives.js +15 -3
  10. package/dist/document-operations.js +30 -3
  11. package/dist/docx/headerFooterParser.js +8 -14
  12. package/dist/docx/paragraphParser.js +45 -0
  13. package/dist/docx/serializer/headerFooterSerializer.js +21 -2
  14. package/dist/docx/serializer/paragraphSerializer.d.ts +1 -1
  15. package/dist/docx/serializer/paragraphSerializer.js +19 -9
  16. package/dist/docx/settingsParser.js +3 -0
  17. package/dist/docx/watermarkParser.d.ts +2 -4
  18. package/dist/docx/watermarkParser.js +4 -6
  19. package/dist/headless-layout.js +14 -2
  20. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -2
  21. package/dist/layout-bridge/convert/footnoteLayout.js +23 -10
  22. package/dist/layout-bridge/convert/headerFooterLayout.js +13 -2
  23. package/dist/layout-bridge/convert/toFlowBlocks.js +96 -13
  24. package/dist/layout-engine/index.js +11 -4
  25. package/dist/layout-engine/justifiedLineFit.d.ts +4 -4
  26. package/dist/layout-engine/justifiedLineFit.js +4 -4
  27. package/dist/layout-engine/measure/lineBreakProvider.js +1 -0
  28. package/dist/layout-engine/measure/measureBlocks.js +1 -1
  29. package/dist/layout-engine/measure/measureParagraph.js +29 -29
  30. package/dist/layout-painter/renderPage.js +6 -1
  31. package/dist/layout-painter/renderParagraph.js +18 -8
  32. package/dist/layout-painter/renderWatermark.js +11 -4
  33. package/dist/prosemirror/attrs/index.js +11 -0
  34. package/dist/prosemirror/commands/comments.js +22 -2
  35. package/dist/prosemirror/conversion/fromProseDoc.js +32 -5
  36. package/dist/prosemirror/conversion/toProseDoc.js +49 -11
  37. package/dist/prosemirror/extensions/core/DocExtension.js +7 -1
  38. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  39. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +1 -0
  40. package/dist/prosemirror/schema/marks.d.ts +2 -8
  41. package/dist/utils/fontResolver.js +51 -0
  42. package/dist/utils/formatToStyle.js +41 -1
  43. package/dist/watermark/index.js +7 -0
  44. package/package.json +2 -2
@@ -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);
@@ -689,9 +689,9 @@ function createTrackedRunWrapper(type, info, child) {
689
689
  content
690
690
  };
691
691
  }
692
- function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, textBoxAnchorMarkers, skipLeadingRenderedPageBreak = false) {
692
+ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, textBoxAnchorMarkers, skipLeadingRenderedPageBreak = false, inheritedFormattingOverride) {
693
693
  const content = [];
694
- const inheritedFormatting = paragraph.type.name === "paragraph" ? expectParagraphAttrs(paragraph).defaultTextFormatting ?? void 0 : void 0;
694
+ const inheritedFormatting = inheritedFormattingOverride ?? (paragraph.type.name === "paragraph" ? expectParagraphAttrs(paragraph).defaultTextFormatting ?? void 0 : void 0);
695
695
  const sortedEmptyHyperlinks = (emptyHyperlinks ?? []).map((attrs, order) => ({
696
696
  attrs,
697
697
  order
@@ -966,7 +966,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
966
966
  }));
967
967
  } else if (node.type.name === "sdt") {
968
968
  flushCurrentInline();
969
- content.push(createInlineSdtFromNode(node, textBoxAnchorMarkers));
969
+ content.push(createInlineSdtFromNode(node, textBoxAnchorMarkers, inheritedFormatting));
970
970
  } else if (node.type.name === "math") {
971
971
  flushCurrentInline();
972
972
  content.push(createMathFromNode(node));
@@ -1369,11 +1369,11 @@ function createMathFromNode(node) {
1369
1369
  /**
1370
1370
  * Create an InlineSdt from a PM sdt node
1371
1371
  */
1372
- function createInlineSdtFromNode(node, textBoxAnchorMarkers) {
1372
+ function createInlineSdtFromNode(node, textBoxAnchorMarkers, inheritedFormatting) {
1373
1373
  return {
1374
1374
  type: "inlineSdt",
1375
1375
  properties: sdtPropertiesFromAttrs(expectSdtAttrs(node)),
1376
- content: extractParagraphContent(node, void 0, void 0, textBoxAnchorMarkers).filter((c) => c.type === "run" || c.type === "hyperlink" || c.type === "simpleField" || c.type === "complexField" || c.type === "inlineSdt" || c.type === "insertion" || c.type === "deletion" || c.type === "moveFrom" || c.type === "moveTo" || c.type === "mathEquation")
1376
+ content: extractParagraphContent(node, void 0, void 0, textBoxAnchorMarkers, false, inheritedFormatting).filter((c) => c.type === "run" || c.type === "hyperlink" || c.type === "simpleField" || c.type === "complexField" || c.type === "inlineSdt" || c.type === "insertion" || c.type === "deletion" || c.type === "moveFrom" || c.type === "moveTo" || c.type === "mathEquation")
1377
1377
  };
1378
1378
  }
1379
1379
  /**
@@ -1524,6 +1524,7 @@ function createShapeRun(node) {
1524
1524
  function marksToTextFormatting(marks, options) {
1525
1525
  const formatting = {};
1526
1526
  let directOverrideFormatting;
1527
+ let directFontProperties;
1527
1528
  let characterStyleRPr;
1528
1529
  let runFormattingOverrideMark;
1529
1530
  for (const mark of marks) switch (mark.type.name) {
@@ -1651,9 +1652,14 @@ function marksToTextFormatting(marks, options) {
1651
1652
  }
1652
1653
  if (runFormattingOverrideMark) {
1653
1654
  const overrideAttrs = expectRunFormattingOverrideMarkAttrs(runFormattingOverrideMark);
1655
+ directFontProperties = overrideAttrs.directFontProperties;
1654
1656
  applyRunFormattingOverrideAttrs(formatting, overrideAttrs);
1655
1657
  directOverrideFormatting = {};
1656
1658
  applyRunFormattingOverrideAttrs(directOverrideFormatting, overrideAttrs);
1659
+ for (const property of directFontProperties ?? []) {
1660
+ const value = formatting[property];
1661
+ if (value !== void 0) Reflect.set(directOverrideFormatting, property, value);
1662
+ }
1657
1663
  }
1658
1664
  if (characterStyleRPr) return subtractCharacterStyleFormatting({
1659
1665
  directOverrideFormatting,
@@ -1661,8 +1667,29 @@ function marksToTextFormatting(marks, options) {
1661
1667
  inheritedFormatting: options?.inheritedFormatting,
1662
1668
  styleRPr: characterStyleRPr
1663
1669
  });
1670
+ for (const property of [
1671
+ "fontFamily",
1672
+ "fontSize",
1673
+ "color"
1674
+ ]) {
1675
+ if (directFontProperties?.includes(property)) continue;
1676
+ const value = formatting[property];
1677
+ const inheritedValue = options?.inheritedFormatting?.[property];
1678
+ const matchesInherited = property === "fontFamily" ? sameFontFamily(formatting.fontFamily, options?.inheritedFormatting?.fontFamily) : JSON.stringify(value) === JSON.stringify(inheritedValue);
1679
+ if (inheritedValue !== void 0 && matchesInherited) Reflect.deleteProperty(formatting, property);
1680
+ }
1664
1681
  return formatting;
1665
1682
  }
1683
+ const sameFontFamily = (left, right) => [
1684
+ "ascii",
1685
+ "hAnsi",
1686
+ "eastAsia",
1687
+ "hint",
1688
+ "asciiTheme",
1689
+ "hAnsiTheme",
1690
+ "eastAsiaTheme",
1691
+ "csTheme"
1692
+ ].every((property) => left?.[property] === right?.[property]) && (left?.cs ?? left?.ascii) === (right?.cs ?? right?.ascii);
1666
1693
  /**
1667
1694
  * Negatable boolean run-property keys whose serializer emits an explicit
1668
1695
  * `w:val="0"` override when the value is `false` (see `serializeTextFormatting`
@@ -18,6 +18,7 @@ import { shadingToRunShadingAttrs } from "./runShadingMark.js";
18
18
  import { sdtAttrsFromProperties } from "./sdtAttrs.js";
19
19
  import { panic } from "better-result";
20
20
  //#region src/prosemirror/conversion/toProseDoc.ts
21
+ const DETACHED_WATERMARK_HOST = Symbol.for("stll.detachedWatermarkHost");
21
22
  /**
22
23
  * Build a `nextTextBoxGroupId()` generator salted with a random per-load
23
24
  * nonce, so minted text-box anchor ids (`<salt>:<group>:<index>`) are unique
@@ -139,7 +140,12 @@ function toProseDoc(document, options) {
139
140
  };
140
141
  nodes.push(...convertBodyBlocks(paragraphs));
141
142
  if (nodes.length === 0) nodes.push(schema.node("paragraph", {}, []));
142
- const pmDoc = stampNumberedRefFieldBaselines(schema.node("doc", null, nodes));
143
+ const finalSectionStart = document.package.document.sections?.at(-1)?.properties.sectionStart ?? null;
144
+ const adjustLineHeightInTable = document.package.settings?.adjustLineHeightInTable === true;
145
+ const pmDoc = stampNumberedRefFieldBaselines(schema.node("doc", {
146
+ _finalSectionStart: finalSectionStart,
147
+ _adjustLineHeightInTable: adjustLineHeightInTable
148
+ }, nodes));
143
149
  assertValidProseMirrorDocument(pmDoc, "Document conversion produced an invalid ProseMirror document");
144
150
  return pmDoc;
145
151
  }
@@ -210,6 +216,7 @@ function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex,
210
216
  const paragraphRunFormatting = resolveRunFormattingWithoutDefaults(paragraph.formatting?.runProperties, styleResolver);
211
217
  let inheritableParagraphRunFormatting;
212
218
  if (paragraphRunFormatting && !isTocParagraph) inheritableParagraphRunFormatting = stripParagraphMarkFormattingForBodyRuns(paragraphRunFormatting);
219
+ const ordinaryStyleFormatting = paragraph.formatting?.styleId === void 0 ? mergeTextFormatting(styleRunFormatting, extraRunFormatting) : mergeTextFormatting(extraRunFormatting, styleRunFormatting);
213
220
  const orderedToggleFormatting = cascadeStyleTextFormatting([
214
221
  {
215
222
  formatting: styleResolver?.getDocDefaults()?.rPr,
@@ -223,7 +230,7 @@ function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex,
223
230
  formatting: paragraphStyleRunFormatting,
224
231
  type: "style"
225
232
  }
226
- ], { ordinaryFormatting: mergeTextFormatting(styleRunFormatting, extraRunFormatting) });
233
+ ], { ordinaryFormatting: ordinaryStyleFormatting });
227
234
  let baseRunFormatting = orderedToggleFormatting.formatting;
228
235
  if (paragraphStyleFontFamily) baseRunFormatting = mergeTextFormatting(baseRunFormatting, { fontFamily: paragraphStyleFontFamily });
229
236
  const paragraphMarkPrecedesStyle = paragraph.formatting?.styleId !== void 0;
@@ -1000,10 +1007,10 @@ function convertTableRow(row, styleResolver, context, isHeaderRow, columnWidths,
1000
1007
  let cellConditionalStyle = conditionalStyles?.wholeTable;
1001
1008
  cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, effectiveRowBandStyle);
1002
1009
  cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, vertBandStyle);
1003
- if (cellIsFirstRow && (tableLook?.firstRow || rowCnf?.firstRow || cellCnf?.firstRow)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.firstRow);
1004
- if (cellIsLastRow && (tableLook?.lastRow || rowCnf?.lastRow || cellCnf?.lastRow)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.lastRow);
1005
1010
  if (cellIsFirstCol && (tableLook?.firstColumn || rowCnf?.firstColumn || cellCnf?.firstColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.firstCol);
1006
1011
  if (cellIsLastCol && (tableLook?.lastColumn || rowCnf?.lastColumn || cellCnf?.lastColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.lastCol);
1012
+ if (cellIsFirstRow && (tableLook?.firstRow || rowCnf?.firstRow || cellCnf?.firstRow)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.firstRow);
1013
+ if (cellIsLastRow && (tableLook?.lastRow || rowCnf?.lastRow || cellCnf?.lastRow)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.lastRow);
1007
1014
  if (cellIsFirstRow && cellIsFirstCol && (tableLook?.firstRow || rowCnf?.firstRow || cellCnf?.firstRow) && (tableLook?.firstColumn || rowCnf?.firstColumn || cellCnf?.firstColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.nwCell);
1008
1015
  if (cellIsFirstRow && cellIsLastCol && (tableLook?.firstRow || rowCnf?.firstRow || cellCnf?.firstRow) && (tableLook?.lastColumn || rowCnf?.lastColumn || cellCnf?.lastColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.neCell);
1009
1016
  if (cellIsLastRow && cellIsFirstCol && (tableLook?.lastRow || rowCnf?.lastRow || cellCnf?.lastRow) && (tableLook?.firstColumn || rowCnf?.firstColumn || cellCnf?.firstColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.swCell);
@@ -1265,6 +1272,7 @@ function buildRunMarks(runFormatting, inherited, styleResolver) {
1265
1272
  hasCharacterStyle: styleId !== void 0,
1266
1273
  paragraphMarkOverrides: inherited.paragraphMarkOverrides
1267
1274
  }) });
1275
+ addDirectFontProvenance(marks, runFormatting);
1268
1276
  if (styleId) {
1269
1277
  const styleRPr = characterStyleFormatting ? marksToTextFormatting(textFormattingToMarks(characterStyleFormatting)) : void 0;
1270
1278
  marks.push(schema.mark("characterStyle", {
@@ -1277,6 +1285,24 @@ function buildRunMarks(runFormatting, inherited, styleResolver) {
1277
1285
  mergedFormatting
1278
1286
  };
1279
1287
  }
1288
+ const addDirectFontProvenance = (marks, directFormatting) => {
1289
+ const directFontProperties = [];
1290
+ if (directFormatting?.fontFamily !== void 0) directFontProperties.push("fontFamily");
1291
+ if (directFormatting?.fontSize !== void 0) directFontProperties.push("fontSize");
1292
+ if (directFormatting?.color !== void 0) directFontProperties.push("color");
1293
+ if (!directFormatting || !Object.keys(directFormatting).some((property) => property !== "styleId")) return;
1294
+ const index = marks.findIndex(({ type }) => type.name === "runFormattingOverride");
1295
+ const existing = index >= 0 ? marks.at(index) : void 0;
1296
+ const override = schema.mark("runFormattingOverride", {
1297
+ ...existing?.attrs,
1298
+ ...directFontProperties.length > 0 && { directFontProperties }
1299
+ });
1300
+ if (index >= 0) {
1301
+ marks[index] = override;
1302
+ return;
1303
+ }
1304
+ marks.push(override);
1305
+ };
1280
1306
  const ORDINARY_STYLE_TOGGLE_KEYS = [
1281
1307
  "bold",
1282
1308
  "italic",
@@ -1658,12 +1684,12 @@ function convertShape(shape) {
1658
1684
  position
1659
1685
  });
1660
1686
  }
1661
- function convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId, context, extraRunFormatting, tableParagraphOverlay }) {
1687
+ function convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId, context, extraRunFormatting, preserveEmptyWrapper, tableParagraphOverlay }) {
1662
1688
  const { textBoxes, textBoxAnchors } = extractTextBoxesFromParagraph(block, textBoxGroupId);
1663
1689
  const pmParagraph = convertParagraph(block, styleResolver, context.nextHyperlinkInstanceIndex, context.pairedBookmarkIds, void 0, extraRunFormatting, tableParagraphOverlay, textBoxAnchors);
1664
1690
  const nodes = [];
1665
1691
  const isEmptyAfterExtraction = textBoxes.length > 0 && !hasContentBesidesTextBoxAnchors(pmParagraph);
1666
- const keepWrapperParagraph = isEmptyAfterExtraction && hasParagraphBoundaryPayload(block, pmParagraph);
1692
+ const keepWrapperParagraph = isEmptyAfterExtraction && (preserveEmptyWrapper === true || hasParagraphBoundaryPayload(block, pmParagraph));
1667
1693
  if (!isEmptyAfterExtraction || keepWrapperParagraph) nodes.push(pmParagraph);
1668
1694
  for (const { textBox, anchorId, trackedChange, inlineSdts } of textBoxes) nodes.push(convertTextBox(textBox, styleResolver, {
1669
1695
  placement: isEmptyAfterExtraction && !keepWrapperParagraph ? "standalone" : "inlineWithPrevious",
@@ -1929,11 +1955,23 @@ function headerFooterToProseDoc(content, options) {
1929
1955
  };
1930
1956
  const convertBlocks = (blocks) => {
1931
1957
  const out = [];
1932
- for (const block of blocks) if (block.type === "paragraph") out.push(...convertParagraphWithTextBoxes(block, styleResolver, {
1933
- textBoxGroupId: nextTextBoxGroupId(),
1934
- context: conversionContext
1935
- }));
1936
- else if (block.type === "table") out.push(convertTable(block, styleResolver, conversionContext));
1958
+ for (const block of blocks) if (block.type === "paragraph") {
1959
+ const isDetachedWatermarkHost = Reflect.get(block, DETACHED_WATERMARK_HOST) === true;
1960
+ const paragraphNodes = convertParagraphWithTextBoxes(block, styleResolver, {
1961
+ textBoxGroupId: nextTextBoxGroupId(),
1962
+ context: conversionContext,
1963
+ preserveEmptyWrapper: isDetachedWatermarkHost
1964
+ });
1965
+ if (isDetachedWatermarkHost) {
1966
+ const paragraphNodeIndex = paragraphNodes.findIndex(({ type }) => type.name === "paragraph");
1967
+ const paragraphNode = paragraphNodes[paragraphNodeIndex];
1968
+ if (paragraphNode) paragraphNodes[paragraphNodeIndex] = paragraphNode.type.create({
1969
+ ...paragraphNode.attrs,
1970
+ _detachedWatermarkHost: true
1971
+ }, paragraphNode.content, paragraphNode.marks);
1972
+ }
1973
+ out.push(...paragraphNodes);
1974
+ } else if (block.type === "table") out.push(convertTable(block, styleResolver, conversionContext));
1937
1975
  else out.push(convertBlockSdt(block, convertBlocks));
1938
1976
  return out;
1939
1977
  };
@@ -6,7 +6,13 @@ import { createNodeExtension } from "../create.js";
6
6
  const DocExtension = createNodeExtension({
7
7
  name: "doc",
8
8
  schemaNodeName: "doc",
9
- nodeSpec: { content: "(paragraph | horizontalRule | pageBreak | table | textBox | blockSdt)+" }
9
+ nodeSpec: {
10
+ attrs: {
11
+ _finalSectionStart: { default: null },
12
+ _adjustLineHeightInTable: { default: false }
13
+ },
14
+ content: "(paragraph | horizontalRule | pageBreak | table | textBox | blockSdt)+"
15
+ }
10
16
  });
11
17
  //#endregion
12
18
  export { DocExtension };