@stll/folio-core 0.4.0 → 0.5.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 (99) hide show
  1. package/dist/ai-edits/apply.d.ts +3 -9
  2. package/dist/ai-edits/apply.js +94 -14
  3. package/dist/ai-edits/blockRange.d.ts +33 -0
  4. package/dist/ai-edits/blockRange.js +58 -0
  5. package/dist/ai-edits/headless.d.ts +33 -4
  6. package/dist/ai-edits/headless.js +83 -15
  7. package/dist/ai-edits/index.d.ts +5 -3
  8. package/dist/ai-edits/index.js +3 -2
  9. package/dist/ai-edits/snapshot.d.ts +14 -2
  10. package/dist/ai-edits/snapshot.js +12 -1
  11. package/dist/ai-edits/types.d.ts +42 -3
  12. package/dist/compat/eigenpal.d.ts +3 -2
  13. package/dist/compat/eigenpal.js +6 -5
  14. package/dist/controller/layoutPipeline.d.ts +1 -0
  15. package/dist/controller/layoutPipeline.js +35 -137
  16. package/dist/controller/layoutScheduler.d.ts +2 -1
  17. package/dist/controller/layoutScheduler.js +6 -0
  18. package/dist/document-operations.d.ts +122 -0
  19. package/dist/document-operations.js +575 -0
  20. package/dist/docx/blockContentParser.js +17 -7
  21. package/dist/docx/encryption/agileDecryption.js +2 -1
  22. package/dist/docx/encryption/compoundFile.js +4 -2
  23. package/dist/docx/footnoteParser.d.ts +1 -17
  24. package/dist/docx/footnoteParser.js +1 -27
  25. package/dist/docx/groupDrawingParser.d.ts +8 -0
  26. package/dist/docx/groupDrawingParser.js +131 -0
  27. package/dist/docx/numberingParser.d.ts +3 -1
  28. package/dist/docx/numberingParser.js +8 -2
  29. package/dist/docx/paragraphParser.js +9 -3
  30. package/dist/docx/runParser.js +8 -1
  31. package/dist/docx/selectiveXmlPatch.js +4 -10
  32. package/dist/docx/serializer/numberingSerializer.js +3 -1
  33. package/dist/docx/settingsParser.d.ts +6 -3
  34. package/dist/docx/settingsParser.js +2 -0
  35. package/dist/docx/xmlParser.js +11 -4
  36. package/dist/index.d.ts +3 -2
  37. package/dist/index.js +6 -5
  38. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  39. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  40. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +5 -7
  41. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -7
  42. package/dist/layout-bridge/convert/toFlowBlocks.js +56 -19
  43. package/dist/layout-engine/index.d.ts +6 -5
  44. package/dist/layout-engine/index.js +89 -26
  45. package/dist/layout-engine/keep-together.d.ts +13 -8
  46. package/dist/layout-engine/keep-together.js +40 -25
  47. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  48. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  49. package/dist/layout-engine/measure/measureBlocks.js +28 -18
  50. package/dist/layout-engine/measure/measureParagraph.js +61 -15
  51. package/dist/layout-engine/paginator.d.ts +2 -1
  52. package/dist/layout-engine/paginator.js +9 -3
  53. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  54. package/dist/layout-engine/textBoxGroup.js +8 -0
  55. package/dist/layout-engine/types.d.ts +30 -9
  56. package/dist/layout-engine/types.js +20 -2
  57. package/dist/layout-painter/renderPage.d.ts +15 -1
  58. package/dist/layout-painter/renderPage.js +27 -11
  59. package/dist/layout-painter/renderParagraph.js +18 -8
  60. package/dist/layout-painter/renderTable.js +10 -7
  61. package/dist/managers/DocumentLoaderManager.js +12 -5
  62. package/dist/managers/TableSelectionManager.js +1 -1
  63. package/dist/model.js +1 -1
  64. package/dist/paged-layout/pageText.d.ts +14 -0
  65. package/dist/paged-layout/pageText.js +21 -0
  66. package/dist/prosemirror/attrs/index.js +2 -0
  67. package/dist/prosemirror/commands/comments.js +75 -5
  68. package/dist/prosemirror/commands/hyperlink.js +4 -2
  69. package/dist/prosemirror/commands/image.js +2 -1
  70. package/dist/prosemirror/commands/index.js +1 -1
  71. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  72. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  73. package/dist/prosemirror/conversion/fromProseDoc.js +14 -3
  74. package/dist/prosemirror/conversion/toProseDoc.js +29 -8
  75. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  76. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  77. package/dist/prosemirror/extensions/nodes/TableExtension.js +10 -11
  78. package/dist/prosemirror/findReplaceSelection.js +2 -1
  79. package/dist/prosemirror/index.js +1 -1
  80. package/dist/prosemirror/insertOperations.js +1 -1
  81. package/dist/prosemirror/schema/nodes.d.ts +10 -5
  82. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  83. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  84. package/dist/redline.d.ts +20 -0
  85. package/dist/redline.js +127 -0
  86. package/dist/server.d.ts +7 -3
  87. package/dist/server.js +6 -2
  88. package/dist/symbols.d.ts +35 -0
  89. package/dist/symbols.js +512 -0
  90. package/dist/utils/clipboard.js +3 -3
  91. package/dist/utils/fontLoader.js +3 -0
  92. package/dist/utils/fontResolver.js +48 -1
  93. package/dist/utils/zoom.d.ts +21 -0
  94. package/dist/utils/zoom.js +21 -0
  95. package/dist/version-comparison.d.ts +92 -0
  96. package/dist/version-comparison.js +436 -0
  97. package/package.json +3 -3
  98. package/dist/paged-layout/headerFooterMargins.d.ts +0 -75
  99. package/dist/paged-layout/headerFooterMargins.js +0 -122
