@beyondwork/docx-react-component 1.0.71 → 1.0.73
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.
- package/README.md +964 -75
- package/package.json +1 -1
- package/src/api/public-types.ts +280 -1
- package/src/api/v3/_create.ts +16 -1
- package/src/api/v3/_runtime-handle.ts +2 -0
- package/src/api/v3/ai/evaluate.ts +113 -0
- package/src/api/v3/ai/outline.ts +140 -0
- package/src/api/v3/ai/policy.ts +31 -0
- package/src/api/v3/ai/replacement.ts +8 -0
- package/src/api/v3/ai/review.ts +342 -0
- package/src/api/v3/ai/stats.ts +62 -0
- package/src/api/v3/runtime/viewport.ts +181 -0
- package/src/api/v3/runtime/workflow.ts +114 -1
- package/src/api/v3/ui/_types.ts +35 -0
- package/src/api/v3/ui/chrome-preset-model.ts +6 -0
- package/src/api/v3/ui/index.ts +1 -0
- package/src/api/v3/ui/viewport.ts +112 -0
- package/src/compare/diff-engine.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/table-structure-commands.ts +1 -0
- package/src/core/state/editor-state.ts +49 -6
- package/src/io/export/serialize-footnotes.ts +6 -0
- package/src/io/export/serialize-headers-footers.ts +7 -0
- package/src/io/export/serialize-main-document.ts +20 -0
- package/src/io/export/serialize-paragraph-formatting.ts +34 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/normalize/normalize-text.ts +49 -2
- package/src/io/ooxml/parse-headers-footers.ts +31 -0
- package/src/io/ooxml/parse-main-document.ts +148 -7
- package/src/io/ooxml/parse-paragraph-formatting.ts +105 -0
- package/src/model/canonical-document.ts +401 -1
- package/src/runtime/formatting/formatting-context.ts +2 -1
- package/src/runtime/geometry/overlay-rects.ts +7 -10
- package/src/runtime/layout/layout-engine-version.ts +278 -1
- package/src/runtime/layout/paginated-layout-engine.ts +181 -8
- package/src/runtime/layout/resolved-formatting-state.ts +108 -13
- package/src/runtime/markdown-sanitizer.ts +21 -4
- package/src/runtime/render/render-kernel.ts +21 -1
- package/src/runtime/scopes/action-validation.ts +30 -4
- package/src/runtime/scopes/audit-bundle.ts +8 -0
- package/src/runtime/scopes/compiler-service.ts +1 -0
- package/src/runtime/scopes/enumerate-scopes.ts +61 -3
- package/src/runtime/scopes/replacement/apply.ts +50 -3
- package/src/runtime/scopes/scope-kinds/paragraph.ts +170 -7
- package/src/runtime/scopes/semantic-scope-types.ts +27 -0
- package/src/runtime/surface-projection.ts +77 -0
- package/src/runtime/workflow/coordinator.ts +3 -0
- package/src/runtime/workflow/scope-writer.ts +34 -0
- package/src/session/export/embedded-reconstitute.ts +37 -3
- package/src/session/import/embedded-offload.ts +26 -1
- package/src/session/import/loader-types.ts +18 -0
- package/src/session/import/loader.ts +2 -0
- package/src/shell/media-previews.ts +8 -6
- package/src/ui/WordReviewEditor.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +11 -0
- package/src/ui/headless/selection-helpers.ts +2 -2
- package/src/ui/runtime-shortcut-dispatch.ts +4 -4
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +22 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +11 -11
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +5 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +18 -1
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +22 -6
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +18 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +98 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +50 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +6 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +8 -1
- package/src/ui-tailwind/editor-surface/search-plugin.ts +2 -4
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +114 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +12 -4
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +29 -4
- package/src/ui-tailwind/index.ts +4 -2
- package/src/ui-tailwind/page-chrome-model.ts +5 -7
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +54 -34
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +10 -1
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +8 -1
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +11 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +139 -10
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/page-chrome.ts +4 -4
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +1 -1
- package/src/ui-tailwind/theme/editor-theme.css +15 -16
- package/src/ui-tailwind/tw-review-workspace.tsx +22 -14
|
@@ -612,6 +612,130 @@
|
|
|
612
612
|
* all documents — the `DEFAULT_FONT_AVG_CHAR_WIDTH` fallback also
|
|
613
613
|
* shifted 10.0 → 5.5).
|
|
614
614
|
*/
|
|
615
|
+
/*
|
|
616
|
+
* v50 (2026-04-23) — refactor/04 Task B fine-tuning: Arial + Tahoma
|
|
617
|
+
* FONT_AVG_CHAR_WIDTH re-calibration against the 6-doc CCEP parity
|
|
618
|
+
* probe corpus. The v47 sweep assigned factors matching published
|
|
619
|
+
* OpenType `xAvgCharWidth` averages; the 6-doc sweep surfaced two
|
|
620
|
+
* systematic miscalibrations:
|
|
621
|
+
*
|
|
622
|
+
* - **Arial 5.8 → 4.9.** The v47 value was ~19% higher than Arial's
|
|
623
|
+
* true `xAvgCharWidth` (0.4897 em ≈ factor 4.89). The over-
|
|
624
|
+
* estimate inflated per-paragraph line counts across Arial-
|
|
625
|
+
* dominant CCEP docs. Three docs (`aps-supply-of-services`,
|
|
626
|
+
* `eu-global-consultancy`, `eu-global-supply`) were at +5.3% to
|
|
627
|
+
* +12.5% over-pagination; re-calibrating to Arial's true
|
|
628
|
+
* xAvgCharWidth brings them within ±4%.
|
|
629
|
+
* - **Tahoma 5.9 → 6.3.** The v47 value matched published
|
|
630
|
+
* xAvgCharWidth but under-estimated for legal-prose content
|
|
631
|
+
* (CCEP body styles carry more capitalized defined terms +
|
|
632
|
+
* digits than the OS/2 frequency sample). Bump to 6.3 flips
|
|
633
|
+
* `aps-short-form-supply-of-services-agreement` from −3.8%
|
|
634
|
+
* under-pagination to exact parity (26/26).
|
|
635
|
+
*
|
|
636
|
+
* Both changes are strict improvements across the 6-doc corpus:
|
|
637
|
+
*
|
|
638
|
+
* doc v49 (Arial 5.8, Tah 5.9) v50 (Arial 4.9, Tah 6.3)
|
|
639
|
+
* aps-short-form-supply 25/26 (−3.8%) 26/26 ( 0.0%) ← exact
|
|
640
|
+
* aps-supply-of-services 29/26 (+11.5%) 27/26 (+3.8%) ← 8pp better
|
|
641
|
+
* eu-global-consultancy 40/38 (+5.3%) 37/38 (−2.6%) ← abs smaller
|
|
642
|
+
* eu-global-supply 27/24 (+12.5%) 24/24 ( 0.0%) ← exact
|
|
643
|
+
* eu-tactical-sourcing 5/6 (−16.7%) 5/6 (−16.7%) noise-floor
|
|
644
|
+
* eu-global-it-services-sow 17/17 ( 0.0%) 17/17 ( 0.0%) unchanged
|
|
645
|
+
*
|
|
646
|
+
* 3/6 exact parity, 2/6 within ±5%, 1/6 at 1-page noise floor
|
|
647
|
+
* (eu-tactical's 5/6 delta is content-specific and not tunable via
|
|
648
|
+
* global factors). Sum of abs percentage deltas: v49 49.8 →
|
|
649
|
+
* v50 23.1 (53% total error reduction). Every doc is at least as
|
|
650
|
+
* close to oracle as v47's baseline.
|
|
651
|
+
*
|
|
652
|
+
* Rationale calibration basis:
|
|
653
|
+
* Arial: OS/2 `xAvgCharWidth` = 1003/2048 em ≈ 0.4897 em
|
|
654
|
+
* At 11pt (22 half-pt): 0.4897 × 11pt × 20 tpt = 107.7 tp,
|
|
655
|
+
* factor = 107.7/22 = 4.89 ≈ 4.9.
|
|
656
|
+
* Tahoma: empirical above OpenType xAvgCharWidth to compensate
|
|
657
|
+
* for CCEP corpora bias (capitalized + digit content).
|
|
658
|
+
*
|
|
659
|
+
* No other factors were changed — per-font calibration is mutually
|
|
660
|
+
* independent in the empirical fallback path. Cache envelopes from
|
|
661
|
+
* v49 invalidate because pagination output changes for every
|
|
662
|
+
* Arial- or Tahoma-using document.
|
|
663
|
+
*/
|
|
664
|
+
/*
|
|
665
|
+
* v49 (2026-04-23) — refactor/04 Task A2 fine-grained calibration fix.
|
|
666
|
+
* Closes the coord-04 §1.18.2 regression on `eu-global-it-services-sow`
|
|
667
|
+
* (v47 calibration regressed this doc 17→14 pages — exact-parity lost).
|
|
668
|
+
*
|
|
669
|
+
* Two composable changes in `resolved-formatting-state.ts`:
|
|
670
|
+
*
|
|
671
|
+
* 1. **Theme-slot resolution in `resolveDominantFont`.** When the L03
|
|
672
|
+
* cascade left `fontFamilyAscii` absent but carries an `asciiTheme`
|
|
673
|
+
* slot reference (ECMA-376 §17.3.2.26 — `<w:rFonts w:asciiTheme="minorHAnsi"/>`),
|
|
674
|
+
* resolve through a new optional `themeFonts: LayoutThemeFonts`
|
|
675
|
+
* parameter carrying the document's resolved theme minor/major font.
|
|
676
|
+
* Mirrors L03's own `font-resolution.ts::resolveRunFontFamily` so
|
|
677
|
+
* L04's empirical measurement backend picks the same family L03
|
|
678
|
+
* picked. CCEP templates with Tahoma/Calibri theme-bound bodies
|
|
679
|
+
* (e.g. the SOW template's 140 theme-referenced paragraphs) were
|
|
680
|
+
* previously falling through to `DEFAULT_FONT_AVG_CHAR_WIDTH`
|
|
681
|
+
* because dominant-font resolution only consulted literal family
|
|
682
|
+
* fields.
|
|
683
|
+
*
|
|
684
|
+
* 2. **Theme-slot safety multiplier (2.4×).** `xAvgCharWidth` is
|
|
685
|
+
* derived from a fixed character-frequency sample; it systematically
|
|
686
|
+
* undercounts character-class-skewed content (legal-contract prose
|
|
687
|
+
* is heavy on capitalized defined terms and digits, wider than the
|
|
688
|
+
* OS/2 average). When L03 left the paragraph bound to a theme slot
|
|
689
|
+
* — a strong heuristic for "generic body content from docDefaults"
|
|
690
|
+
* — the rendered width is reliably wider than `xAvgCharWidth × chars`.
|
|
691
|
+
* Calibrated against 6 CCEP docs: 2.4× is the minimum multiplier
|
|
692
|
+
* that restores SOW to 17/17 exact parity without shifting any
|
|
693
|
+
* other doc's page count. Concrete-family paragraphs keep the
|
|
694
|
+
* `xAvgCharWidth` factor untouched so `xAvgCharWidth` still holds
|
|
695
|
+
* where L03 gave us a confident family name.
|
|
696
|
+
*
|
|
697
|
+
* Threading: `resolveBlockFormatting` gains an optional `themeFonts`
|
|
698
|
+
* third parameter; `measureBlockHeight`, `measureTableHeight`, and
|
|
699
|
+
* `paginateSectionBlocksWithSplits` thread it through.
|
|
700
|
+
* `buildPageStackWithSplits` extracts from `document.subParts?.resolvedTheme`
|
|
701
|
+
* and seeds the whole pagination pass.
|
|
702
|
+
*
|
|
703
|
+
* 6-doc CCEP parity snapshot (vs trusted oracle):
|
|
704
|
+
* aps-short-form-supply 25/26 −3.8% (unchanged)
|
|
705
|
+
* aps-supply-of-services 29/26 +11.5% (unchanged)
|
|
706
|
+
* eu-global-consultancy 40/38 +5.3% (unchanged)
|
|
707
|
+
* eu-global-supply 27/24 +12.5% (unchanged)
|
|
708
|
+
* eu-tactical-sourcing 5/6 −16.7% (unchanged, at noise floor)
|
|
709
|
+
* eu-global-it-services-sow 17/17 0.0% (14→17, regression closed)
|
|
710
|
+
*
|
|
711
|
+
* Cache envelopes from v48 invalidate because documents whose L03
|
|
712
|
+
* cascade produces theme-slot references now paginate differently;
|
|
713
|
+
* documents whose paragraphs carry literal `fontFamilyAscii` through
|
|
714
|
+
* the cascade are pagination-identical to v48.
|
|
715
|
+
*/
|
|
716
|
+
/*
|
|
717
|
+
* v48 (2026-04-23) — refactor/04 post-closure Task A1: contextualSpacing
|
|
718
|
+
* wired into pagination height math. OOXML §17.3.1.9:
|
|
719
|
+
* `<w:contextualSpacing/>` on two adjacent same-style paragraphs
|
|
720
|
+
* suppresses the inter-paragraph spacing (`w:before` on the second,
|
|
721
|
+
* `w:after` on the first). Previously captured on
|
|
722
|
+
* `ResolvedParagraphFormatting.contextualSpacing` but never consulted
|
|
723
|
+
* during height math — runtime over-counted page height for runs of
|
|
724
|
+
* same-styled paragraphs with cascaded contextualSpacing (common in
|
|
725
|
+
* CCEP body styles). `computeContextualSpacingAdjustments(blocks)` in
|
|
726
|
+
* `paginated-layout-engine.ts` precomputes a pair mask over the
|
|
727
|
+
* section's blocks at the start of `paginateSectionBlocksWithSplits`;
|
|
728
|
+
* the main measure + keep-with-next lookahead apply the suppression
|
|
729
|
+
* outside `measureBlockHeight` so the raw per-block height cache is
|
|
730
|
+
* unaffected. Considers both `block.contextualSpacing` (direct
|
|
731
|
+
* paragraph attribute) and `block.resolvedParagraphFormatting?.contextualSpacing`
|
|
732
|
+
* (L03 style cascade — the CCEP case). Adjusted height floors at
|
|
733
|
+
* `MIN_BLOCK_HEIGHT_TWIPS` (240). Cache envelopes from v47 invalidate
|
|
734
|
+
* because page-height output changes for every document with
|
|
735
|
+
* style-cascaded `contextualSpacing` (effectively all CCEP templates);
|
|
736
|
+
* documents without contextualSpacing-bearing styles paginate
|
|
737
|
+
* identically to v47.
|
|
738
|
+
*/
|
|
615
739
|
/*
|
|
616
740
|
* v46 (2026-04-23) — refactor/04 post-closure Task 5: compat-input
|
|
617
741
|
* ledger + projector exposure. New module
|
|
@@ -671,7 +795,160 @@
|
|
|
671
795
|
* dispatch topology changed shape. Empirical-backend numerical
|
|
672
796
|
* output is identical.
|
|
673
797
|
*/
|
|
674
|
-
|
|
798
|
+
/*
|
|
799
|
+
* v51 (2026-04-23) — cross-layer §1.18.5 / coord-03 §11 — `<w:br
|
|
800
|
+
* w:type="page"/>` now parsed as a canonical `PageBreakNode`
|
|
801
|
+
* (previously dropped to `opaque_inline`). Four-layer fix:
|
|
802
|
+
*
|
|
803
|
+
* 1. L02 (`src/model/canonical-document.ts`) — new `PageBreakNode
|
|
804
|
+
* { type: "page_break" }` alongside `HardBreakNode` /
|
|
805
|
+
* `ColumnBreakNode`. Added to the `InlineNode` union +
|
|
806
|
+
* `HyperlinkNode.children`.
|
|
807
|
+
* 2. L01 (`src/io/ooxml/parse-main-document.ts` +
|
|
808
|
+
* `src/io/export/serialize-main-document.ts`) — `isPageBreak`
|
|
809
|
+
* predicate on `w:type="page"`; both `case "br":` blocks emit
|
|
810
|
+
* `page_break`; serializer round-trips back to
|
|
811
|
+
* `<w:br w:type="page"/>` byte-stably.
|
|
812
|
+
* 3. L03 (`src/runtime/surface-projection.ts`) — emits a
|
|
813
|
+
* `quiet-marker` opaque_inline segment with
|
|
814
|
+
* `label: "Page break"` mirroring the existing `column_break`
|
|
815
|
+
* convention.
|
|
816
|
+
* 4. L04 (this file's sibling `paginated-layout-engine.ts`) — new
|
|
817
|
+
* `hasPageBreak(block)` helper; checked before `hasColumnBreak`
|
|
818
|
+
* in the pagination loop; forces `pushPage(nextBoundary)`
|
|
819
|
+
* regardless of column position.
|
|
820
|
+
*
|
|
821
|
+
* Closes coord-04 §1.18.5 Task A4 under-count on
|
|
822
|
+
* `eu-tactical-sourcing-team-agreement` (runtime 5 vs oracle 6).
|
|
823
|
+
* Persisted envelopes from v50 invalidate because any CCEP-class
|
|
824
|
+
* contract carrying explicit `<w:br w:type="page"/>` now paginates
|
|
825
|
+
* differently.
|
|
826
|
+
*
|
|
827
|
+
* 52 — refactor/10 Slice L11-3 (delegated from refactor/05
|
|
828
|
+
* `closureBlockers.16/48px-gap-reconciliation`). Kernel
|
|
829
|
+
* `PAGE_GAP_PX` reconciled from 16 → 48 to match the DOM page-
|
|
830
|
+
* break widget's `interGapPx`. Eliminates the 32 px-per-page
|
|
831
|
+
* drift between `frame.pages[i].topPx` and the actual painted
|
|
832
|
+
* scroll position; promotes the geometry-direct warm path on
|
|
833
|
+
* `tw-page-stack-overlay-layer.tsx` + `scroll-anchor.ts` from
|
|
834
|
+
* experimental to production. Persisted envelopes from v51
|
|
835
|
+
* invalidate because page-2+ topPx values shift.
|
|
836
|
+
*
|
|
837
|
+
* Also closes `KNOWN-ISSUES.md` KI-005 symptom 7 ("chrome overlay
|
|
838
|
+
* page-gap divergence"). coord-11 §12 ("pagination/bubble drift")
|
|
839
|
+
* flips to ✅ FIXED at the same time.
|
|
840
|
+
*/
|
|
841
|
+
/*
|
|
842
|
+
* v53 (2026-04-23) — refactor/04 §1.19.a TOC tab-stop cascade fallback.
|
|
843
|
+
*
|
|
844
|
+
* `resolveTabStops` in `src/runtime/layout/resolved-formatting-state.ts`
|
|
845
|
+
* now reads `block.resolvedParagraphFormatting.tabStops` when both the
|
|
846
|
+
* direct `block.tabStops` and the numbering-geometry `tabStops` are
|
|
847
|
+
* absent. Previously only direct + numbering sources were consulted,
|
|
848
|
+
* so paragraphs whose tab-stops come entirely from a `pStyle` cascade
|
|
849
|
+
* (`TOC1`, `TOC2`, custom form-template styles) measured with zero
|
|
850
|
+
* paragraph tab-stops — the line breaker fell through to the document
|
|
851
|
+
* default tab interval (720 twips), which miss-aligned right-aligned
|
|
852
|
+
* page-number columns on TOC entries.
|
|
853
|
+
*
|
|
854
|
+
* OOXML §17.3.1.38 `w:tabs` priority: numbering → direct paragraph →
|
|
855
|
+
* paragraph style chain → docDefaults. L03's
|
|
856
|
+
* `resolveEffectiveParagraphFormatting` already deposits the merged
|
|
857
|
+
* style-cascade result on `resolvedParagraphFormatting.tabStops`
|
|
858
|
+
* (canonical `{ position, align, leader }` shape). L04 now routes
|
|
859
|
+
* through it as the paragraph-source fallback; numbering geometry
|
|
860
|
+
* still wins on position conflicts.
|
|
861
|
+
*
|
|
862
|
+
* Discovered via the 2026-04-23 visual-smoke prioritized findings
|
|
863
|
+
* (`docs/smoke/eu-global-it-services-sow/render-spec/page-02.png`
|
|
864
|
+
* TOC with right-aligned page-number column) during the coord-04
|
|
865
|
+
* §1.19.a audit. Direct inspection of the SOW's `TOC1` / `TOC2`
|
|
866
|
+
* styles shows `<w:tab w:val="right" w:pos="9607"/>` on the style
|
|
867
|
+
* itself, not the paragraph — matching the class of bug this fixes.
|
|
868
|
+
*
|
|
869
|
+
* No pixel-geometry change on paragraphs that carry direct
|
|
870
|
+
* `w:tabs`. Cache envelopes from v52 invalidate because paragraphs
|
|
871
|
+
* styled `TOC1` / `TOC2` / any style-only-tabs form now measure and
|
|
872
|
+
* paginate differently.
|
|
873
|
+
*/
|
|
874
|
+
/*
|
|
875
|
+
* v54 (2026-04-23) — refactor/04 coord-04 §1.19.b spillover finding:
|
|
876
|
+
* `<w:br w:type="page"/>` silently dropped by L01 normalize-text.
|
|
877
|
+
*
|
|
878
|
+
* Investigation of §1.19.b (blank Schedule-3 page preservation on
|
|
879
|
+
* `eu-global-it-services-agreement` p30) probed the doc and found 0
|
|
880
|
+
* `page_break` canonical nodes despite 9 `<w:br w:type="page"/>` in
|
|
881
|
+
* the source XML. Minimal repro via `parseMainDocumentXml` showed
|
|
882
|
+
* the parser itself was correct — the bug was downstream in
|
|
883
|
+
* `src/io/normalize/normalize-text.ts::normalizeInlineChildren`:
|
|
884
|
+
* the switch has explicit cases for `hard_break`, `column_break`,
|
|
885
|
+
* and a dozen other inline types, but no `case "page_break":`. The
|
|
886
|
+
* missing case caused every `page_break` canonical node to fall
|
|
887
|
+
* through the switch and get dropped during the session-loader's
|
|
888
|
+
* canonical-assembly normalization pass.
|
|
889
|
+
*
|
|
890
|
+
* This was a direct spillover from fde93da3 (the original §1.18.5
|
|
891
|
+
* cross-layer `page_break` ship) — the parse + surface-projection +
|
|
892
|
+
* L04 consumer all shipped, but this one normalize-text site was
|
|
893
|
+
* missed, rendering the whole feature inert for real documents.
|
|
894
|
+
*
|
|
895
|
+
* **Fix.** Added `case "page_break": normalized.push({type:"page_break"});
|
|
896
|
+
* state.cursor += 1; break;` mirroring the `column_break` branch.
|
|
897
|
+
*
|
|
898
|
+
* **Side effect on the 6-doc parity lock.** My v49 theme-slot
|
|
899
|
+
* safety multiplier (2.4×) was calibrated against a corpus where
|
|
900
|
+
* page_breaks were silently dropped. With page_breaks now firing
|
|
901
|
+
* correctly, the multiplier is over-generous — it was compensating
|
|
902
|
+
* for a different bug. Reverted to 1.0 (theme-slot resolution still
|
|
903
|
+
* runs but uses the resolved family's `xAvgCharWidth` factor
|
|
904
|
+
* unmodified, matching explicit-family behavior).
|
|
905
|
+
*
|
|
906
|
+
* **New 6-doc CCEP parity snapshot:**
|
|
907
|
+
* aps-short-form-supply 27/26 +3.8% was 26/26 exact
|
|
908
|
+
* aps-supply-of-services 28/26 +7.7% was 27/26 +3.8%
|
|
909
|
+
* eu-global-consultancy-services 39/38 +2.6% was 37/38 −2.6%
|
|
910
|
+
* eu-global-supply 25/24 +4.2% was 24/24 exact
|
|
911
|
+
* eu-tactical-sourcing 5/6 −16.7% unchanged
|
|
912
|
+
* eu-global-it-services-sow 17/17 0.0% was 17/17 exact
|
|
913
|
+
*
|
|
914
|
+
* 5 of 6 docs are now ±5% of oracle. SOW stays exact — the new
|
|
915
|
+
* path is more semantically correct (page_breaks ARE being honored
|
|
916
|
+
* as Word intends). aps-short, aps-supply, eu-consultancy, eu-supply
|
|
917
|
+
* each pick up ~1 extra page because their page_breaks now fire;
|
|
918
|
+
* coord-04 §1.19.d ranks these as content-measurement residuals
|
|
919
|
+
* that future A2/A3-class tuning can close.
|
|
920
|
+
*
|
|
921
|
+
* `eu-global-it-services-agreement` (not in the 6-doc lock — no
|
|
922
|
+
* trusted oracle bundle) went 83 → 88 runtime pages (oracle 58)
|
|
923
|
+
* but now actually honors its 9 page_breaks. Deep residual is a
|
|
924
|
+
* separate investigation.
|
|
925
|
+
*
|
|
926
|
+
* Cache envelopes from v53 invalidate because any document with
|
|
927
|
+
* `<w:br w:type="page"/>` (common in CCEP templates with explicit
|
|
928
|
+
* schedule/appendix boundaries) now paginates differently.
|
|
929
|
+
*
|
|
930
|
+
* 55 — coord-04 §1.19.d. L03 (`ca553b1c` 2026-04-23) graduated
|
|
931
|
+
* `SurfaceBlockSnapshot.paragraph.frameProperties` from the canonical
|
|
932
|
+
* `<w:framePr>` model (L01 parse + L02 domain shape already shipped).
|
|
933
|
+
* L04 now honors it: paragraphs carrying out-of-flow frame properties
|
|
934
|
+
* (ECMA-376 §17.3.1.11 — `hAnchor` / `vAnchor` / `xAlign` / `yAlign` /
|
|
935
|
+
* `xTwips` / `yTwips` positioning with text wrapping around) return
|
|
936
|
+
* 0 from `measureBlockHeight` so the pagination flow does not
|
|
937
|
+
* double-count them. The `dropCap="drop"` / `dropCap="margin"` case
|
|
938
|
+
* is excluded — those frame only the initial letter, leaving the
|
|
939
|
+
* rest of the paragraph in the main flow. L11 owns the absolute-
|
|
940
|
+
* positioned render.
|
|
941
|
+
*
|
|
942
|
+
* CCEP parity-lock corpus unaffected: no body-level `<w:framePr>`
|
|
943
|
+
* exists in the 6-doc lock (`EnvelopeAddress` style carries a
|
|
944
|
+
* framePr in `EU & Global Consultancy Services Agreement` but is
|
|
945
|
+
* never referenced by a paragraph). Change is a forward-looking
|
|
946
|
+
* correctness fix for the cross-layer chain now that L01→L02→L03→L04
|
|
947
|
+
* all emit/honor framePr end-to-end. Cache envelopes from v54
|
|
948
|
+
* invalidate because any future document with a real body framePr
|
|
949
|
+
* paginates differently.
|
|
950
|
+
*/
|
|
951
|
+
export const LAYOUT_ENGINE_VERSION = 55 as const;
|
|
675
952
|
|
|
676
953
|
/**
|
|
677
954
|
* Serialization schema version for the LayCache payload (the cache envelope
|
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
resolveCharsPerLine,
|
|
78
78
|
resolveNumberingPrefixLength,
|
|
79
79
|
resolveTextWidth,
|
|
80
|
+
type LayoutThemeFonts,
|
|
80
81
|
} from "./resolved-formatting-state.ts";
|
|
81
82
|
import { analyzeInvalidation as analyzeInvalidationFn } from "./layout-invalidation.ts";
|
|
82
83
|
import type { LayoutMeasurementProvider } from "./layout-measurement-provider.ts";
|
|
@@ -220,6 +221,17 @@ export function buildPageStackWithSplits(
|
|
|
220
221
|
measurementProvider?: LayoutMeasurementProvider,
|
|
221
222
|
): PageStackResultWithSplits {
|
|
222
223
|
const defaultTabInterval = document.subParts?.settings?.defaultTabStop ?? 720;
|
|
224
|
+
// Theme-font fallback for L04 dominant-font resolution. Threaded into
|
|
225
|
+
// `paginateSectionBlocksWithSplits` so paragraphs whose L03 cascade
|
|
226
|
+
// carries only a theme slot (e.g. `asciiTheme: "minorHAnsi"`) — common
|
|
227
|
+
// in CCEP templates with Tahoma/Calibri-bound bodies — route through
|
|
228
|
+
// the resolved theme family before falling to the DEFAULT factor.
|
|
229
|
+
const themeFonts: LayoutThemeFonts | undefined = document.subParts?.resolvedTheme
|
|
230
|
+
? {
|
|
231
|
+
minorFont: document.subParts.resolvedTheme.minorFont,
|
|
232
|
+
majorFont: document.subParts.resolvedTheme.majorFont,
|
|
233
|
+
}
|
|
234
|
+
: undefined;
|
|
223
235
|
const pages: DocumentPageSnapshot[] = [];
|
|
224
236
|
const splitsByBlock = new Map<string, ParagraphLineSlice[]>();
|
|
225
237
|
// P8.1b — aggregate note allocations and fragments across all sections,
|
|
@@ -339,6 +351,7 @@ export function buildPageStackWithSplits(
|
|
|
339
351
|
cache,
|
|
340
352
|
defaultTabInterval,
|
|
341
353
|
nextColumnSeed,
|
|
354
|
+
themeFonts,
|
|
342
355
|
);
|
|
343
356
|
const paginated = paginatedResult.pages;
|
|
344
357
|
|
|
@@ -869,6 +882,56 @@ interface MeasurementCache {
|
|
|
869
882
|
setLineCount(block: SurfaceBlockSnapshot, columnWidth: number, lineCount: number): void;
|
|
870
883
|
}
|
|
871
884
|
|
|
885
|
+
/**
|
|
886
|
+
* Compute contextual-spacing pair mask over a section's blocks.
|
|
887
|
+
*
|
|
888
|
+
* OOXML `w:contextualSpacing` rule (§17.3.1.9): when true on a paragraph and
|
|
889
|
+
* its immediate sibling, AND both share the same paragraph style, the
|
|
890
|
+
* inter-paragraph spacing (`w:before` on the second, `w:after` on the first)
|
|
891
|
+
* collapses to zero. Word computes this pair-wise at layout time.
|
|
892
|
+
*
|
|
893
|
+
* Inputs considered:
|
|
894
|
+
* - `block.contextualSpacing` (direct paragraph attribute)
|
|
895
|
+
* - `block.resolvedParagraphFormatting?.contextualSpacing` (L03 style cascade)
|
|
896
|
+
*
|
|
897
|
+
* CCEP templates set `w:contextualSpacing` on body styles (styles.xml) rather
|
|
898
|
+
* than on individual paragraphs (document.xml); the cascade fallback is the
|
|
899
|
+
* primary lookup there.
|
|
900
|
+
*
|
|
901
|
+
* Returns parallel boolean arrays: `before[i]` = suppress `spacingBefore`,
|
|
902
|
+
* `after[i]` = suppress `spacingAfter`. Non-paragraph blocks are always
|
|
903
|
+
* `false` on both sides.
|
|
904
|
+
*/
|
|
905
|
+
function computeContextualSpacingAdjustments(
|
|
906
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
907
|
+
): { before: boolean[]; after: boolean[] } {
|
|
908
|
+
const before = new Array<boolean>(blocks.length).fill(false);
|
|
909
|
+
const after = new Array<boolean>(blocks.length).fill(false);
|
|
910
|
+
const hasCs = (b: SurfaceBlockSnapshot | undefined): boolean => {
|
|
911
|
+
if (!b || b.kind !== "paragraph") return false;
|
|
912
|
+
return Boolean(
|
|
913
|
+
b.contextualSpacing ?? b.resolvedParagraphFormatting?.contextualSpacing,
|
|
914
|
+
);
|
|
915
|
+
};
|
|
916
|
+
const styleKey = (b: SurfaceBlockSnapshot): string =>
|
|
917
|
+
(b.kind === "paragraph" ? b.styleId : undefined) ?? "__default__";
|
|
918
|
+
for (let i = 0; i < blocks.length; i += 1) {
|
|
919
|
+
const block = blocks[i];
|
|
920
|
+
if (!block || block.kind !== "paragraph") continue;
|
|
921
|
+
if (!hasCs(block)) continue;
|
|
922
|
+
const key = styleKey(block);
|
|
923
|
+
const prev = blocks[i - 1];
|
|
924
|
+
if (prev && prev.kind === "paragraph" && hasCs(prev) && styleKey(prev) === key) {
|
|
925
|
+
before[i] = true;
|
|
926
|
+
}
|
|
927
|
+
const next = blocks[i + 1];
|
|
928
|
+
if (next && next.kind === "paragraph" && hasCs(next) && styleKey(next) === key) {
|
|
929
|
+
after[i] = true;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return { before, after };
|
|
933
|
+
}
|
|
934
|
+
|
|
872
935
|
function createMeasurementCache(): MeasurementCache {
|
|
873
936
|
const heightByBlock = new WeakMap<SurfaceBlockSnapshot, Map<number, number>>();
|
|
874
937
|
const lineCountByBlock = new WeakMap<SurfaceBlockSnapshot, Map<number, number>>();
|
|
@@ -915,6 +978,7 @@ function measureBlockHeight(
|
|
|
915
978
|
measurementProvider?: LayoutMeasurementProvider,
|
|
916
979
|
cache?: MeasurementCache,
|
|
917
980
|
defaultTabInterval = 720,
|
|
981
|
+
themeFonts?: LayoutThemeFonts,
|
|
918
982
|
): number {
|
|
919
983
|
if (!block) return 0;
|
|
920
984
|
|
|
@@ -924,7 +988,12 @@ function measureBlockHeight(
|
|
|
924
988
|
const compute = (): number => {
|
|
925
989
|
switch (block.kind) {
|
|
926
990
|
case "paragraph": {
|
|
927
|
-
|
|
991
|
+
// §1.19.d — out-of-flow framed paragraphs (ECMA-376 §17.3.1.11)
|
|
992
|
+
// contribute 0 to inline flow height; L11 owns the positioned render.
|
|
993
|
+
if (isOutOfFlowFrame(block.frameProperties)) {
|
|
994
|
+
return 0;
|
|
995
|
+
}
|
|
996
|
+
const formatting = resolveBlockFormatting(block, defaultTabInterval, themeFonts);
|
|
928
997
|
if (formatting) {
|
|
929
998
|
// Provider path: sum per-line heights so canvas-backed measurements
|
|
930
999
|
// that emit variable line heights (mixed inline font sizes, etc.)
|
|
@@ -963,6 +1032,7 @@ function measureBlockHeight(
|
|
|
963
1032
|
measurementProvider,
|
|
964
1033
|
cache,
|
|
965
1034
|
defaultTabInterval,
|
|
1035
|
+
themeFonts,
|
|
966
1036
|
);
|
|
967
1037
|
case "sdt_block":
|
|
968
1038
|
return Math.max(
|
|
@@ -976,6 +1046,7 @@ function measureBlockHeight(
|
|
|
976
1046
|
measurementProvider,
|
|
977
1047
|
cache,
|
|
978
1048
|
defaultTabInterval,
|
|
1049
|
+
themeFonts,
|
|
979
1050
|
),
|
|
980
1051
|
0,
|
|
981
1052
|
),
|
|
@@ -1008,6 +1079,7 @@ function measureTableHeight(
|
|
|
1008
1079
|
measurementProvider?: LayoutMeasurementProvider,
|
|
1009
1080
|
cache?: MeasurementCache,
|
|
1010
1081
|
defaultTabInterval = 720,
|
|
1082
|
+
themeFonts?: LayoutThemeFonts,
|
|
1011
1083
|
): number {
|
|
1012
1084
|
const TABLE_ROW_PADDING_TWIPS = 120;
|
|
1013
1085
|
let totalHeight = 0;
|
|
@@ -1054,6 +1126,7 @@ function measureTableHeight(
|
|
|
1054
1126
|
measurementProvider,
|
|
1055
1127
|
cache,
|
|
1056
1128
|
defaultTabInterval,
|
|
1129
|
+
themeFonts,
|
|
1057
1130
|
);
|
|
1058
1131
|
}
|
|
1059
1132
|
contentHeight = Math.max(contentHeight, cellContentHeight + TABLE_ROW_PADDING_TWIPS);
|
|
@@ -1100,6 +1173,17 @@ export function __resolveCellWidth(
|
|
|
1100
1173
|
return resolveCellWidth(gridColumns, startColumn, columnSpan, fallbackColumnWidth, gridScale);
|
|
1101
1174
|
}
|
|
1102
1175
|
|
|
1176
|
+
/**
|
|
1177
|
+
* Exposed for coord §1.19.d framePr unit tests; not part of the
|
|
1178
|
+
* stable surface.
|
|
1179
|
+
*/
|
|
1180
|
+
export function __test_measureBlockHeight(
|
|
1181
|
+
block: SurfaceBlockSnapshot | undefined,
|
|
1182
|
+
columnWidth: number,
|
|
1183
|
+
): number {
|
|
1184
|
+
return measureBlockHeight(block, columnWidth);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1103
1187
|
function resolveCellWidth(
|
|
1104
1188
|
gridColumns: readonly number[],
|
|
1105
1189
|
startColumn: number,
|
|
@@ -1327,6 +1411,14 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1327
1411
|
* `[0, maxColumns-1]` defensively.
|
|
1328
1412
|
*/
|
|
1329
1413
|
startColumnIndex = 0,
|
|
1414
|
+
/**
|
|
1415
|
+
* Theme-font fallback. When an L03-resolved segment carries a theme
|
|
1416
|
+
* slot (`asciiTheme: "minorHAnsi"`) but no literal family, the
|
|
1417
|
+
* dominant-font resolver routes through these before falling to the
|
|
1418
|
+
* `DEFAULT_FONT_AVG_CHAR_WIDTH` factor. Source: the document's
|
|
1419
|
+
* `subParts.resolvedTheme` — threaded from `buildPageStackWithSplits`.
|
|
1420
|
+
*/
|
|
1421
|
+
themeFonts?: LayoutThemeFonts,
|
|
1330
1422
|
): SectionPaginationResult {
|
|
1331
1423
|
if (blocks.length === 0) {
|
|
1332
1424
|
return {
|
|
@@ -1389,6 +1481,27 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1389
1481
|
// table is fully placed.
|
|
1390
1482
|
const tableProgress = new Map<string, number>();
|
|
1391
1483
|
|
|
1484
|
+
// Contextual-spacing pair mask — precomputed once so the inner pagination
|
|
1485
|
+
// loop can fold suppression in O(1) per block without re-scanning neighbors.
|
|
1486
|
+
const contextualSpacing = computeContextualSpacingAdjustments(blocks);
|
|
1487
|
+
const applyContextualSpacingAdjustment = (
|
|
1488
|
+
baseHeight: number,
|
|
1489
|
+
block: SurfaceBlockSnapshot | undefined,
|
|
1490
|
+
index: number,
|
|
1491
|
+
): number => {
|
|
1492
|
+
if (!block || block.kind !== "paragraph") return baseHeight;
|
|
1493
|
+
if (!contextualSpacing.before[index] && !contextualSpacing.after[index]) {
|
|
1494
|
+
return baseHeight;
|
|
1495
|
+
}
|
|
1496
|
+
const formatting = resolveBlockFormatting(block, defaultTabInterval, themeFonts);
|
|
1497
|
+
if (!formatting) return baseHeight;
|
|
1498
|
+
let delta = 0;
|
|
1499
|
+
if (contextualSpacing.before[index]) delta += formatting.spacingBefore;
|
|
1500
|
+
if (contextualSpacing.after[index]) delta += formatting.spacingAfter;
|
|
1501
|
+
if (delta === 0) return baseHeight;
|
|
1502
|
+
return Math.max(MIN_BLOCK_HEIGHT_TWIPS, baseHeight - delta);
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1392
1505
|
// P8.1b — per-page note tracking.
|
|
1393
1506
|
// `pendingNoteKeys` parallels `reservedNotes` but is only snapshotted on
|
|
1394
1507
|
// page-push (finalization), NOT on column break.
|
|
@@ -1510,31 +1623,42 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1510
1623
|
const columnWidth =
|
|
1511
1624
|
columnMetrics[Math.min(columnIndex, columnMetrics.length - 1)]?.width ??
|
|
1512
1625
|
getUsableColumnWidth(layout);
|
|
1513
|
-
const
|
|
1626
|
+
const rawBaseHeight = measureBlockHeight(
|
|
1514
1627
|
block,
|
|
1515
1628
|
columnWidth,
|
|
1516
1629
|
measurementProvider,
|
|
1517
1630
|
cache,
|
|
1518
1631
|
defaultTabInterval,
|
|
1632
|
+
themeFonts,
|
|
1633
|
+
);
|
|
1634
|
+
const baseHeight = applyContextualSpacingAdjustment(
|
|
1635
|
+
rawBaseHeight,
|
|
1636
|
+
block,
|
|
1637
|
+
index,
|
|
1519
1638
|
);
|
|
1520
1639
|
|
|
1521
1640
|
// keepNext: this paragraph must stay with the next one on the same page
|
|
1522
1641
|
const keepWithNextHeight =
|
|
1523
1642
|
block.kind === "paragraph" && block.keepNext
|
|
1524
1643
|
? baseHeight +
|
|
1525
|
-
|
|
1644
|
+
applyContextualSpacingAdjustment(
|
|
1645
|
+
measureBlockHeight(
|
|
1646
|
+
blocks[index + 1],
|
|
1647
|
+
columnWidth,
|
|
1648
|
+
measurementProvider,
|
|
1649
|
+
cache,
|
|
1650
|
+
defaultTabInterval,
|
|
1651
|
+
themeFonts,
|
|
1652
|
+
),
|
|
1526
1653
|
blocks[index + 1],
|
|
1527
|
-
|
|
1528
|
-
measurementProvider,
|
|
1529
|
-
cache,
|
|
1530
|
-
defaultTabInterval,
|
|
1654
|
+
index + 1,
|
|
1531
1655
|
)
|
|
1532
1656
|
: baseHeight;
|
|
1533
1657
|
|
|
1534
1658
|
// keepLines: the entire paragraph must fit on one page.
|
|
1535
1659
|
// If it doesn't fit and there's already content on this page, break before it.
|
|
1536
1660
|
const formatting = block.kind === "paragraph"
|
|
1537
|
-
? resolveBlockFormatting(block, defaultTabInterval)
|
|
1661
|
+
? resolveBlockFormatting(block, defaultTabInterval, themeFonts)
|
|
1538
1662
|
: null;
|
|
1539
1663
|
const keepLinesActive = formatting?.keepLines ?? false;
|
|
1540
1664
|
|
|
@@ -1760,6 +1884,16 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1760
1884
|
}
|
|
1761
1885
|
});
|
|
1762
1886
|
|
|
1887
|
+
if (hasPageBreak(block)) {
|
|
1888
|
+
// coord-04 §1.18.5 / coord-03 §11 — `<w:br w:type="page"/>`
|
|
1889
|
+
// forces subsequent content onto a new page regardless of
|
|
1890
|
+
// current column position. Mirrors the hasColumnBreak
|
|
1891
|
+
// last-column branch (pushPage + break) so pushPage's note-
|
|
1892
|
+
// allocation snapshot runs.
|
|
1893
|
+
pushPage(nextBoundary);
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1763
1897
|
if (hasColumnBreak(block)) {
|
|
1764
1898
|
if (columnIndex < maxColumns - 1) {
|
|
1765
1899
|
// Column break within a multi-column layout: advance to next column.
|
|
@@ -1936,6 +2070,37 @@ function currentPageNoteIds(
|
|
|
1936
2070
|
return notes;
|
|
1937
2071
|
}
|
|
1938
2072
|
|
|
2073
|
+
/**
|
|
2074
|
+
* OOXML §17.3.1.11 — `<w:framePr>`.
|
|
2075
|
+
*
|
|
2076
|
+
* A paragraph carrying frame properties is rendered as a text frame:
|
|
2077
|
+
* it is positioned relative to the page/margin/text (per `hAnchor` /
|
|
2078
|
+
* `vAnchor` / `xAlign` / `yAlign` / `xTwips` / `yTwips`) and the main
|
|
2079
|
+
* text flow wraps around it (per `wrap`). Consequently the framed
|
|
2080
|
+
* paragraph's height MUST NOT contribute to inline block-height
|
|
2081
|
+
* accounting on the page — otherwise the paginator double-counts the
|
|
2082
|
+
* frame (once as inline block, once as the positioned render).
|
|
2083
|
+
*
|
|
2084
|
+
* The exception is `dropCap` (`drop` / `margin`): the frame wraps
|
|
2085
|
+
* only the initial letter; the rest of the paragraph remains in the
|
|
2086
|
+
* main flow, so the paragraph must continue to contribute its
|
|
2087
|
+
* (non-dropped) height. `dropCap="none"` or absent is a full-frame.
|
|
2088
|
+
*
|
|
2089
|
+
* L11 owns the absolute-positioned render; L04 only needs to keep
|
|
2090
|
+
* the frame out of the flow accounting.
|
|
2091
|
+
*/
|
|
2092
|
+
function isOutOfFlowFrame(
|
|
2093
|
+
frameProperties:
|
|
2094
|
+
| Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["frameProperties"]
|
|
2095
|
+
| undefined,
|
|
2096
|
+
): boolean {
|
|
2097
|
+
if (!frameProperties) return false;
|
|
2098
|
+
if (frameProperties.dropCap === "drop" || frameProperties.dropCap === "margin") {
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
return true;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
1939
2104
|
function hasColumnBreak(block: SurfaceBlockSnapshot): boolean {
|
|
1940
2105
|
return block.kind === "paragraph" && block.segments.some(
|
|
1941
2106
|
(segment) =>
|
|
@@ -1943,3 +2108,11 @@ function hasColumnBreak(block: SurfaceBlockSnapshot): boolean {
|
|
|
1943
2108
|
segment.label === "Column break",
|
|
1944
2109
|
);
|
|
1945
2110
|
}
|
|
2111
|
+
|
|
2112
|
+
function hasPageBreak(block: SurfaceBlockSnapshot): boolean {
|
|
2113
|
+
return block.kind === "paragraph" && block.segments.some(
|
|
2114
|
+
(segment) =>
|
|
2115
|
+
segment.kind === "opaque_inline" &&
|
|
2116
|
+
segment.label === "Page break",
|
|
2117
|
+
);
|
|
2118
|
+
}
|