@@ -22,8 +22,10 @@ const DEFAULT_FONT_FAMILY = "Calibri";
22
22
  const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
23
23
  const WIDTH_TOLERANCE = .5;
24
24
  const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
25
+ const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
25
26
  const JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO = .025;
26
27
  const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
28
+ const DEFAULT_LIST_HANGING_INDENT_PX = 24;
27
29
  const ALL_CAPS_RATIO_THRESHOLD = .8;
28
30
  /**
29
31
  * Find the longest prefix of `text` that fits within `maxWidth` pixels.
@@ -270,8 +272,16 @@ function hasFollowingTabOnLine(runs, tabIndex) {
270
272
  }
271
273
  return false;
272
274
  }
273
- function canClampTabToRightEdge(alignment, currentLineWidth) {
274
- if (alignment === "start" || alignment === "default") return currentLineWidth > WIDTH_TOLERANCE;
275
+ function hasPriorTabOnLine(runs, tabIndex) {
276
+ for (let i = tabIndex - 1; i >= 0; i--) {
277
+ const prior = runs[i];
278
+ if (!prior || isLineBreakRun(prior) || isImageRun(prior) && isBlockLayoutImageRun(prior)) break;
279
+ if (isTabRun(prior)) return true;
280
+ }
281
+ return false;
282
+ }
283
+ function canClampTabToRightEdge(alignment, currentLineWidth, hasPriorTab, followingWidth, availableWidth) {
284
+ if (alignment === "start" || alignment === "default") return currentLineWidth > WIDTH_TOLERANCE && (hasPriorTab || followingWidth <= availableWidth);
275
285
  return true;
276
286
  }
277
287
  /**
@@ -319,10 +329,18 @@ function uppercaseLetterRatio(text) {
319
329
  }
320
330
  return letters === 0 ? 0 : uppercase / letters;
321
331
  }
322
- function justifyShrinkToleranceRatio(block) {
332
+ function justifyShrinkToleranceRatio(block, isFirstLine, regularSpaceCount, nonBreakingSpaceCount) {
333
+ if (block.attrs?.listMarker !== void 0) {
334
+ if ((block.attrs.indent?.hanging ?? 0) <= DEFAULT_LIST_HANGING_INDENT_PX) return JUSTIFY_SHRINK_TOLERANCE_RATIO;
335
+ if (isFirstLine) return JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO;
336
+ const totalSpaces = regularSpaceCount + nonBreakingSpaceCount;
337
+ if (totalSpaces === 0) return JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO;
338
+ return Math.max(JUSTIFY_SHRINK_TOLERANCE_RATIO, JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO * (regularSpaceCount / totalSpaces));
339
+ }
323
340
  const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
324
341
  const hasTabRuns = block.runs.some(isTabRun);
325
- if (hasTabStops || hasTabRuns) return (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO;
342
+ if (!isFirstLine && hasTabRuns && !hasTabStops) return JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO;
343
+ if ((isFirstLine || hasTabStops) && (hasTabStops || hasTabRuns)) return (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO;
326
344
  if (uppercaseLetterRatio(block.runs.map((run) => isTextRun(run) ? run.text ?? "" : "").join("")) > ALL_CAPS_RATIO_THRESHOLD) return JUSTIFY_SHRINK_TOLERANCE_RATIO;
327
345
  return JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO;
328
346
  }
@@ -412,7 +430,6 @@ function measureParagraph(block, maxWidth, options) {
412
430
  const attrs = block.attrs;
413
431
  const spacing = attrs?.spacing;
414
432
  const isJustifiedParagraph = attrs?.alignment === "justify";
415
- const justifyToleranceRatio = justifyShrinkToleranceRatio(block);
416
433
  const floatingZones = options?.floatingZones;
417
434
  const paragraphYOffset = options?.paragraphYOffset ?? 0;
418
435
  const indent = attrs?.indent;
@@ -506,6 +523,9 @@ function measureParagraph(block, maxWidth, options) {
506
523
  toRun: 0,
507
524
  toChar: 0,
508
525
  width: 0,
526
+ trailingWhitespaceWidth: 0,
527
+ regularSpaceCount: 0,
528
+ nonBreakingSpaceCount: 0,
509
529
  maxFontSize: DEFAULT_FONT_SIZE,
510
530
  maxFontMetrics: null,
511
531
  maxImageHeightPx: 0,
@@ -552,7 +572,7 @@ function measureParagraph(block, maxWidth, options) {
552
572
  fromChar: currentLine.fromChar,
553
573
  toRun: currentLine.toRun,
554
574
  toChar: currentLine.toChar,
555
- width: currentLine.width,
575
+ width: Math.max(0, currentLine.width - currentLine.trailingWhitespaceWidth),
556
576
  ...finalTypography
557
577
  };
558
578
  if (currentLine.leftOffset > 0) line.leftOffset = currentLine.leftOffset;
@@ -572,17 +592,21 @@ function measureParagraph(block, maxWidth, options) {
572
592
  const estimatedLineHeight = ptToPx(DEFAULT_FONT_SIZE) * DEFAULT_LINE_HEIGHT_MULTIPLIER;
573
593
  skipObstructingFloats(estimatedLineHeight, bodyContentWidth);
574
594
  const floatingMargins = getFloatingMargins(cumulativeHeight, estimatedLineHeight, floatingZones, paragraphYOffset);
595
+ const adjustedWidth = Math.max(1, getFloatingAvailableWidth(floatingMargins, bodyContentWidth));
575
596
  currentLine = {
576
597
  fromRun: runIndex,
577
598
  fromChar: charIndex,
578
599
  toRun: runIndex,
579
600
  toChar: charIndex,
580
601
  width: 0,
602
+ trailingWhitespaceWidth: 0,
603
+ regularSpaceCount: 0,
604
+ nonBreakingSpaceCount: 0,
581
605
  maxFontSize: DEFAULT_FONT_SIZE,
582
606
  maxFontMetrics: null,
583
607
  maxImageHeightPx: 0,
584
608
  maxMathHeightPx: 0,
585
- availableWidth: Math.max(1, getFloatingAvailableWidth(floatingMargins, bodyContentWidth)),
609
+ availableWidth: adjustedWidth,
586
610
  leftOffset: floatingMargins.leftMargin,
587
611
  rightOffset: floatingMargins.rightMargin,
588
612
  ...floatingMargins.segments?.length ? { segmentZones: floatingMargins.segments } : {}
@@ -620,6 +644,7 @@ function measureParagraph(block, maxWidth, options) {
620
644
  const contentX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + lineX;
621
645
  const tabResult = calculateTabWidth(contentX, {
622
646
  ...attrs?.tabs !== void 0 ? { explicitStops: attrs.tabs } : {},
647
+ ...attrs?.defaultTabStopTwips !== void 0 ? { defaultTabInterval: attrs.defaultTabStopTwips } : {},
623
648
  leftIndent: pixelsToTwips(indentLeft)
624
649
  }, {
625
650
  followingWidth,
@@ -627,12 +652,13 @@ function measureParagraph(block, maxWidth, options) {
627
652
  });
628
653
  let tabWidth = tabResult.width;
629
654
  const lineRightEdgeX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + currentLine.availableWidth + currentLine.leftOffset;
630
- if (!hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
655
+ if (!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);
631
656
  if (currentLine.width + tabWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
632
657
  startNewLine(runIndex, 0);
633
658
  updateMaxFont(style);
634
659
  }
635
660
  currentLine.width += tabWidth;
661
+ currentLine.trailingWhitespaceWidth = 0;
636
662
  currentLine.toRun = runIndex;
637
663
  currentLine.toChar = 1;
638
664
  continue;
@@ -662,6 +688,7 @@ function measureParagraph(block, maxWidth, options) {
662
688
  const imageFootprintPx = imageHeight + (run.distTop ?? 0) + (run.distBottom ?? 0);
663
689
  if (imageFootprintPx > currentLine.maxImageHeightPx) currentLine.maxImageHeightPx = imageFootprintPx;
664
690
  currentLine.width += imageWidth;
691
+ currentLine.trailingWhitespaceWidth = 0;
665
692
  currentLine.toRun = runIndex;
666
693
  currentLine.toChar = 1;
667
694
  continue;
@@ -676,6 +703,7 @@ function measureParagraph(block, maxWidth, options) {
676
703
  updateMaxFont(style);
677
704
  }
678
705
  currentLine.width += fieldWidth;
706
+ currentLine.trailingWhitespaceWidth = 0;
679
707
  currentLine.toRun = runIndex;
680
708
  currentLine.toChar = 1;
681
709
  continue;
@@ -696,6 +724,7 @@ function measureParagraph(block, maxWidth, options) {
696
724
  }
697
725
  if (mathFootprintPx > currentLine.maxMathHeightPx) currentLine.maxMathHeightPx = mathFootprintPx;
698
726
  currentLine.width += mathWidth;
727
+ currentLine.trailingWhitespaceWidth = 0;
699
728
  currentLine.toRun = runIndex;
700
729
  currentLine.toChar = 1;
701
730
  continue;
@@ -720,13 +749,17 @@ function measureParagraph(block, maxWidth, options) {
720
749
  break;
721
750
  }
722
751
  const word = text.slice(charIndex, nextBreak);
723
- const wordWidth = measureTextWidth(word, style);
724
- const widthTolerance = isJustifiedParagraph ? Math.max(WIDTH_TOLERANCE, currentLine.availableWidth * justifyToleranceRatio) : WIDTH_TOLERANCE;
752
+ const measuredWord = trimTrailingSpacesAndTabs(word);
753
+ const wordWidth = measureTextWidth(measuredWord, style);
754
+ const fullWordWidth = measureTextWidth(word, style);
755
+ const regularSpaces = measuredWord.split(" ").length - 1;
756
+ const nonBreakingSpaces = measuredWord.split("\xA0").length - 1;
757
+ const widthTolerance = isJustifiedParagraph ? Math.max(WIDTH_TOLERANCE, currentLine.availableWidth * justifyShrinkToleranceRatio(block, lines.length === 0, currentLine.regularSpaceCount + regularSpaces, currentLine.nonBreakingSpaceCount + nonBreakingSpaces)) : WIDTH_TOLERANCE;
725
758
  if (wordWidth > currentLine.availableWidth + widthTolerance) {
726
759
  let chunkStart = 0;
727
- while (chunkStart < word.length) {
760
+ while (chunkStart < measuredWord.length) {
728
761
  const spaceLeft = currentLine.availableWidth - currentLine.width + WIDTH_TOLERANCE;
729
- let bestEnd = findMaxFittingLength(word.slice(chunkStart), style, spaceLeft);
762
+ let bestEnd = findMaxFittingLength(measuredWord.slice(chunkStart), style, spaceLeft);
730
763
  if (bestEnd === 0) {
731
764
  if (currentLine.width > 0) {
732
765
  startNewLine(runIndex, charIndex + chunkStart);
@@ -736,16 +769,26 @@ function measureParagraph(block, maxWidth, options) {
736
769
  bestEnd = 1;
737
770
  }
738
771
  const chunkEnd = chunkStart + bestEnd;
739
- const chunkWidth = measureTextWidth(word.slice(chunkStart, chunkEnd), style);
772
+ const chunk = measuredWord.slice(chunkStart, chunkEnd);
773
+ const chunkWidth = measureTextWidth(chunk, style);
740
774
  currentLine.width += chunkWidth;
775
+ currentLine.trailingWhitespaceWidth = chunkWidth - measureTextWidth(trimTrailingSpacesAndTabs(chunk), style);
776
+ currentLine.regularSpaceCount += chunk.split(" ").length - 1;
777
+ currentLine.nonBreakingSpaceCount += chunk.split("\xA0").length - 1;
741
778
  currentLine.toRun = runIndex;
742
779
  currentLine.toChar = charIndex + chunkEnd;
743
780
  chunkStart = chunkEnd;
744
- if (chunkStart < word.length) {
781
+ if (chunkStart < measuredWord.length) {
745
782
  startNewLine(runIndex, charIndex + chunkStart);
746
783
  updateMaxFont(lineHeightStyle);
747
784
  }
748
785
  }
786
+ const trailingWhitespaceWidth = fullWordWidth - wordWidth;
787
+ currentLine.width += trailingWhitespaceWidth;
788
+ currentLine.trailingWhitespaceWidth = trailingWhitespaceWidth;
789
+ currentLine.regularSpaceCount += word.split(" ").length - 1;
790
+ currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1;
791
+ currentLine.toChar = nextBreak;
749
792
  charIndex = nextBreak;
750
793
  continue;
751
794
  }
@@ -755,7 +798,10 @@ function measureParagraph(block, maxWidth, options) {
755
798
  startNewLine(runIndex, charIndex);
756
799
  updateMaxFont(lineHeightStyle);
757
800
  }
758
- currentLine.width += wordWidth;
801
+ currentLine.width += fullWordWidth;
802
+ currentLine.trailingWhitespaceWidth = fullWordWidth - wordWidth;
803
+ currentLine.regularSpaceCount += word.split(" ").length - 1;
804
+ currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1;
759
805
  currentLine.toRun = runIndex;
760
806
  currentLine.toChar = nextBreak;
761
807
  charIndex = nextBreak;
@@ -27,7 +27,8 @@ type PaginatorOptions = {
27
27
  w: number;
28
28
  h: number;
29
29
  }; /** Page margins. */
30
- margins: PageMargins;
30
+ margins: PageMargins; /** Swap left/right margins on even physical pages. */
31
+ mirrorMargins?: boolean;
31
32
  /**
32
33
  * Margins applied only to the very first page (page 1) of the paginator.
33
34
  * Used when a `<w:titlePg/>`-enabled section needs different margins for
@@ -46,7 +46,7 @@ function createPaginator(options) {
46
46
  }
47
47
  function currentPageUsesActiveLayout(state) {
48
48
  if (pendingPageSize || pendingMargins) return false;
49
- return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, margins);
49
+ return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, getPageMargins(state.page.number));
50
50
  }
51
51
  if (getContentHeight() <= 0) panic("Paginator: page size and margins yield no content area");
52
52
  let columnWidth = calculateColumnWidth(pageSize.w, margins.left, margins.right, columns);
@@ -62,14 +62,20 @@ function createPaginator(options) {
62
62
  recalculateColumnWidth();
63
63
  }
64
64
  function getPageMargins(pageNumber) {
65
- return pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...margins };
65
+ const pageMargins = pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...margins };
66
+ if (options.mirrorMargins === true && pageNumber % 2 === 0) return {
67
+ ...pageMargins,
68
+ left: pageMargins.right,
69
+ right: pageMargins.left
70
+ };
71
+ return pageMargins;
66
72
  }
67
73
  let columnRegionTop = margins.top;
68
74
  /**
69
75
  * Get X position for a given column index.
70
76
  */
71
77
  function getColumnX(columnIndex) {
72
- return margins.left + columnIndex * (columnWidth + columns.gap);
78
+ return (states.at(-1)?.page.margins.left ?? getPageMargins(1).left) + columnIndex * (columnWidth + columns.gap);
73
79
  }
74
80
  /**
75
81
  * Create a new page and add it to the list.
@@ -0,0 +1,7 @@
1
+ import { TextBoxBlock } from "./types.js";
2
+
3
+ //#region src/layout-engine/textBoxGroup.d.ts
4
+ declare const setTextBoxGroupId: (textBox: TextBoxBlock, groupId: string) => void;
5
+ declare const getTextBoxGroupId: (textBox: TextBoxBlock) => string | undefined;
6
+ //#endregion
7
+ export { getTextBoxGroupId, setTextBoxGroupId };
@@ -0,0 +1,8 @@
1
+ //#region src/layout-engine/textBoxGroup.ts
2
+ const textBoxGroupIds = /* @__PURE__ */ new WeakMap();
3
+ const setTextBoxGroupId = (textBox, groupId) => {
4
+ textBoxGroupIds.set(textBox, groupId);
5
+ };
6
+ const getTextBoxGroupId = (textBox) => textBoxGroupIds.get(textBox);
7
+ //#endregion
8
+ export { getTextBoxGroupId, setTextBoxGroupId };
@@ -3,7 +3,7 @@ import { ImagePosition, ImageWrap, SdtProperties, SdtType, TableWidthType } from
3
3
 
4
4
  //#region src/layout-engine/types.d.ts
5
5
  declare namespace types_d_exports {
6
- export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun };
6
+ export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned };
7
7
  }
8
8
  /**
9
9
  * Unique identifier for a block in the document.
@@ -304,7 +304,8 @@ type ParagraphAttrs = {
304
304
  keepNext?: boolean;
305
305
  keepLines?: boolean;
306
306
  widowControl?: boolean;
307
- pageBreakBefore?: boolean;
307
+ pageBreakBefore?: boolean; /** Word's cached pagination hint (`w:lastRenderedPageBreak`). */
308
+ renderedPageBreakBefore?: boolean;
308
309
  styleId?: string;
309
310
  contextualSpacing?: boolean;
310
311
  /**
@@ -459,7 +460,14 @@ type TableBlock = {
459
460
  rows: TableRow[];
460
461
  columnWidths?: number[]; /** Table width value (twips for dxa, 50ths of percent for pct). */
461
462
  width?: number; /** Table width type ('auto', 'pct', 'dxa', 'nil'). */
462
- widthType?: TableWidthType; /** Table horizontal alignment */
463
+ widthType?: TableWidthType;
464
+ /**
465
+ * Table layout algorithm (`w:tblLayout`). "fixed" pins columns to their grid
466
+ * widths; "autofit" (the default when absent) lets Word size columns to
467
+ * content. Consulted alongside `widthType` to decide whether `w:noWrap` cells
468
+ * can be honored (see `tableColumnsArePinned`).
469
+ */
470
+ layout?: "fixed" | "autofit"; /** Table horizontal alignment */
463
471
  justification?: "left" | "center" | "right"; /** Table indent from left margin (in pixels, from w:tblInd) */
464
472
  indent?: number; /** Right-to-left column order (w:bidiVisual): logical column 0 paints on the right. */
465
473
  bidi?: boolean; /** Floating table properties (pixel values). */
@@ -903,7 +911,8 @@ type LayoutOptions = {
903
911
  headerContentHeights?: HeaderFooterContentHeights; /** Footer content heights by variant. */
904
912
  footerContentHeights?: HeaderFooterContentHeights; /** Whether section has different first page header/footer. */
905
913
  titlePage?: boolean; /** Whether section has different even/odd headers/footers. */
906
- evenAndOddHeaders?: boolean; /** Per-page footnote reserved heights (pageNumber height in pixels). */
914
+ evenAndOddHeaders?: boolean; /** Swap left/right margins on even physical pages. */
915
+ mirrorMargins?: boolean; /** Per-page footnote reserved heights (pageNumber → height in pixels). */
907
916
  footnoteReservedHeights?: Map<number, number>;
908
917
  /**
909
918
  * Footnote content heights keyed by internal footnote id (the OOXML
@@ -970,10 +979,8 @@ type HeaderFooterContent = {
970
979
  visualTop?: number; /** Bottom-most visual extent relative to the nominal flow origin. */
971
980
  visualBottom?: number;
972
981
  /**
973
- * Flow-only bounds used by `computeHeaderFooterMarginExtender` to push
974
- * body margins clear of HF overflow. Excludes `behindDoc` images that
975
- * the renderer paints behind body content; including them would reserve
976
- * a full-page letterhead as body push-down.
982
+ * Flow-only bounds for consumers that need to distinguish normal
983
+ * header/footer flow from floating or `behindDoc` artwork.
977
984
  */
978
985
  marginPushTop?: number;
979
986
  marginPushBottom?: number;
@@ -1031,5 +1038,19 @@ declare function floatingTextBoxWrapsText(block: TextBoxFlowAttrs): boolean;
1031
1038
  * Ported from eigenpal #694.
1032
1039
  */
1033
1040
  declare function floatingTextBoxReservesBand(block: TextBoxFlowAttrs): boolean;
1041
+ /**
1042
+ * A table's columns are pinned when the layout engine cannot widen a column to
1043
+ * fit content that overflows it: either the layout is explicitly fixed
1044
+ * (`w:tblLayout w:type="fixed"`) or an explicit total width (`w:tblW` `dxa`/
1045
+ * `pct`) is fully consumed by the grid.
1046
+ *
1047
+ * In OOXML, `w:noWrap` (§17.4.30) asks Word not to soft-wrap a cell, but Word
1048
+ * can only honor it by widening the column. When the columns are pinned it
1049
+ * cannot, so overflowing content wraps despite `w:noWrap`. An auto-width table
1050
+ * (`w:tblW` `auto`/`nil`/absent with autofit layout) lets Word widen the column
1051
+ * instead, so there `w:noWrap` genuinely keeps content on one line. Measurement
1052
+ * and the painter both consult this so their line count and `white-space` agree.
1053
+ */
1054
+ declare function tableColumnsArePinned(table: TableBlock): boolean;
1034
1055
  //#endregion
1035
- export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, types_d_exports };
1056
+ export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_d_exports };
@@ -11,7 +11,8 @@ var types_exports = /* @__PURE__ */ __exportAll({
11
11
  floatingTextBoxWrapsText: () => floatingTextBoxWrapsText,
12
12
  isFloatingImageRun: () => isFloatingImageRun,
13
13
  isFloatingTextBoxBlock: () => isFloatingTextBoxBlock,
14
- isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun
14
+ isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun,
15
+ tableColumnsArePinned: () => tableColumnsArePinned
15
16
  });
16
17
  /** Default internal margins for text boxes (OOXML defaults in pixels) */
17
18
  const DEFAULT_TEXTBOX_MARGINS = {
@@ -96,5 +97,22 @@ function floatingTextBoxWrapsText(block) {
96
97
  function floatingTextBoxReservesBand(block) {
97
98
  return isFloatingTextBoxBlock(block) && block.wrapType === "topAndBottom";
98
99
  }
100
+ /**
101
+ * A table's columns are pinned when the layout engine cannot widen a column to
102
+ * fit content that overflows it: either the layout is explicitly fixed
103
+ * (`w:tblLayout w:type="fixed"`) or an explicit total width (`w:tblW` `dxa`/
104
+ * `pct`) is fully consumed by the grid.
105
+ *
106
+ * In OOXML, `w:noWrap` (§17.4.30) asks Word not to soft-wrap a cell, but Word
107
+ * can only honor it by widening the column. When the columns are pinned it
108
+ * cannot, so overflowing content wraps despite `w:noWrap`. An auto-width table
109
+ * (`w:tblW` `auto`/`nil`/absent with autofit layout) lets Word widen the column
110
+ * instead, so there `w:noWrap` genuinely keeps content on one line. Measurement
111
+ * and the painter both consult this so their line count and `white-space` agree.
112
+ */
113
+ function tableColumnsArePinned(table) {
114
+ if (table.layout === "fixed") return true;
115
+ return table.widthType === "dxa" || table.widthType === "pct";
116
+ }
99
117
  //#endregion
100
- export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, types_exports };
118
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_exports };
@@ -42,6 +42,20 @@ type PageFloatingImage = {
42
42
  */
43
43
  behindDoc: boolean;
44
44
  };
45
+ type FloatingTableBandOptions = {
46
+ contentX: number;
47
+ tableWidth: number;
48
+ contentWidth: number;
49
+ distLeft: number;
50
+ distRight: number;
51
+ };
52
+ declare const floatingTableReservesBand: ({
53
+ contentX,
54
+ tableWidth,
55
+ contentWidth,
56
+ distLeft,
57
+ distRight
58
+ }: FloatingTableBandOptions) => boolean;
45
59
  /**
46
60
  * CSS class names for page elements
47
61
  */
@@ -276,4 +290,4 @@ declare function findPageShellForPmPos(container: HTMLElement, pmPos: number): {
276
290
  isExact: boolean;
277
291
  } | null;
278
292
  //#endregion
279
- export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
293
+ export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
@@ -19,6 +19,11 @@ import { panic } from "better-result";
19
19
  * Renders a single page from Layout data to DOM elements.
20
20
  * Each page contains positioned fragments within a content area.
21
21
  */
22
+ const floatingTableReservesBand = ({ contentX, tableWidth, contentWidth, distLeft, distRight }) => {
23
+ const leftClearance = Math.max(0, contentX - distLeft);
24
+ const rightClearance = Math.max(0, contentWidth - contentX - tableWidth - distRight);
25
+ return Math.max(leftClearance, rightClearance) < 24;
26
+ };
22
27
  /**
23
28
  * CSS class names for page elements
24
29
  */
@@ -874,7 +879,8 @@ function renderPage(page, context, options = {}) {
874
879
  ...img.wrapType !== void 0 ? { wrapType: img.wrapType } : {}
875
880
  });
876
881
  }
877
- if (options.blockLookup) for (const fragment of page.fragments) {
882
+ if (options.blockLookup) for (let fragmentIndex = 0; fragmentIndex < page.fragments.length; fragmentIndex++) {
883
+ const fragment = page.fragments[fragmentIndex];
878
884
  if (fragment.kind !== "table") continue;
879
885
  const blockData = options.blockLookup.get(String(fragment.blockId));
880
886
  if (blockData?.block.kind !== "table") continue;
@@ -886,9 +892,8 @@ function renderPage(page, context, options = {}) {
886
892
  const distBottom = floating.bottomFromText ?? 0;
887
893
  const distLeft = floating.leftFromText ?? 12;
888
894
  const distRight = floating.rightFromText ?? 12;
889
- const side = contentX < contentWidth / 2 ? "left" : "right";
890
- floatingRects.push({
891
- side,
895
+ const rect = {
896
+ side: contentX < contentWidth / 2 ? "left" : "right",
892
897
  x: contentX,
893
898
  y: contentY,
894
899
  width: fragment.width,
@@ -897,7 +902,19 @@ function renderPage(page, context, options = {}) {
897
902
  distBottom,
898
903
  distLeft,
899
904
  distRight
905
+ };
906
+ if (floatingTableReservesBand({
907
+ contentX,
908
+ tableWidth: fragment.width,
909
+ contentWidth,
910
+ distLeft,
911
+ distRight
912
+ })) rect.wrapType = "topAndBottom";
913
+ if (floating.vertAnchor === "text") scopedFloatingRects.push({
914
+ startsAtFragmentIndex: fragmentIndex,
915
+ rect
900
916
  });
917
+ else floatingRects.push(rect);
901
918
  }
902
919
  if (options.blockLookup) for (let fragmentIndex = 0; fragmentIndex < page.fragments.length; fragmentIndex++) {
903
920
  const fragment = page.fragments[fragmentIndex];
@@ -1557,22 +1574,21 @@ function renderPages(pages, container, options = {}) {
1557
1574
  const pageShells = [];
1558
1575
  for (let i = 0; i < pages.length; i++) {
1559
1576
  const page = pages[i];
1577
+ let pageEl;
1560
1578
  if (!useVirtualization) {
1561
1579
  const { context, pageOptions } = buildPageRenderArgs(page, totalPages, options);
1562
- const pageEl = renderPage(page, context, pageOptions);
1563
- container.insertBefore(pageEl, overlayBefore);
1564
- pageShells.push(pageEl);
1580
+ pageEl = renderPage(page, context, pageOptions);
1565
1581
  } else {
1566
1582
  const doc = options.document ?? document;
1567
- const pageEl = doc.createElement("div");
1583
+ pageEl = doc.createElement("div");
1568
1584
  pageEl.className = options.pageClassName ?? PAGE_CLASS_NAMES.page;
1569
1585
  pageEl.dataset["pageNumber"] = String(page.number);
1570
1586
  pageEl.dataset["pageIndex"] = String(i);
1571
1587
  applyPageStyles(pageEl, page.size.w, page.size.h, options);
1572
1588
  syncPageBorderOverlay(pageEl, page, options, doc);
1573
- container.insertBefore(pageEl, overlayBefore);
1574
- pageShells.push(pageEl);
1575
1589
  }
1590
+ container.insertBefore(pageEl, overlayBefore);
1591
+ pageShells.push(pageEl);
1576
1592
  }
1577
1593
  if (!useVirtualization) {
1578
1594
  emitPainterPainted(container);
@@ -1820,4 +1836,4 @@ function depopulatePageShell(shell, pageDataMap) {
1820
1836
  }
1821
1837
  }
1822
1838
  //#endregion
1823
- export { PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
1839
+ export { PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
@@ -330,8 +330,8 @@ function renderTabRun(run, doc, width, leader) {
330
330
  } else span.textContent = "\xA0";
331
331
  return span;
332
332
  }
333
- function canClampTabToRightEdge(alignment, hasPriorRenderedContent) {
334
- if (alignment === "start" || alignment === "default") return hasPriorRenderedContent;
333
+ function canClampTabToRightEdge(alignment, hasPriorRenderedContent, hasPriorTab, isLastLine) {
334
+ if (alignment === "start" || alignment === "default") return hasPriorRenderedContent && (hasPriorTab || isLastLine);
335
335
  return true;
336
336
  }
337
337
  function applyTabUnderline(element, run) {
@@ -905,24 +905,32 @@ function renderLine(block, line, alignment, doc, options) {
905
905
  lineEl.append(emptySpan);
906
906
  return lineEl;
907
907
  }
908
+ const hasTabRuns = runsForLine.some(isTabRun);
908
909
  if (alignment === "justify" && options) {
909
910
  if (!options.isLastLine || options.paragraphEndsWithLineBreak) {
910
- const overfullPx = line.width - options.availableWidth;
911
+ const firstLineIndentPx = options.isFirstLine ? options.firstLineIndentPx ?? 0 : 0;
912
+ const firstLineHangingPx = Math.max(0, -firstLineIndentPx);
913
+ const justifyCapacityPx = options.availableWidth + firstLineHangingPx;
914
+ const overfullPx = line.width - justifyCapacityPx;
911
915
  const shrinkableSpaces = countShrinkableSpaces(runsForLine);
912
916
  if (overfullPx > RIGHT_EDGE_EPSILON_PX && shrinkableSpaces > 0) {
913
917
  lineEl.style.textAlign = "left";
914
918
  lineEl.style.textAlignLast = "auto";
915
919
  lineEl.style.wordSpacing = `${-overfullPx / shrinkableSpaces}px`;
920
+ } else if (hasTabRuns && shrinkableSpaces > 0 && -overfullPx > RIGHT_EDGE_EPSILON_PX) {
921
+ lineEl.style.textAlign = "left";
922
+ lineEl.style.textAlignLast = "auto";
923
+ lineEl.style.wordSpacing = `${-overfullPx / shrinkableSpaces}px`;
916
924
  } else {
917
925
  lineEl.style.textAlign = "justify";
918
926
  lineEl.style.textAlignLast = "justify";
919
927
  }
920
- lineEl.style.width = `${options.availableWidth}px`;
928
+ const listFirstLineOffset = options.isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden ? options.firstLineIndentPx ?? 0 : 0;
929
+ lineEl.style.width = `${options.availableWidth - listFirstLineOffset}px`;
921
930
  }
922
931
  }
923
932
  lineEl.style.whiteSpace = "pre";
924
933
  lineEl.style.overflow = "visible";
925
- const hasTabRuns = runsForLine.some(isTabRun);
926
934
  let tabContext;
927
935
  const measureText = createTextMeasurer(doc);
928
936
  if (hasTabRuns) {
@@ -930,6 +938,7 @@ function renderLine(block, line, alignment, doc, options) {
930
938
  const leftIndentTwips = options?.leftIndentPx ? Math.round(options.leftIndentPx * 15) : 0;
931
939
  tabContext = {
932
940
  ...explicitStops !== void 0 ? { explicitStops } : {},
941
+ ...block.attrs?.defaultTabStopTwips !== void 0 ? { defaultTabInterval: block.attrs.defaultTabStopTwips } : {},
933
942
  leftIndent: leftIndentTwips
934
943
  };
935
944
  }
@@ -992,7 +1001,7 @@ function renderLine(block, line, alignment, doc, options) {
992
1001
  break;
993
1002
  }
994
1003
  let tabWidth = tabResult.width;
995
- if (lineRightEdgeX !== void 0 && canClampTabToRightEdge(tabResult.alignment, i > 0) && currentX + tabWidth + followingWidthForCheck > lineRightEdgeX) tabWidth = Math.max(1, lineRightEdgeX - currentX - followingWidthForCheck);
1004
+ if (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);
996
1005
  const tabEl = renderTabRun(run, doc, tabWidth, tabResult.leader);
997
1006
  lineEl.append(tabEl);
998
1007
  currentX += tabWidth;
@@ -1240,7 +1249,7 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1240
1249
  } else if (indentLeft > 0) lineEl.style.paddingLeft = `${indentLeft}px`;
1241
1250
  else if (hasFirstLine && !isFlexLine) lineEl.style.textIndent = `${indent.firstLine ?? 0}px`;
1242
1251
  } else if (indentLeft > 0) lineEl.style.paddingLeft = `${indentLeft}px`;
1243
- else if (hasHanging) lineEl.style.paddingLeft = `${indent.hanging ?? 0}px`;
1252
+ else if (hasHanging && indentLeft === 0) lineEl.style.paddingLeft = `${indent.hanging ?? 0}px`;
1244
1253
  if (indentRight > 0) lineEl.style.paddingRight = `${indentRight}px`;
1245
1254
  if (isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden) {
1246
1255
  const hanging = indent?.hanging ?? 0;
@@ -1259,7 +1268,8 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1259
1268
  const markerFontFamily = block.attrs.listMarkerFontFamily ?? firstTextRun?.fontFamily ?? block.attrs.defaultFontFamily;
1260
1269
  const markerFontSize = block.attrs.listMarkerFontSize ?? firstTextRun?.fontSize ?? block.attrs.defaultFontSize;
1261
1270
  const marker = renderListMarker(block.attrs.listMarker, getListMarkerInlineWidth(block), doc, markerFontFamily, markerFontSize, block.attrs.listMarkerRevision, block.attrs.listMarkerSecondSlotOffsetTwips);
1262
- if (markerStart < 0 && indentLeft > 0) marker.style.marginLeft = `${markerStart}px`;
1271
+ const markerMarginLeft = markerStart - Math.min(indentLeft, 0);
1272
+ if (markerMarginLeft < 0 && indentLeft !== 0) marker.style.marginLeft = `${markerMarginLeft}px`;
1263
1273
  lineEl.prepend(marker);
1264
1274
  }
1265
1275
  fragmentEl.append(lineEl);