postext 1.23.0 → 1.25.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.
- package/dist/__tests__/bundle/bitmapInfo.test.d.ts +2 -0
- package/dist/__tests__/bundle/bitmapInfo.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/bitmapInfo.test.js +78 -0
- package/dist/__tests__/bundle/bitmapInfo.test.js.map +1 -0
- package/dist/__tests__/bundle/cjkRulesPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/cjkRulesPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/cjkRulesPin.test.js +78 -0
- package/dist/__tests__/bundle/cjkRulesPin.test.js.map +1 -0
- package/dist/__tests__/bundle/designOverflowPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/designOverflowPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/designOverflowPin.test.js +89 -0
- package/dist/__tests__/bundle/designOverflowPin.test.js.map +1 -0
- package/dist/__tests__/bundle/flowColumnsPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/flowColumnsPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/flowColumnsPin.test.js +40 -0
- package/dist/__tests__/bundle/flowColumnsPin.test.js.map +1 -0
- package/dist/__tests__/bundle/gridBalancingPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/gridBalancingPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/gridBalancingPin.test.js +53 -0
- package/dist/__tests__/bundle/gridBalancingPin.test.js.map +1 -0
- package/dist/__tests__/bundle/inlineTableSplitPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/inlineTableSplitPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/inlineTableSplitPin.test.js +43 -0
- package/dist/__tests__/bundle/inlineTableSplitPin.test.js.map +1 -0
- package/dist/__tests__/bundle/openerHeadFloatsPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/openerHeadFloatsPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/openerHeadFloatsPin.test.js +50 -0
- package/dist/__tests__/bundle/openerHeadFloatsPin.test.js.map +1 -0
- package/dist/__tests__/bundle.test.js +11 -1
- package/dist/__tests__/bundle.test.js.map +1 -1
- package/dist/__tests__/cjk/annotationsRender.test.js +5 -2
- package/dist/__tests__/cjk/annotationsRender.test.js.map +1 -1
- package/dist/__tests__/cjk/circledNumbers.test.d.ts +2 -0
- package/dist/__tests__/cjk/circledNumbers.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/circledNumbers.test.js +83 -0
- package/dist/__tests__/cjk/circledNumbers.test.js.map +1 -0
- package/dist/__tests__/cjk/gridBalancing.test.d.ts +2 -0
- package/dist/__tests__/cjk/gridBalancing.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/gridBalancing.test.js +227 -0
- package/dist/__tests__/cjk/gridBalancing.test.js.map +1 -0
- package/dist/__tests__/cjk/justifyRender.test.js +2 -1
- package/dist/__tests__/cjk/justifyRender.test.js.map +1 -1
- package/dist/__tests__/cjk/titleBreaks.test.d.ts +2 -0
- package/dist/__tests__/cjk/titleBreaks.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/titleBreaks.test.js +76 -0
- package/dist/__tests__/cjk/titleBreaks.test.js.map +1 -0
- package/dist/__tests__/cjk/wordJoiner.test.js +13 -4
- package/dist/__tests__/cjk/wordJoiner.test.js.map +1 -1
- package/dist/__tests__/codeBlocks.test.js +1 -1
- package/dist/__tests__/codeBlocks.test.js.map +1 -1
- package/dist/__tests__/design/cjkDesignText.test.d.ts +2 -0
- package/dist/__tests__/design/cjkDesignText.test.d.ts.map +1 -0
- package/dist/__tests__/design/cjkDesignText.test.js +130 -0
- package/dist/__tests__/design/cjkDesignText.test.js.map +1 -0
- package/dist/__tests__/design/cjkDesignTextRender.test.d.ts +2 -0
- package/dist/__tests__/design/cjkDesignTextRender.test.d.ts.map +1 -0
- package/dist/__tests__/design/cjkDesignTextRender.test.js +100 -0
- package/dist/__tests__/design/cjkDesignTextRender.test.js.map +1 -0
- package/dist/__tests__/design/designTextOverflow.test.d.ts +2 -0
- package/dist/__tests__/design/designTextOverflow.test.d.ts.map +1 -0
- package/dist/__tests__/design/designTextOverflow.test.js +102 -0
- package/dist/__tests__/design/designTextOverflow.test.js.map +1 -0
- package/dist/__tests__/exports.test.js +60 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/frontmatterInvalid.test.d.ts +2 -0
- package/dist/__tests__/frontmatterInvalid.test.d.ts.map +1 -0
- package/dist/__tests__/frontmatterInvalid.test.js +101 -0
- package/dist/__tests__/frontmatterInvalid.test.js.map +1 -0
- package/dist/__tests__/hardBreaks.test.js +1 -1
- package/dist/__tests__/hardBreaks.test.js.map +1 -1
- package/dist/__tests__/headingFirstLineIndent.test.d.ts +2 -0
- package/dist/__tests__/headingFirstLineIndent.test.d.ts.map +1 -0
- package/dist/__tests__/headingFirstLineIndent.test.js +185 -0
- package/dist/__tests__/headingFirstLineIndent.test.js.map +1 -0
- package/dist/__tests__/headingInlineMarks.test.js +1 -1
- package/dist/__tests__/headingInlineMarks.test.js.map +1 -1
- package/dist/__tests__/headingLetterSpacing.test.js +1 -1
- package/dist/__tests__/headingLetterSpacing.test.js.map +1 -1
- package/dist/__tests__/headingNumberSpans.test.d.ts +2 -0
- package/dist/__tests__/headingNumberSpans.test.d.ts.map +1 -0
- package/dist/__tests__/headingNumberSpans.test.js +96 -0
- package/dist/__tests__/headingNumberSpans.test.js.map +1 -0
- package/dist/__tests__/mathSizeMigration.test.js +2 -2
- package/dist/__tests__/mathSizeMigration.test.js.map +1 -1
- package/dist/__tests__/multipleColumns.test.js +207 -0
- package/dist/__tests__/multipleColumns.test.js.map +1 -1
- package/dist/__tests__/parse/citations.test.js +29 -0
- package/dist/__tests__/parse/citations.test.js.map +1 -1
- package/dist/__tests__/pipeline/bitmapResolution.test.d.ts +2 -0
- package/dist/__tests__/pipeline/bitmapResolution.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/bitmapResolution.test.js +139 -0
- package/dist/__tests__/pipeline/bitmapResolution.test.js.map +1 -0
- package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js +10 -6
- package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js.map +1 -1
- package/dist/__tests__/pipeline/calloutOverflow.test.js +4 -2
- package/dist/__tests__/pipeline/calloutOverflow.test.js.map +1 -1
- package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js +1 -1
- package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js.map +1 -1
- package/dist/__tests__/pipeline/calloutTallKeepTogether.test.js +6 -5
- package/dist/__tests__/pipeline/calloutTallKeepTogether.test.js.map +1 -1
- package/dist/__tests__/pipeline/colonListRoom.test.js +1 -1
- package/dist/__tests__/pipeline/colonListRoom.test.js.map +1 -1
- package/dist/__tests__/pipeline/compoundBreaks.test.js +1 -1
- package/dist/__tests__/pipeline/compoundBreaks.test.js.map +1 -1
- package/dist/__tests__/pipeline/dashBreaks.test.js +1 -1
- package/dist/__tests__/pipeline/dashBreaks.test.js.map +1 -1
- package/dist/__tests__/pipeline/designTextTruncated.test.d.ts +2 -0
- package/dist/__tests__/pipeline/designTextTruncated.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/designTextTruncated.test.js +116 -0
- package/dist/__tests__/pipeline/designTextTruncated.test.js.map +1 -0
- package/dist/__tests__/pipeline/floatCitingPage.test.d.ts +2 -0
- package/dist/__tests__/pipeline/floatCitingPage.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/floatCitingPage.test.js +241 -0
- package/dist/__tests__/pipeline/floatCitingPage.test.js.map +1 -0
- package/dist/__tests__/pipeline/floatOverInlineResource.test.js +18 -6
- package/dist/__tests__/pipeline/floatOverInlineResource.test.js.map +1 -1
- package/dist/__tests__/pipeline/floatPlacement.test.js +3 -3
- package/dist/__tests__/pipeline/floatPlacement.test.js.map +1 -1
- package/dist/__tests__/pipeline/floatShrink.test.d.ts +2 -0
- package/dist/__tests__/pipeline/floatShrink.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/floatShrink.test.js +298 -0
- package/dist/__tests__/pipeline/floatShrink.test.js.map +1 -0
- package/dist/__tests__/pipeline/flowColumns.test.d.ts +2 -0
- package/dist/__tests__/pipeline/flowColumns.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/flowColumns.test.js +261 -0
- package/dist/__tests__/pipeline/flowColumns.test.js.map +1 -0
- package/dist/__tests__/pipeline/headingKeepFitEps.test.d.ts +2 -0
- package/dist/__tests__/pipeline/headingKeepFitEps.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/headingKeepFitEps.test.js +83 -0
- package/dist/__tests__/pipeline/headingKeepFitEps.test.js.map +1 -0
- package/dist/__tests__/pipeline/headingSplitOrphans.test.js +1 -1
- package/dist/__tests__/pipeline/headingSplitOrphans.test.js.map +1 -1
- package/dist/__tests__/pipeline/inlineFigureSpace.test.js +1 -1
- package/dist/__tests__/pipeline/inlineFigureSpace.test.js.map +1 -1
- package/dist/__tests__/pipeline/inlineTableSplit.test.d.ts +2 -0
- package/dist/__tests__/pipeline/inlineTableSplit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/inlineTableSplit.test.js +238 -0
- package/dist/__tests__/pipeline/inlineTableSplit.test.js.map +1 -0
- package/dist/__tests__/pipeline/paragraphContainerTail.test.js +1 -1
- package/dist/__tests__/pipeline/paragraphContainerTail.test.js.map +1 -1
- package/dist/__tests__/pipeline/raggedOptimal.test.js +1 -1
- package/dist/__tests__/pipeline/raggedOptimal.test.js.map +1 -1
- package/dist/__tests__/pipeline/resolvedPaletteCopy.test.js +3 -2
- package/dist/__tests__/pipeline/resolvedPaletteCopy.test.js.map +1 -1
- package/dist/__tests__/pipeline/sideBoxAfterText.test.d.ts +2 -0
- package/dist/__tests__/pipeline/sideBoxAfterText.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/sideBoxAfterText.test.js +163 -0
- package/dist/__tests__/pipeline/sideBoxAfterText.test.js.map +1 -0
- package/dist/__tests__/pipeline/textWrap.test.d.ts +2 -0
- package/dist/__tests__/pipeline/textWrap.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/textWrap.test.js +453 -0
- package/dist/__tests__/pipeline/textWrap.test.js.map +1 -0
- package/dist/__tests__/preflight.test.js +30 -1
- package/dist/__tests__/preflight.test.js.map +1 -1
- package/dist/__tests__/prepareFonts.test.d.ts +2 -0
- package/dist/__tests__/prepareFonts.test.d.ts.map +1 -0
- package/dist/__tests__/prepareFonts.test.js +358 -0
- package/dist/__tests__/prepareFonts.test.js.map +1 -0
- package/dist/__tests__/presets/showcaseDesignOverflow.test.d.ts +2 -0
- package/dist/__tests__/presets/showcaseDesignOverflow.test.d.ts.map +1 -0
- package/dist/__tests__/presets/showcaseDesignOverflow.test.js +58 -0
- package/dist/__tests__/presets/showcaseDesignOverflow.test.js.map +1 -0
- package/dist/__tests__/svgFonts.test.d.ts +2 -0
- package/dist/__tests__/svgFonts.test.d.ts.map +1 -0
- package/dist/__tests__/svgFonts.test.js +286 -0
- package/dist/__tests__/svgFonts.test.js.map +1 -0
- package/dist/__tests__/table/booktabs.test.d.ts +2 -0
- package/dist/__tests__/table/booktabs.test.d.ts.map +1 -0
- package/dist/__tests__/table/booktabs.test.js +290 -0
- package/dist/__tests__/table/booktabs.test.js.map +1 -0
- package/dist/__tests__/textWrapMeasure.test.d.ts +2 -0
- package/dist/__tests__/textWrapMeasure.test.d.ts.map +1 -0
- package/dist/__tests__/textWrapMeasure.test.js +162 -0
- package/dist/__tests__/textWrapMeasure.test.js.map +1 -0
- package/dist/__tests__/verseLines.test.js +134 -4
- package/dist/__tests__/verseLines.test.js.map +1 -1
- package/dist/__tests__/vertical/measurePaint.test.js +16 -0
- package/dist/__tests__/vertical/measurePaint.test.js.map +1 -1
- package/dist/bitmapResolution.d.ts +42 -0
- package/dist/bitmapResolution.d.ts.map +1 -0
- package/dist/bitmapResolution.js +82 -0
- package/dist/bitmapResolution.js.map +1 -0
- package/dist/bundle/adapters.d.ts +31 -7
- package/dist/bundle/adapters.d.ts.map +1 -1
- package/dist/bundle/adapters.js +113 -34
- package/dist/bundle/adapters.js.map +1 -1
- package/dist/bundle/codec.d.ts +9 -2
- package/dist/bundle/codec.d.ts.map +1 -1
- package/dist/bundle/codec.js +25 -2
- package/dist/bundle/codec.js.map +1 -1
- package/dist/bundle/configVersion.d.ts +168 -10
- package/dist/bundle/configVersion.d.ts.map +1 -1
- package/dist/bundle/configVersion.js +414 -8
- package/dist/bundle/configVersion.js.map +1 -1
- package/dist/bundle/index.d.ts +5 -5
- package/dist/bundle/index.d.ts.map +1 -1
- package/dist/bundle/index.js +3 -3
- package/dist/bundle/index.js.map +1 -1
- package/dist/bundle/manifest.d.ts +12 -2
- package/dist/bundle/manifest.d.ts.map +1 -1
- package/dist/bundle/manifest.js +134 -7
- package/dist/bundle/manifest.js.map +1 -1
- package/dist/bundle/types.d.ts +32 -2
- package/dist/bundle/types.d.ts.map +1 -1
- package/dist/canvas-backend/blockRender.d.ts.map +1 -1
- package/dist/canvas-backend/blockRender.js +6 -4
- package/dist/canvas-backend/blockRender.js.map +1 -1
- package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.js +3 -1
- package/dist/canvas-backend/headerFooter.js.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.js +15 -1
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
- package/dist/columnRule.d.ts.map +1 -1
- package/dist/columnRule.js +5 -2
- package/dist/columnRule.js.map +1 -1
- package/dist/configWarnings.js +30 -3
- package/dist/configWarnings.js.map +1 -1
- package/dist/defaults/cjk.d.ts +2 -1
- package/dist/defaults/cjk.d.ts.map +1 -1
- package/dist/defaults/cjk.js +17 -1
- package/dist/defaults/cjk.js.map +1 -1
- package/dist/defaults/diagramStyle.d.ts.map +1 -1
- package/dist/defaults/diagramStyle.js +6 -0
- package/dist/defaults/diagramStyle.js.map +1 -1
- package/dist/defaults/headerFooter.d.ts +21 -6
- package/dist/defaults/headerFooter.d.ts.map +1 -1
- package/dist/defaults/headerFooter.js +37 -14
- package/dist/defaults/headerFooter.js.map +1 -1
- package/dist/defaults/headings.d.ts +5 -4
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +18 -4
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/index.d.ts +1 -1
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +1 -1
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/layout.d.ts +35 -1
- package/dist/defaults/layout.d.ts.map +1 -1
- package/dist/defaults/layout.js +137 -1
- package/dist/defaults/layout.js.map +1 -1
- package/dist/defaults/parts.js +4 -4
- package/dist/defaults/parts.js.map +1 -1
- package/dist/defaults/tableStyle.d.ts.map +1 -1
- package/dist/defaults/tableStyle.js +50 -0
- package/dist/defaults/tableStyle.js.map +1 -1
- package/dist/defaults/toc.js +2 -2
- package/dist/defaults/verse.d.ts +2 -1
- package/dist/defaults/verse.d.ts.map +1 -1
- package/dist/defaults/verse.js +6 -1
- package/dist/defaults/verse.js.map +1 -1
- package/dist/design/cjkText.d.ts +34 -0
- package/dist/design/cjkText.d.ts.map +1 -0
- package/dist/design/cjkText.js +82 -0
- package/dist/design/cjkText.js.map +1 -0
- package/dist/design/layout.d.ts +15 -1
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +130 -8
- package/dist/design/layout.js.map +1 -1
- package/dist/design/richText.d.ts +24 -0
- package/dist/design/richText.d.ts.map +1 -1
- package/dist/design/richText.js +37 -1
- package/dist/design/richText.js.map +1 -1
- package/dist/fonts/buildWithFonts.d.ts +19 -0
- package/dist/fonts/buildWithFonts.d.ts.map +1 -0
- package/dist/fonts/buildWithFonts.js +16 -0
- package/dist/fonts/buildWithFonts.js.map +1 -0
- package/dist/fonts/faces.d.ts +88 -0
- package/dist/fonts/faces.d.ts.map +1 -0
- package/dist/fonts/faces.js +264 -0
- package/dist/fonts/faces.js.map +1 -0
- package/dist/fonts/fontSet.d.ts +34 -0
- package/dist/fonts/fontSet.d.ts.map +1 -0
- package/dist/fonts/fontSet.js +173 -0
- package/dist/fonts/fontSet.js.map +1 -0
- package/dist/fonts/prepare.d.ts +120 -0
- package/dist/fonts/prepare.d.ts.map +1 -0
- package/dist/fonts/prepare.js +375 -0
- package/dist/fonts/prepare.js.map +1 -0
- package/dist/frontmatter.d.ts +20 -0
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +69 -3
- package/dist/frontmatter.js.map +1 -1
- package/dist/html-backend.d.ts +17 -0
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +109 -7
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +35 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/dist/measure/bidiLines.d.ts +4 -3
- package/dist/measure/bidiLines.d.ts.map +1 -1
- package/dist/measure/bidiLines.js +12 -7
- package/dist/measure/bidiLines.js.map +1 -1
- package/dist/measure/cache.d.ts.map +1 -1
- package/dist/measure/cache.js +32 -2
- package/dist/measure/cache.js.map +1 -1
- package/dist/measure/canvas.d.ts +15 -0
- package/dist/measure/canvas.d.ts.map +1 -1
- package/dist/measure/canvas.js +68 -4
- package/dist/measure/canvas.js.map +1 -1
- package/dist/measure/cjk.d.ts.map +1 -1
- package/dist/measure/cjk.js +4 -1
- package/dist/measure/cjk.js.map +1 -1
- package/dist/measure/cjkClasses.d.ts +17 -0
- package/dist/measure/cjkClasses.d.ts.map +1 -1
- package/dist/measure/cjkClasses.js +41 -1
- package/dist/measure/cjkClasses.js.map +1 -1
- package/dist/measure/cjkCompose.d.ts.map +1 -1
- package/dist/measure/cjkCompose.js +126 -10
- package/dist/measure/cjkCompose.js.map +1 -1
- package/dist/measure/cjkPunctuation.d.ts +18 -1
- package/dist/measure/cjkPunctuation.d.ts.map +1 -1
- package/dist/measure/cjkPunctuation.js +13 -1
- package/dist/measure/cjkPunctuation.js.map +1 -1
- package/dist/measure/font.d.ts +8 -0
- package/dist/measure/font.d.ts.map +1 -1
- package/dist/measure/font.js +13 -2
- package/dist/measure/font.js.map +1 -1
- package/dist/measure/fontString.d.ts +14 -0
- package/dist/measure/fontString.d.ts.map +1 -0
- package/dist/measure/fontString.js +55 -0
- package/dist/measure/fontString.js.map +1 -0
- package/dist/measure/hardBreaks.d.ts.map +1 -1
- package/dist/measure/hardBreaks.js +13 -1
- package/dist/measure/hardBreaks.js.map +1 -1
- package/dist/measure/index.d.ts +7 -4
- package/dist/measure/index.d.ts.map +1 -1
- package/dist/measure/index.js +7 -5
- package/dist/measure/index.js.map +1 -1
- package/dist/measure/plain.d.ts.map +1 -1
- package/dist/measure/plain.js +11 -7
- package/dist/measure/plain.js.map +1 -1
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +9 -6
- package/dist/measure/rich.js.map +1 -1
- package/dist/measure/types.d.ts +53 -3
- package/dist/measure/types.d.ts.map +1 -1
- package/dist/measure/types.js +67 -3
- package/dist/measure/types.js.map +1 -1
- package/dist/parse/citations.d.ts.map +1 -1
- package/dist/parse/citations.js +7 -3
- package/dist/parse/citations.js.map +1 -1
- package/dist/pipeline/build.d.ts +36 -0
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +1482 -153
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/buildBlockKind.d.ts +5 -0
- package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
- package/dist/pipeline/buildBlockKind.js +34 -7
- package/dist/pipeline/buildBlockKind.js.map +1 -1
- package/dist/pipeline/buildMeasurement.d.ts +6 -0
- package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
- package/dist/pipeline/buildMeasurement.js +4 -3
- package/dist/pipeline/buildMeasurement.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts +59 -0
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +287 -65
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/columnBalancing.d.ts +20 -0
- package/dist/pipeline/columnBalancing.d.ts.map +1 -1
- package/dist/pipeline/columnBalancing.js +27 -4
- package/dist/pipeline/columnBalancing.js.map +1 -1
- package/dist/pipeline/columnsGroup.d.ts +61 -0
- package/dist/pipeline/columnsGroup.d.ts.map +1 -0
- package/dist/pipeline/columnsGroup.js +88 -0
- package/dist/pipeline/columnsGroup.js.map +1 -0
- package/dist/pipeline/config.d.ts +28 -0
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +67 -8
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/contentWarnings.d.ts +16 -0
- package/dist/pipeline/contentWarnings.d.ts.map +1 -1
- package/dist/pipeline/contentWarnings.js +141 -17
- package/dist/pipeline/contentWarnings.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +48 -4
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js +43 -5
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/floatSlots.d.ts +16 -3
- package/dist/pipeline/floatSlots.d.ts.map +1 -1
- package/dist/pipeline/floatSlots.js +15 -6
- package/dist/pipeline/floatSlots.js.map +1 -1
- package/dist/pipeline/flowColumns.d.ts +41 -0
- package/dist/pipeline/flowColumns.d.ts.map +1 -0
- package/dist/pipeline/flowColumns.js +116 -0
- package/dist/pipeline/flowColumns.js.map +1 -0
- package/dist/pipeline/headerFooter.d.ts +4 -1
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +19 -5
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/measureContentBlock.d.ts +20 -1
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +35 -11
- package/dist/pipeline/measureContentBlock.js.map +1 -1
- package/dist/pipeline/partPalette.d.ts +3 -0
- package/dist/pipeline/partPalette.d.ts.map +1 -1
- package/dist/pipeline/partPalette.js +5 -0
- package/dist/pipeline/partPalette.js.map +1 -1
- package/dist/pipeline/placement.d.ts +19 -0
- package/dist/pipeline/placement.d.ts.map +1 -1
- package/dist/pipeline/placement.js +12 -2
- package/dist/pipeline/placement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +41 -0
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +148 -18
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/pipeline/styles.d.ts.map +1 -1
- package/dist/pipeline/styles.js +8 -5
- package/dist/pipeline/styles.js.map +1 -1
- package/dist/pipeline/textWrap.d.ts +94 -0
- package/dist/pipeline/textWrap.d.ts.map +1 -0
- package/dist/pipeline/textWrap.js +188 -0
- package/dist/pipeline/textWrap.js.map +1 -0
- package/dist/pipeline/toc.d.ts.map +1 -1
- package/dist/pipeline/toc.js +6 -1
- package/dist/pipeline/toc.js.map +1 -1
- package/dist/pipeline/verseLines.d.ts +22 -2
- package/dist/pipeline/verseLines.d.ts.map +1 -1
- package/dist/pipeline/verseLines.js +52 -4
- package/dist/pipeline/verseLines.js.map +1 -1
- package/dist/preflight.d.ts +46 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +42 -7
- package/dist/preflight.js.map +1 -1
- package/dist/svg/fontRegistry.d.ts +66 -0
- package/dist/svg/fontRegistry.d.ts.map +1 -0
- package/dist/svg/fontRegistry.js +294 -0
- package/dist/svg/fontRegistry.js.map +1 -0
- package/dist/svg/fonts.d.ts +140 -0
- package/dist/svg/fonts.d.ts.map +1 -0
- package/dist/svg/fonts.js +642 -0
- package/dist/svg/fonts.js.map +1 -0
- package/dist/svg/image.d.ts +44 -0
- package/dist/svg/image.d.ts.map +1 -0
- package/dist/svg/image.js +67 -0
- package/dist/svg/image.js.map +1 -0
- package/dist/table/booktabs.d.ts +37 -0
- package/dist/table/booktabs.d.ts.map +1 -0
- package/dist/table/booktabs.js +82 -0
- package/dist/table/booktabs.js.map +1 -0
- package/dist/types.d.ts +365 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/util/stableHash.d.ts +20 -0
- package/dist/util/stableHash.d.ts.map +1 -0
- package/dist/util/stableHash.js +59 -0
- package/dist/util/stableHash.js.map +1 -0
- package/dist/vdt.d.ts +249 -9
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js +3 -2
- package/dist/vdt.js.map +1 -1
- package/dist/word/index.d.ts +1 -1
- package/dist/word/index.d.ts.map +1 -1
- package/dist/word/index.js +1 -1
- package/dist/word/index.js.map +1 -1
- package/dist/word/toMarkdown.d.ts +10 -0
- package/dist/word/toMarkdown.d.ts.map +1 -1
- package/dist/word/toMarkdown.js +16 -1
- package/dist/word/toMarkdown.js.map +1 -1
- package/dist/worker/client.d.ts +8 -0
- package/dist/worker/client.d.ts.map +1 -1
- package/dist/worker/client.js +22 -0
- package/dist/worker/client.js.map +1 -1
- package/dist/worker/fonts.d.ts +1 -13
- package/dist/worker/fonts.d.ts.map +1 -1
- package/dist/worker/fonts.js +3 -61
- package/dist/worker/fonts.js.map +1 -1
- package/dist/worker/layout.worker.js +23 -16
- package/dist/worker/layout.worker.js.map +1 -1
- package/package.json +1 -1
package/dist/pipeline/build.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withBitmapResolutions } from '../bitmapResolution';
|
|
1
2
|
import { plainTitleBreaks } from '../parse/inlineFormatting';
|
|
2
3
|
import { dimensionToPx } from '../units';
|
|
3
4
|
import { createVDTDocument, createVDTBlock, createBoundingBox, createVDTColumn, pageIsMirrored, pageIsVertical, flowRectToPage, } from '../vdt';
|
|
@@ -14,7 +15,10 @@ import { stampCentralBaselines } from './verticalMetrics';
|
|
|
14
15
|
import { orientLinesToFrames } from './mirrorFrame';
|
|
15
16
|
import { verseMarginTopPx } from './buildBlockKind';
|
|
16
17
|
import { verseLinesSettings, versesLineByLine } from './verseLines';
|
|
17
|
-
import { resolveAllConfig, computeBaselineGrid, resolvedDirection, resolvedLocale } from './config';
|
|
18
|
+
import { resolveAllConfig, computeBaselineGrid, resolvedDirection, resolvedLocale, validateConfigCache } from './config';
|
|
19
|
+
import { resolveDebugConfig } from '../defaults/debug';
|
|
20
|
+
import { defaultFontSet, documentFontFaces, fontFallbacks } from '../fonts/faces';
|
|
21
|
+
import { syncFontSet } from '../fonts/fontSet';
|
|
18
22
|
import { asciiDigits } from '../arabicNumerals';
|
|
19
23
|
import { getMeasureDirection, setMeasureDirection, shiftLineX } from '../measure/bidiLines';
|
|
20
24
|
import { createHeadingLevelResolver, deriveSectionGeometryConfig, sectionWritingMode, deriveSectionMeasureContext, headingIsHidden, headingIsNumbered, headingMarksFor, headingStyleOf, numberTitlesFor, planHeadingSections, } from './headingStyles';
|
|
@@ -24,12 +28,13 @@ import { bookCitationContexts, needsCitationContext } from '../citations/context
|
|
|
24
28
|
import { anchorTargetsOf, hasAnchorRefs, printsAnchorPages } from './crossRefs';
|
|
25
29
|
import { applyStatementNumbering, numberStatements, outlineLookup } from './statementNumbering';
|
|
26
30
|
import { SPAN_EMBED_STYLE_ID, isPageSpanStrip, spanEmbedStyle, wrapPageSpanEmbeds } from './spanEmbeds';
|
|
31
|
+
import { flowColumnsStyles, isFlowColumnsStyle, wrapFlowColumns } from './flowColumns';
|
|
27
32
|
import { anchorOutline, computeOutline, hasIndexDirective, hasTocDirective, headingNumberingOptions, headingTemplatesOf, outlineFromDoc, sameOutline } from './outline';
|
|
28
33
|
import { expandTocDirectives } from './toc';
|
|
29
34
|
import { expandIndexDirectives, locateIndexMarks } from './indexDirective';
|
|
30
35
|
import { resolveBodyStyle, resolveBlockquoteStyle, resolveParagraphStyle } from './styles';
|
|
31
36
|
import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, listBulletPosition, listItemGapPx, listItemSpacingPx, mirrorListMarker, } from './lists';
|
|
32
|
-
import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeAtomicBlock, createPartPage, pageHasContent, sideColumnOf, sideColumns, sideUsedBottom, pageIsOccupied, bandColumns, currentBand, isBandLevel, bandUsedBottom, closeBandAndInsertSpan, closeBandAt, pageLayoutOf, } from './placement';
|
|
37
|
+
import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeAtomicBlock, createPartPage, pageHasContent, sideColumnOf, sideColumns, sideUsedBottom, pageIsOccupied, bandColumns, currentBand, isBandLevel, bandUsedBottom, closeBandAndInsertSpan, closeBandAt, pageLayoutOf, FIT_EPS, } from './placement';
|
|
33
38
|
import { chooseParagraphSplit } from './orphanWidow';
|
|
34
39
|
import { layoutComicPage, comicPageLayoutWarnings } from '../comics/layoutPage';
|
|
35
40
|
import { comicStripExtent, comicStripOffset, comicStripPlacement, comicStripWidth, layoutComicStrip } from '../comics/strip';
|
|
@@ -40,25 +45,28 @@ import { containerStyle, paragraphStyleIdOf, planParagraphContainers } from './p
|
|
|
40
45
|
import { appendChapterEndNotes, footnoteIdsOfLines, footnoteParagraphStyle, noteContentBlock, noteNumberPositionOf, numberFootnotes, footnoteFormatOf, numberFootnotesByPlacement, sameFootnoteNumbers, splitFootnoteDefinitions, withFootnoteStyle, } from './footnotes';
|
|
41
46
|
import { planParts, derivePartMeasureContext } from './parts';
|
|
42
47
|
import { layoutCallout, offsetBlock, offsetCalloutToAbsolute, pickCalloutStyle, planCallouts, resolveCalloutAttrs, } from './calloutLayout';
|
|
43
|
-
import {
|
|
48
|
+
import { fillGalley, galleyBreaks } from './columnsGroup';
|
|
49
|
+
import { chooseTableSlice, layoutCaptionText, layoutResourceBlock, MIN_TAIL_ROWS } from './resourceLayout';
|
|
44
50
|
import { computeFloatPlan, floatedResourceIds, floatColumnCount, } from './floatPlacement';
|
|
45
|
-
import { enumerateCurrentPageSlots, measureFloatBand, floatGapAbove, measureSideStack, clearSideObstacles, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
|
|
51
|
+
import { enumerateCurrentPageSlots, measureFloatBand, floatGapAbove, floatGapBelow, measureSideStack, clearSideObstacles, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
|
|
46
52
|
import { computeHeadingContext, computeResourceNumberingState, } from './resourceNumbering';
|
|
47
53
|
import { documentLocale, effectiveResourceTypes } from '../defaults/resourceTypes';
|
|
48
54
|
import { resolveCalloutStylesConfig } from '../defaults/calloutStyles';
|
|
49
55
|
import { codeBoxStyle, codeParseOptions, wrapCodeBlocks } from './codeBlocks';
|
|
50
56
|
import { buildCodeLineNumbers, codeOverflowWarnings } from './codeLines';
|
|
51
57
|
import { pickTableStyle } from '../defaults/tableStyle';
|
|
58
|
+
import { DEFAULT_FLOAT_MIN_SCALE } from '../defaults/layout';
|
|
52
59
|
import { buildLineNumbers, lineNumberWarnings } from './lineNumbers';
|
|
53
60
|
import { buildHeadersAndFooters, defaultOpenerTitle, headingDesignBoxes, measureDefaultOpenerHeight, measureHeadingDesign } from './headerFooter';
|
|
54
61
|
import { flowColorValues } from './partPalette';
|
|
55
62
|
import { chapterNumberCounter, leadingBoldText } from './placeholders';
|
|
56
|
-
import { proposeBalanceLines, collectColumnGaps, firstDivergentColumn, gapLinesIn, boxRoomIn, boxLeverKeys, pageSegments, MAX_BALANCING_PASSES, MAX_BALANCING_PASSES_PER_DOCUMENT, balanceKey, candidateKey, flexFigureKey, flexFloatKey } from './columnBalancing';
|
|
63
|
+
import { proposeBalanceLines, GRID_LINE_LEVERS, collectColumnGaps, firstDivergentColumn, gapLinesIn, boxRoomIn, boxLeverKeys, pageSegments, MAX_BALANCING_PASSES, MAX_BALANCING_PASSES_PER_DOCUMENT, balanceKey, candidateKey, flexFigureKey, flexFloatKey } from './columnBalancing';
|
|
57
64
|
import { applyBandCap, uncapBand, columnBottom, bandCapLines, bandTop, bareBandTop, resolveBandCapsGen, drainPasses, resolveTrailingCapsGen, bandCapLinesAroundZone, } from './bandCaps';
|
|
58
65
|
import { raggedLooseLines } from './raggedLines';
|
|
59
|
-
import { cjkLooseLineWarnings, collectContentWarnings, dropCapWarnings, joiningLetterSpacingWarnings, locateContentWarnings, wordOverflowWarnings } from './contentWarnings';
|
|
66
|
+
import { cjkLooseLineWarnings, collectContentWarnings, designTruncationWarnings, dropCapWarnings, floatShrinkWarnings, joiningLetterSpacingWarnings, locateContentWarnings, wordOverflowWarnings } from './contentWarnings';
|
|
60
67
|
import { dropCapSinkAfter } from './dropCap';
|
|
61
68
|
import { mostlyJoiningScript } from '../measure/joining';
|
|
69
|
+
import { composesAsCjk } from '../measure/cjkCompose';
|
|
62
70
|
import { annotateDocument } from '../cjkMarks';
|
|
63
71
|
import { annotateArabicMarks } from '../arabicMarks';
|
|
64
72
|
import { literalBreaksFor } from './hardBreaks';
|
|
@@ -67,9 +75,8 @@ import { overlineEmphasis } from '../emphasisOverline';
|
|
|
67
75
|
import { withBookTitleBrackets } from './annotations';
|
|
68
76
|
import { paperByBlock, stampPagePaper } from './paper';
|
|
69
77
|
import { headingLineSpanBand } from './lineSpan';
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const FIT_EPS = 0.01;
|
|
78
|
+
import { columnCursorY, insetsBefore, minTextWidthPx, openColumnWrap, resolveCalloutWrap, resolveResourceWrap, runsBesideWrap, sameInsetsFrom, settleColumnWraps, transparentToWrap, wrapGapPx, wrapInsetSteps } from './textWrap';
|
|
79
|
+
import { lineInsetsAt } from '../measure/types';
|
|
73
80
|
/** Room (px) under closing boxes a balancing pass must close to be kept on
|
|
74
81
|
* that ground alone (see `boxRoomIn`). */
|
|
75
82
|
const BOX_ROOM_EPS_PX = 0.5;
|
|
@@ -97,6 +104,16 @@ function floatPagesOf(doc) {
|
|
|
97
104
|
}
|
|
98
105
|
return out;
|
|
99
106
|
}
|
|
107
|
+
/** Whether `pageIndex` opens a run of pages after an explicit break (the
|
|
108
|
+
* document's first page, the page after a chapter's opener break, a part
|
|
109
|
+
* or a `:::pagebreak`, past the blank pages parity put between them): no
|
|
110
|
+
* float heads such a page ahead of its citing line (#633). */
|
|
111
|
+
function opensAfterBreak(pages, pageIndex, forcedBreakPages) {
|
|
112
|
+
let p = pageIndex - 1;
|
|
113
|
+
while (p >= 0 && (pages[p]?.blankForParity || pages[p]?.blankForForce))
|
|
114
|
+
p--;
|
|
115
|
+
return p < 0 || forcedBreakPages.has(p);
|
|
116
|
+
}
|
|
100
117
|
/** `{chapterNumber}` at each content block: the value of the last level-1
|
|
101
118
|
* heading at or before it ('' before the first), by the counter the
|
|
102
119
|
* running heads use over the placed blocks (`computeChapterNumbers`) —
|
|
@@ -144,7 +161,7 @@ function chapterNumbersOfContent(blocks, resolved, prefixes, values, ordinalOffs
|
|
|
144
161
|
* line less (EF-129: asked for one more, it could only match the target by
|
|
145
162
|
* falling back to its optimum, unchecked).
|
|
146
163
|
*/
|
|
147
|
-
function measureLooseParagraph(rawBlock, blockIdx, columnWidth, ctx, styleOverride, extraLines, trackingLadder, looseOutcome) {
|
|
164
|
+
function measureLooseParagraph(rawBlock, blockIdx, columnWidth, ctx, styleOverride, extraLines, trackingLadder, looseOutcome, offGrid) {
|
|
148
165
|
const base = measureContentBlock(rawBlock, blockIdx, columnWidth, ctx, { styleOverride });
|
|
149
166
|
if (!base)
|
|
150
167
|
return null;
|
|
@@ -161,25 +178,39 @@ function measureLooseParagraph(rawBlock, blockIdx, columnWidth, ctx, styleOverri
|
|
|
161
178
|
// A paragraph mostly in a joining script (Arabic…) is not tracked: its
|
|
162
179
|
// words take no letter-spacing (`measure/joining.ts`), so only the first
|
|
163
180
|
// rung, word spacing alone, can gain it the line.
|
|
164
|
-
|
|
181
|
+
// On a character grid (#632) a paragraph the CJK composer sets keeps
|
|
182
|
+
// every character in its one-em cell: no tracking is added to its
|
|
183
|
+
// letters, and a line it breaks short may spread between its characters
|
|
184
|
+
// no more than `maxTracking` (on a grid of whole cells that refuses any
|
|
185
|
+
// line a character short, whose spread is an em over the line's gaps).
|
|
186
|
+
const onGrid = ctx.resolved.cjk.grid.enabled && composesAsCjk(rawBlock.text);
|
|
187
|
+
const ladder = trackingLadder.length > 1 && (onGrid || mostlyJoiningScript(rawBlock.text)) ? trackingLadder.slice(0, 1) : trackingLadder;
|
|
188
|
+
const gridCap = onGrid ? { gridTrackingEm: ctx.resolved.headings.balancing.maxTracking / 1000 } : {};
|
|
165
189
|
const maxWordSpacing = ctx.resolved.bodyText.maxWordSpacing + 1e-9;
|
|
166
190
|
// A CJK line spread past its tracking cap (`cjkLoose`) is past the
|
|
167
191
|
// limit too.
|
|
168
192
|
const withinLimit = (lines) => lines.every((l) => l.isLastLine || ((l.justifiedSpaceRatio === undefined || l.justifiedSpaceRatio <= maxWordSpacing) && !l.cjkLoose));
|
|
193
|
+
const gains = (loose) => loose !== null
|
|
194
|
+
&& loose.measured.lines.length === target
|
|
195
|
+
&& !loose.measured.lastLineRunt
|
|
196
|
+
&& withinLimit(loose.measured.lines);
|
|
169
197
|
for (const tracking of ladder) {
|
|
170
198
|
const loose = measureContentBlock(rawBlock, blockIdx, columnWidth, ctx, {
|
|
171
199
|
styleOverride,
|
|
172
200
|
looseness,
|
|
173
201
|
trackingEm: tracking > 0 ? tracking / 1000 : undefined,
|
|
202
|
+
...gridCap,
|
|
174
203
|
});
|
|
175
|
-
if (loose
|
|
176
|
-
&& loose.measured.lines.length === target
|
|
177
|
-
&& !loose.measured.lastLineRunt
|
|
178
|
-
&& withinLimit(loose.measured.lines)) {
|
|
204
|
+
if (gains(loose)) {
|
|
179
205
|
looseOutcome.set(blockIdx, tracking);
|
|
180
206
|
return loose;
|
|
181
207
|
}
|
|
182
208
|
}
|
|
209
|
+
// Refused for the grid's sake alone: the paragraph would gain its line
|
|
210
|
+
// off the grid. Recorded, so the column says why it ends short.
|
|
211
|
+
if (onGrid && offGrid && gains(measureContentBlock(rawBlock, blockIdx, columnWidth, ctx, { styleOverride, looseness }))) {
|
|
212
|
+
offGrid.add(blockIdx);
|
|
213
|
+
}
|
|
183
214
|
looseOutcome.set(blockIdx, null);
|
|
184
215
|
return base;
|
|
185
216
|
}
|
|
@@ -216,7 +247,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
216
247
|
}
|
|
217
248
|
}
|
|
218
249
|
function placeDocumentPass(content, config, cache, options, hints) {
|
|
219
|
-
const { balanceExtraPx, balanceLooseness, balanceLooseBudget, bandCaps, captionUnder, floatPages } = hints;
|
|
250
|
+
const { balanceExtraPx, balanceLooseness, balanceLooseBudget, bandCaps, captionUnder, floatPages, citingPageFloats } = hints;
|
|
220
251
|
const captionUnderProposals = new Set();
|
|
221
252
|
/** Side columns whose foot a figure's side caption has cut, by the figure. */
|
|
222
253
|
const asideCutBy = new WeakMap();
|
|
@@ -225,6 +256,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
225
256
|
* shortened it, so what does not fit it may still fit a full column. */
|
|
226
257
|
const shortSideColumn = (page, col) => col.bbox.height < page.contentArea.height - 0.5;
|
|
227
258
|
const resolved = resolveAllConfig(config);
|
|
259
|
+
// Bitmaps sized by the document's `layout.bitmapResolution` carry it as
|
|
260
|
+
// their own `resolution` from here on (#631); under the default policy
|
|
261
|
+
// the content is untouched.
|
|
262
|
+
const stampedResources = content.resources && withBitmapResolutions(content.resources, resolved.layout.bitmapResolution);
|
|
263
|
+
if (stampedResources && stampedResources !== content.resources)
|
|
264
|
+
content = { ...content, resources: stampedResources };
|
|
228
265
|
// Tracking rungs a loose paragraph may climb: none, then 5‰ steps up to
|
|
229
266
|
// the cap (`headings.balancing.maxTracking`, thousandths of an em), the
|
|
230
267
|
// cap itself always included. Only the smallest rung that gains the line
|
|
@@ -237,6 +274,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
237
274
|
trackingLadder.push(balancingCfg.maxTracking);
|
|
238
275
|
}
|
|
239
276
|
const looseOutcome = new Map();
|
|
277
|
+
const looseOffGrid = new Set();
|
|
240
278
|
// Lines gained so far per column budget group (see `LooseBudget`).
|
|
241
279
|
const looseGained = new Map();
|
|
242
280
|
// Level configs with heading-style overrides merged in (`{style="…"}`).
|
|
@@ -390,7 +428,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
390
428
|
// An inline `::resource` placed `here` with `span: 'page'` spans the
|
|
391
429
|
// page where it stands (#535): each goes in a frameless page-span box,
|
|
392
430
|
// set by the span-block path like any other.
|
|
393
|
-
|
|
431
|
+
// A `:::columns` group in the running text (#634): each in a frameless
|
|
432
|
+
// box the callout machinery places, cuts and splits.
|
|
433
|
+
const flowGroups = resolved.layout.flowColumns ? wrapFlowColumns(numberedBlocks) : { blocks: numberedBlocks, wrapped: false };
|
|
434
|
+
const spanEmbeds = wrapPageSpanEmbeds(flowGroups.blocks, content.resources ?? [], effectiveResourceTypes(config, content.resources ?? []));
|
|
394
435
|
// Code listings (#624): each in a box of the code style, which the
|
|
395
436
|
// callout machinery places and splits.
|
|
396
437
|
const codeBoxes = wrapCodeBlocks(spanEmbeds.blocks, resolved);
|
|
@@ -435,13 +476,18 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
435
476
|
const syntheticBoxes = [
|
|
436
477
|
...(spanEmbeds.wrapped ? [spanEmbedStyle(bodyStyle.lineHeightPx, resolved.layout.inlineResourceGap !== 'above')] : []),
|
|
437
478
|
...(codeBoxes.wrapped ? [codeBoxStyle(resolved)] : []),
|
|
479
|
+
// The boxes of main-flow groups: sub-columns a body line apart in the
|
|
480
|
+
// text column, a gutter apart across the page.
|
|
481
|
+
...(flowGroups.wrapped
|
|
482
|
+
? flowColumnsStyles(bodyStyle.lineHeightPx, dimensionToPx(resolved.layout.gutterWidth, resolved.page.dpi, bodyStyle.fontSizePx), resolved.bodyText.paragraphSpacing === false ? { value: 0, unit: 'px' } : { value: bodyStyle.marginBottomPx, unit: 'px' }, Math.max(resolved.bodyText.avoidOrphans ? resolved.bodyText.orphanMinLines : 1, resolved.bodyText.avoidWidows ? resolved.bodyText.widowMinLines : 1))
|
|
483
|
+
: []),
|
|
438
484
|
];
|
|
439
485
|
const calloutStyles = syntheticBoxes.length > 0
|
|
440
486
|
? [...resolved.calloutStyles, ...resolveCalloutStylesConfig(syntheticBoxes, resolved.bodyText, resolved.headings, resolved.unorderedLists, config?.locale)]
|
|
441
487
|
: resolved.calloutStyles;
|
|
442
488
|
// A box nested in another looks its style up in the configuration it is
|
|
443
489
|
// laid out with: the listings' box among them.
|
|
444
|
-
const calloutResolved = codeBoxes.wrapped ? { ...resolved, calloutStyles } : resolved;
|
|
490
|
+
const calloutResolved = codeBoxes.wrapped || flowGroups.wrapped ? { ...resolved, calloutStyles } : resolved;
|
|
445
491
|
const blockquoteStyle = resolveBlockquoteStyle(resolved);
|
|
446
492
|
const listLevelIndentsPx = computeLevelIndentsPx(resolved, bodyStyle.fontSizePx);
|
|
447
493
|
const orderedMetrics = computeOrderedListRunMetrics(contentBlocks, resolved, bodyStyle.fontSizePx);
|
|
@@ -472,7 +518,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
472
518
|
// it asks for is not applied there. Decided per block, by the writing
|
|
473
519
|
// mode of the styled section the reference sits in (a horizontal
|
|
474
520
|
// appendix of a vertical book turns its figures as asked).
|
|
475
|
-
const floatPlan = computeFloatPlan(contentBlocks, resources, resourceTypes, incorporated, (blockIdx) => sectionWritingMode(sectionPlan, resolved, blockIdx) === 'vertical-rl');
|
|
521
|
+
const floatPlan = computeFloatPlan(contentBlocks, resources, resourceTypes, incorporated, (blockIdx) => sectionWritingMode(sectionPlan, resolved, blockIdx) === 'vertical-rl', resolved.layout.floatShrink, resolved.layout.wrap, resolved.layout.floatsAtCitingPage);
|
|
476
522
|
const floatedIds = floatedResourceIds(floatPlan, incorporated, resources, resourceTypes);
|
|
477
523
|
const floatsByFirstBlock = new Map();
|
|
478
524
|
for (const f of floatPlan) {
|
|
@@ -489,6 +535,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
489
535
|
* page: they take the side column of the next page the flow opens, in
|
|
490
536
|
* order (see `placeCalloutSide`). */
|
|
491
537
|
const pendingSideBoxes = [];
|
|
538
|
+
/** The flow segment has ended and its floats and side boxes are taking
|
|
539
|
+
* pages after the text (see `drainPendingFloats`, #639). */
|
|
540
|
+
let drainingAfterText = false;
|
|
492
541
|
/** `span: 'side'` boxes of a style with `sideAtColumnEnd: 'after'`
|
|
493
542
|
* (EF-161), and the side boxes fenced behind them, waiting for the text
|
|
494
543
|
* after their fence to land (see `settleAwaitingSideBoxes`). Each keeps
|
|
@@ -835,7 +884,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
835
884
|
/** Pages whose head or foot a floated box took (see the gallery rule in
|
|
836
885
|
* `placeFloatInColumns`). */
|
|
837
886
|
const calloutBandPages = new Set();
|
|
838
|
-
const enqueueCalloutFloat = (startIdx, plan, style, L, placement, span
|
|
887
|
+
const enqueueCalloutFloat = (startIdx, plan, style, L, placement, span,
|
|
888
|
+
/** Text wraps round the box (#627): a one-column float at its side. */
|
|
889
|
+
wrap) => {
|
|
839
890
|
const key = `${CALLOUT_FLOAT_PREFIX}${startIdx}`;
|
|
840
891
|
const columns = plan.attrs.columns !== undefined ? floatColumnCount(plan.attrs.columns) : style.columns;
|
|
841
892
|
// A keep-with-next replay passes the fence again: enqueue once.
|
|
@@ -851,6 +902,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
851
902
|
span: span === 'page' ? 'page' : 'column',
|
|
852
903
|
...(span === 'column' && columns > 1 ? { columns } : {}),
|
|
853
904
|
callout: { startIdx },
|
|
905
|
+
...(wrap && span === 'column' && columns === 1
|
|
906
|
+
? { widthFraction: wrap.width, align: wrap.side, wrap: wrap.side, ...(wrap.gap ? { wrapGap: wrap.gap } : {}) }
|
|
907
|
+
: {}),
|
|
854
908
|
...(col ? { refPageIndex: page.index, refY: col.bbox.y + (col.bbox.height - col.availableHeight) } : {}),
|
|
855
909
|
});
|
|
856
910
|
}
|
|
@@ -869,8 +923,6 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
869
923
|
};
|
|
870
924
|
const floatGapPx = bodyStyle.lineHeightPx;
|
|
871
925
|
const minTextPx = bodyStyle.lineHeightPx * 3;
|
|
872
|
-
/** Fewest body rows the closing slice of a split table carries. */
|
|
873
|
-
const MIN_TAIL_ROWS = 3;
|
|
874
926
|
/** Offset a resolved resource block's caption/table geometry from
|
|
875
927
|
* block-relative to absolute page coordinates (mirrors inline placement). */
|
|
876
928
|
const offsetResourceBlockToAbsolute = (rb, ox, oy) => {
|
|
@@ -918,7 +970,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
918
970
|
};
|
|
919
971
|
const rotationKey = (rotated) => rotated ? `:${rotated.direction}${rotated.length.toFixed(2)}${rotated.upright ? 'u' : ''}` : '';
|
|
920
972
|
const asideKey = (a) => (a ? `:aside${a.dx.toFixed(1)}x${a.width.toFixed(1)}${a.alignBottom ? 'b' : 't'}${(a.offsetY ?? 0).toFixed(1)}` : '');
|
|
921
|
-
const
|
|
973
|
+
const capKey = (cap) => (cap ? `:cap${cap.maxBodyHeight.toFixed(2)}@${cap.minScale}` : '');
|
|
974
|
+
const layoutFloat = (resourceId, width, slice, rotated, aside, cap) => {
|
|
922
975
|
const resource = resourceById.get(resourceId);
|
|
923
976
|
if (!resource)
|
|
924
977
|
return null;
|
|
@@ -939,6 +992,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
939
992
|
...(rotated ? (rotated.upright ? { upright: { maxLength: rotated.length } } : { rotate: rotated.direction, rotatedLength: rotated.length }) : {}),
|
|
940
993
|
...(aside ? { captionAside: aside } : {}),
|
|
941
994
|
...(flexPx > 0 ? { bodyHeightDelta: flexPx } : {}),
|
|
995
|
+
...(cap && !rotated ? { maxBodyHeight: cap.maxBodyHeight, minBodyScale: cap.minScale } : {}),
|
|
942
996
|
});
|
|
943
997
|
};
|
|
944
998
|
/** The rotation of a pending float on a band whose columns keep `avail`
|
|
@@ -989,8 +1043,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
989
1043
|
/** Height (and caption baseline) of a float at a given width, memoised —
|
|
990
1044
|
* fit checks run for every pending float on every loop iteration. */
|
|
991
1045
|
const floatMeasureMemo = new Map();
|
|
992
|
-
const measureFloat = (resourceId, width, slice, rotated, aside) => {
|
|
993
|
-
const key = `${resourceId}:${width.toFixed(2)}${sliceKey(slice)}${rotationKey(rotated)}${asideKey(aside)}`;
|
|
1046
|
+
const measureFloat = (resourceId, width, slice, rotated, aside, cap) => {
|
|
1047
|
+
const key = `${resourceId}:${width.toFixed(2)}${sliceKey(slice)}${rotationKey(rotated)}${asideKey(aside)}${capKey(cap)}`;
|
|
994
1048
|
const memo = floatMeasureMemo.get(key);
|
|
995
1049
|
if (memo !== undefined)
|
|
996
1050
|
return memo;
|
|
@@ -1024,7 +1078,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1024
1078
|
floatMeasureMemo.set(key, mc);
|
|
1025
1079
|
return mc;
|
|
1026
1080
|
}
|
|
1027
|
-
const laid = layoutFloat(resourceId, width, slice, rotated, aside);
|
|
1081
|
+
const laid = layoutFloat(resourceId, width, slice, rotated, aside, cap);
|
|
1028
1082
|
let m = null;
|
|
1029
1083
|
if (laid && laid.block.rotation) {
|
|
1030
1084
|
// A rotated block takes its band whole: no caption baseline to align,
|
|
@@ -1045,10 +1099,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1045
1099
|
if (lastBaseline === undefined || ln.baseline > lastBaseline)
|
|
1046
1100
|
lastBaseline = ln.baseline;
|
|
1047
1101
|
}
|
|
1102
|
+
const picture = rb.kind === 'bitmap' || rb.kind === 'svg' || rb.kind === 'video';
|
|
1048
1103
|
m = {
|
|
1049
1104
|
height: laid.totalHeight,
|
|
1050
1105
|
...(lastBaseline !== undefined ? { lastCaptionBaseline: lastBaseline } : {}),
|
|
1051
1106
|
...(laid.asideHeight !== undefined ? { asideHeight: laid.asideHeight } : {}),
|
|
1107
|
+
...(picture ? { body: { height: rb.bodyRect.height, scale: rb.shrinkScale ?? 1 } } : {}),
|
|
1052
1108
|
};
|
|
1053
1109
|
}
|
|
1054
1110
|
floatMeasureMemo.set(key, m);
|
|
@@ -1064,7 +1120,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1064
1120
|
flushEnd = false, aside,
|
|
1065
1121
|
/** The page is a verso of mirrored margins (a floated box's `'outer'`
|
|
1066
1122
|
* corner icon hangs on the left there). */
|
|
1067
|
-
mirrored = false
|
|
1123
|
+
mirrored = false,
|
|
1124
|
+
/** A picture scaled to its slot (`placement.shrink`). */
|
|
1125
|
+
cap) => {
|
|
1068
1126
|
const strip = comicStripOf(resourceId);
|
|
1069
1127
|
if (strip) {
|
|
1070
1128
|
const block = buildComicStripBlock(strip.idx, strip.source, x, width, contentArea.height - 2 * floatGapPx, cursor.pageIndex);
|
|
@@ -1077,7 +1135,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1077
1135
|
calloutFloatResults.set(result.frame, { result, startIdx, plan: cf.plan });
|
|
1078
1136
|
return { block: result.frame, height: result.totalHeight };
|
|
1079
1137
|
}
|
|
1080
|
-
const laid = layoutFloat(resourceId, width, slice, rotated, aside);
|
|
1138
|
+
const laid = layoutFloat(resourceId, width, slice, rotated, aside, cap);
|
|
1081
1139
|
if (!laid)
|
|
1082
1140
|
return null;
|
|
1083
1141
|
const { block: rb, totalHeight } = laid;
|
|
@@ -1116,6 +1174,52 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1116
1174
|
/** Pages a `:::pagebreak{center}` opened (#424). */
|
|
1117
1175
|
const centredPages = new Set();
|
|
1118
1176
|
const reservedOf = (col) => floatReserved.get(col) ?? { top: 0, bottom: 0 };
|
|
1177
|
+
/** Text wrap (#627): floats set in a band at the head or foot of one
|
|
1178
|
+
* column that text may run beside, per column. The band stays a band
|
|
1179
|
+
* until running text comes to take it back (`openFloatWraps`). */
|
|
1180
|
+
/** The room a wrapped item takes from the lines beside it (#627): its
|
|
1181
|
+
* width and gap, in whole characters of the body under a CJK character
|
|
1182
|
+
* grid (`cjk.grid`), so the lines beside it keep to the grid. */
|
|
1183
|
+
const wrapExclusionWidth = (columnWidth, itemWidth, gap) => {
|
|
1184
|
+
const w = itemWidth + gap;
|
|
1185
|
+
const em = bodyStyle.fontSizePx;
|
|
1186
|
+
return Math.min(columnWidth, resolved.cjk.grid.enabled && em > 0 ? Math.ceil((w - 0.01) / em) * em : w);
|
|
1187
|
+
};
|
|
1188
|
+
/** The `textWrap` warnings of this pass (#627). */
|
|
1189
|
+
const textWrapNotes = [];
|
|
1190
|
+
const floatWraps = new Map();
|
|
1191
|
+
/** Give the bands of the wrapped floats of `col` back to the flow, as
|
|
1192
|
+
* exclusions the text runs beside: a float at the head of a column the
|
|
1193
|
+
* text has not entered yet, one at its foot the text has not reached. */
|
|
1194
|
+
const openFloatWraps = (col) => {
|
|
1195
|
+
const list = floatWraps.get(col);
|
|
1196
|
+
if (!list)
|
|
1197
|
+
return;
|
|
1198
|
+
floatWraps.delete(col);
|
|
1199
|
+
for (const w of list) {
|
|
1200
|
+
const r = { ...reservedOf(col) };
|
|
1201
|
+
if (w.position === 'top') {
|
|
1202
|
+
// A block already stands under it, or another band between: it
|
|
1203
|
+
// keeps its band.
|
|
1204
|
+
if (col.blocks.length > 0 || Math.abs(w.ex.y + w.need - col.bbox.y) > 0.5)
|
|
1205
|
+
continue;
|
|
1206
|
+
col.bbox.y -= w.need;
|
|
1207
|
+
col.bbox.height += w.need;
|
|
1208
|
+
col.availableHeight += w.need;
|
|
1209
|
+
r.top = Math.max(0, r.top - w.need);
|
|
1210
|
+
openColumnWrap(col, w.ex, 'top');
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
if (uncappedBottoms.has(col) || Math.abs(col.bbox.y + col.bbox.height - w.ex.y) > 0.5 || columnCursorY(col) > w.ex.y + 0.01)
|
|
1214
|
+
continue;
|
|
1215
|
+
col.bbox.height += w.need;
|
|
1216
|
+
col.availableHeight += w.need;
|
|
1217
|
+
r.bottom = Math.max(0, r.bottom - w.need);
|
|
1218
|
+
openColumnWrap(col, w.ex, 'bottom');
|
|
1219
|
+
}
|
|
1220
|
+
floatReserved.set(col, r);
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1119
1223
|
const floatHeadOf = (col) => reservedOf(col).top;
|
|
1120
1224
|
/** `BandCap.headPx` for a cap proposed on `cols`: how far the float bands
|
|
1121
1225
|
* every column holds at its head put the band's top below its bare one. */
|
|
@@ -1177,28 +1281,6 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1177
1281
|
// A top band rounds up to the grid: the tallest float it holds within
|
|
1178
1282
|
// `avail` is the grid multiple below it, less the gap.
|
|
1179
1283
|
const hMax = Math.floor((avail + 0.01) / baselineGrid) * baselineGrid - floatGapPx;
|
|
1180
|
-
let end = planTableSlice(metrics, startRow, (rotated ? width : hMax) - overhead);
|
|
1181
|
-
const floor = firstBody + 1;
|
|
1182
|
-
if (end < floor) {
|
|
1183
|
-
// Nothing fits. A fresh page carries the smallest slice anyway (it
|
|
1184
|
-
// overflows, as a dominating figure would) rather than stall the
|
|
1185
|
-
// queue; a slot of the current page is simply not this table's.
|
|
1186
|
-
if (mode === 'strict')
|
|
1187
|
-
return 'none';
|
|
1188
|
-
end = floor;
|
|
1189
|
-
}
|
|
1190
|
-
// A last page holding a row or two under a repeated header reads as a
|
|
1191
|
-
// stranded tail: give the closing slice at least `MIN_TAIL_ROWS` rows by
|
|
1192
|
-
// handing some back from this one (at a breakable edge, not under a
|
|
1193
|
-
// group head), when this slice can spare them.
|
|
1194
|
-
const tail = rowCount - end;
|
|
1195
|
-
if (overflow === 'split' && tail > 0 && tail < MIN_TAIL_ROWS) {
|
|
1196
|
-
let e = rowCount - MIN_TAIL_ROWS;
|
|
1197
|
-
while (e > floor && (!(metrics.breakableAfter[e - 1] ?? true) || metrics.groupHeaderRow[e - 1]))
|
|
1198
|
-
e--;
|
|
1199
|
-
if (e >= floor && e >= end - MIN_TAIL_ROWS)
|
|
1200
|
-
end = e;
|
|
1201
|
-
}
|
|
1202
1284
|
const fits = (slice) => {
|
|
1203
1285
|
const measure = measureFloat(f.resourceId, width, slice, rotated);
|
|
1204
1286
|
if (!measure)
|
|
@@ -1208,23 +1290,18 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1208
1290
|
const { need } = measureFloatBand(position, measure, targetCols, contentArea, baselineGrid, floatGapPx, (c) => trueBottom(c, uncappedBottoms));
|
|
1209
1291
|
return need <= avail + 0.01;
|
|
1210
1292
|
};
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1293
|
+
// Nothing fits: a fresh page carries the smallest slice anyway (it
|
|
1294
|
+
// overflows, as a dominating figure would) rather than stall the
|
|
1295
|
+
// queue; a slot of the current page is simply not this table's.
|
|
1296
|
+
const slice = chooseTableSlice(metrics, startRow, (rotated ? width : hMax) - overhead, {
|
|
1297
|
+
floor: firstBody + 1,
|
|
1298
|
+
force: mode === 'fresh',
|
|
1299
|
+
split: overflow === 'split',
|
|
1300
|
+
fits,
|
|
1215
1301
|
});
|
|
1216
|
-
|
|
1217
|
-
// The caption may wrap differently with its suffix, the closing slice
|
|
1218
|
-
// carries the note instead of the marker: verify, backing off a row at
|
|
1219
|
-
// a time (over breakable edges) when the band still overflows.
|
|
1220
|
-
for (let guard = 0; guard < 8 && end > floor && !fits(slice); guard++) {
|
|
1221
|
-
do
|
|
1222
|
-
end--;
|
|
1223
|
-
while (end > floor && !(metrics.breakableAfter[end - 1] ?? true));
|
|
1224
|
-
slice = sliceFor(end);
|
|
1225
|
-
}
|
|
1226
|
-
if (mode === 'strict' && !fits(slice))
|
|
1302
|
+
if (!slice)
|
|
1227
1303
|
return 'none';
|
|
1304
|
+
const end = slice.endRow;
|
|
1228
1305
|
const rest = slice.continues ? { ...f, startRow: end } : undefined;
|
|
1229
1306
|
return rest ? { slice, rest } : { slice };
|
|
1230
1307
|
};
|
|
@@ -1369,6 +1446,50 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1369
1446
|
built.block.contentIndex = anchorIdx;
|
|
1370
1447
|
(page.floats ??= []).push(built.block);
|
|
1371
1448
|
};
|
|
1449
|
+
/**
|
|
1450
|
+
* A picture float scaled to `room` px of a slot instead of moving on
|
|
1451
|
+
* (`placement.shrink`, #626): the tallest body that keeps its band
|
|
1452
|
+
* within the room. A top band rounds up to the grid, so the float may
|
|
1453
|
+
* stand as tall as the grid multiple within the room, less its gap; a
|
|
1454
|
+
* bottom band (its caption's last baseline on the grid) and a side
|
|
1455
|
+
* stack give the room back px for px. The caption's height may change
|
|
1456
|
+
* with the picture's width (`captionMeasure: 'body'`) and a safe area
|
|
1457
|
+
* crops before it scales, so the cap is refined over a few rounds. The
|
|
1458
|
+
* scale never goes below the float's `minScale`; `fits` is false when
|
|
1459
|
+
* even that does not hold the room. Null for a float that does not
|
|
1460
|
+
* scale (no `shrink`, not an upright picture, the rest of a table) or
|
|
1461
|
+
* already fits.
|
|
1462
|
+
*/
|
|
1463
|
+
const shrinkToRoom = (f, width, aside, full, room, topBand, bandOf) => {
|
|
1464
|
+
if (!f.shrink || !full.body || f.rotate || f.startRow)
|
|
1465
|
+
return null;
|
|
1466
|
+
const minScale = f.minScale ?? DEFAULT_FLOAT_MIN_SCALE;
|
|
1467
|
+
const excessOf = (m, band) => topBand
|
|
1468
|
+
? m.height + floatGapBelow(m, floatGapPx) - Math.floor((room + 0.01) / baselineGrid) * baselineGrid
|
|
1469
|
+
: band.need - room;
|
|
1470
|
+
let measure = full;
|
|
1471
|
+
let band = bandOf(full);
|
|
1472
|
+
let cap;
|
|
1473
|
+
for (let round = 0; round < 4; round++) {
|
|
1474
|
+
const excess = excessOf(measure, band);
|
|
1475
|
+
if (excess <= 0.01)
|
|
1476
|
+
break;
|
|
1477
|
+
const next = { maxBodyHeight: Math.max(0, measure.body.height - excess), minScale };
|
|
1478
|
+
if (cap && next.maxBodyHeight >= cap.maxBodyHeight - 0.01)
|
|
1479
|
+
break;
|
|
1480
|
+
cap = next;
|
|
1481
|
+
const m = measureFloat(f.resourceId, width, undefined, undefined, aside, cap);
|
|
1482
|
+
if (!m?.body)
|
|
1483
|
+
return null;
|
|
1484
|
+
measure = m;
|
|
1485
|
+
band = bandOf(m);
|
|
1486
|
+
if (m.body.scale <= minScale + 1e-6)
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
if (!cap)
|
|
1490
|
+
return null;
|
|
1491
|
+
return { measure, ...band, cap, fits: excessOf(measure, band) <= 0.01 };
|
|
1492
|
+
};
|
|
1372
1493
|
const placeFloatInColumns = (page, f, targetCols, position, pageSpan, mode, anchorToCap = false, side = false, refY) => {
|
|
1373
1494
|
const first = targetCols[0];
|
|
1374
1495
|
const probe = probeFloatBand(page, f, targetCols, position, pageSpan, anchorToCap, side, refY);
|
|
@@ -1377,37 +1498,78 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1377
1498
|
const { width, xLeft, rotated, aside, sideCol } = probe;
|
|
1378
1499
|
let { slice, measure, need, y } = probe;
|
|
1379
1500
|
let rest;
|
|
1501
|
+
/** A picture scaled to this slot (`placement.shrink`, #626). */
|
|
1502
|
+
let cap;
|
|
1503
|
+
const bandOf = (m) => measureFloatBand(position, m, targetCols, page.contentArea, baselineGrid, floatGapPx, (c) => (anchorToCap ? c.bbox.y + c.bbox.height : trueBottom(c, uncappedBottoms)));
|
|
1380
1504
|
/** The float was cut to this slot (a table slice). */
|
|
1381
1505
|
let cut = false;
|
|
1382
1506
|
/** A rotated block wider than the band (its upright height). */
|
|
1383
1507
|
const tooWide = (m) => m.rotatedWidth !== undefined && m.rotatedWidth > width + 0.01;
|
|
1508
|
+
/** The columns of the slot that hold a page-span opener and nothing
|
|
1509
|
+
* after it (#639), with the y of their head under the opener's band:
|
|
1510
|
+
* there the band is taken from the flow under the opener — the column
|
|
1511
|
+
* keeps its box, the opener stands in it — where an empty column's
|
|
1512
|
+
* head moves down under the band. */
|
|
1513
|
+
const openerHeads = new Map();
|
|
1514
|
+
if (position === 'top' && mode === 'strict' && !pageSpan && !side) {
|
|
1515
|
+
for (const c of targetCols) {
|
|
1516
|
+
const top = openerHeadOf(c);
|
|
1517
|
+
if (top !== undefined)
|
|
1518
|
+
openerHeads.set(c, top);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
/** What such a column loses above the band: from the flow's position
|
|
1522
|
+
* under the opener down to the grid line the band starts on. */
|
|
1523
|
+
const leadOf = (c) => {
|
|
1524
|
+
const top = openerHeads.get(c);
|
|
1525
|
+
return top === undefined ? 0 : Math.max(0, top - columnCursorY(c));
|
|
1526
|
+
};
|
|
1384
1527
|
if (side) {
|
|
1385
1528
|
// The side column's stack: the float goes under what the column holds
|
|
1386
1529
|
// when the rest of the column takes it; otherwise it waits for the
|
|
1387
1530
|
// side column of the next page — where it is set anyway, overflowing,
|
|
1388
1531
|
// when even an empty column cannot hold it (a dominating figure).
|
|
1532
|
+
let cap;
|
|
1389
1533
|
if (need > first.availableHeight + 0.01) {
|
|
1390
1534
|
// A column that already holds something — a float, a box, or a
|
|
1391
1535
|
// heading design standing in it — is no empty column to overflow.
|
|
1392
1536
|
const holds = sideUsedBottom(first) > first.bbox.y + 0.5 || (sideObstacles.get(first)?.length ?? 0) > 0;
|
|
1393
|
-
if (mode === 'strict' || holds)
|
|
1394
|
-
return 'defer';
|
|
1395
1537
|
// An empty column shortened by a band above it (a page-span box,
|
|
1396
1538
|
// a top float) is no measure of the float: when a full column
|
|
1397
1539
|
// would hold it, it waits for one instead of overflowing this one.
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
//
|
|
1401
|
-
//
|
|
1402
|
-
|
|
1403
|
-
if
|
|
1404
|
-
|
|
1540
|
+
const waitsForFull = shortSideColumn(page, first) && measure.height <= page.contentArea.height + 0.01;
|
|
1541
|
+
const lastResort = mode === 'fresh' && !holds && !waitsForFull;
|
|
1542
|
+
// A picture scaled to the room the column keeps (#626): any slot
|
|
1543
|
+
// with `'slot'`, when it holds it at its smallest scale or more;
|
|
1544
|
+
// the empty column it would overflow with `'page'` too, at its
|
|
1545
|
+
// smallest scale if need be.
|
|
1546
|
+
if (f.shrink === 'slot' || (f.shrink && lastResort)) {
|
|
1547
|
+
const s = shrinkToRoom(f, width, undefined, measure, first.availableHeight, false, (m) => measureSideStack(m, first, refY, page.contentArea, baselineGrid, floatGapPx, sideObstacles.get(first)));
|
|
1548
|
+
if (s && (s.fits || lastResort))
|
|
1549
|
+
({ measure, need, y, cap } = s);
|
|
1550
|
+
}
|
|
1551
|
+
if (need > first.availableHeight + 0.01) {
|
|
1552
|
+
if (!lastResort)
|
|
1553
|
+
return 'defer';
|
|
1554
|
+
// Overflowing a column a side caption has shortened: the next pass
|
|
1555
|
+
// sets that caption under its figure and gives the column back.
|
|
1556
|
+
const cutBy = asideCutBy.get(first);
|
|
1557
|
+
if (cutBy !== undefined)
|
|
1558
|
+
captionUnderProposals.add(cutBy);
|
|
1559
|
+
}
|
|
1405
1560
|
}
|
|
1406
|
-
const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated);
|
|
1561
|
+
const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated, false, undefined, false, cap);
|
|
1407
1562
|
if (!built)
|
|
1408
1563
|
return 'skip';
|
|
1409
1564
|
first.availableHeight = Math.max(0, first.availableHeight - need);
|
|
1410
1565
|
commitFloatBlock(page, first, built, xLeft, y, width, f.firstBlockIdx);
|
|
1566
|
+
if (drainingAfterText && !calloutFloatOf(f.resourceId)) {
|
|
1567
|
+
const citing = contentBlocks[f.firstBlockIdx];
|
|
1568
|
+
setAfterText.set(built.block, {
|
|
1569
|
+
resourceId: f.resourceId,
|
|
1570
|
+
...(citing ? { sourceStart: citing.sourceStart + bodyOffset, sourceEnd: citing.sourceEnd + bodyOffset } : {}),
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1411
1573
|
floatsPlaced++;
|
|
1412
1574
|
return 'placed';
|
|
1413
1575
|
}
|
|
@@ -1431,9 +1593,26 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1431
1593
|
const gallery = need <= minAvail + 0.01
|
|
1432
1594
|
&& f.refPageIndex !== undefined && f.refPageIndex < page.index
|
|
1433
1595
|
&& calloutBandPages.has(page) && !calloutFloatOf(f.resourceId);
|
|
1434
|
-
if (
|
|
1435
|
-
|
|
1436
|
-
|
|
1596
|
+
if (gallery) {
|
|
1597
|
+
galleryFill = true;
|
|
1598
|
+
}
|
|
1599
|
+
else {
|
|
1600
|
+
// A picture that scales to any slot (#626) takes the room left
|
|
1601
|
+
// beside the other band, the text room kept, when it holds it
|
|
1602
|
+
// at its smallest scale or more.
|
|
1603
|
+
const s = f.shrink === 'slot' ? shrinkToRoom(f, width, aside, measure, minAvail - minTextPx, position === 'top', bandOf) : null;
|
|
1604
|
+
if (!s?.fits)
|
|
1605
|
+
return 'defer';
|
|
1606
|
+
({ measure, need, y, cap } = s);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
// Dominating the band: a picture that scales (`'page'` or `'slot'`,
|
|
1610
|
+
// #626) takes the band's room, at its smallest scale if need be —
|
|
1611
|
+
// then it runs past the band, as any dominating float does.
|
|
1612
|
+
if (need > minAvail + 0.01 && f.shrink && !rotated) {
|
|
1613
|
+
const s = shrinkToRoom(f, width, aside, measure, minAvail, position === 'top', bandOf);
|
|
1614
|
+
if (s)
|
|
1615
|
+
({ measure, need, y, cap } = s);
|
|
1437
1616
|
}
|
|
1438
1617
|
if (need > minAvail + 0.01 || tooWide(measure)) {
|
|
1439
1618
|
// Dominating the band: a table is cut to it (a rotated table, to
|
|
@@ -1459,6 +1638,19 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1459
1638
|
// too wide.
|
|
1460
1639
|
if (f.rotate || tooWide(measure))
|
|
1461
1640
|
return 'defer';
|
|
1641
|
+
// A picture that scales to any slot (#626) takes this one when it
|
|
1642
|
+
// holds it at its smallest scale or more (the text room kept next
|
|
1643
|
+
// to another band, as `fitsStrict` keeps it); the loop below checks
|
|
1644
|
+
// the band as for any float.
|
|
1645
|
+
if (f.shrink === 'slot' && measure.body && !rotated) {
|
|
1646
|
+
const room = Math.min(...targetCols.map((c) => c.availableHeight - leadOf(c) - (columnHasFloatBand(page, c) ? minTextPx : 0)));
|
|
1647
|
+
if (need > room + 0.01) {
|
|
1648
|
+
const s = shrinkToRoom(f, width, aside, measure, room, position === 'top', bandOf);
|
|
1649
|
+
if (!s?.fits)
|
|
1650
|
+
return 'defer';
|
|
1651
|
+
({ measure, need, y, cap } = s);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1462
1654
|
for (const c of targetCols) {
|
|
1463
1655
|
if (position === 'bottom' && uncappedBottoms.has(c) && !anchorToCap) {
|
|
1464
1656
|
// Trailing cap: the band must lie entirely below the level cut.
|
|
@@ -1467,7 +1659,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1467
1659
|
continue;
|
|
1468
1660
|
}
|
|
1469
1661
|
const hasBand = columnHasFloatBand(page, c);
|
|
1470
|
-
if (fitsStrict(need, c, hasBand, minTextPx))
|
|
1662
|
+
if (fitsStrict(need + leadOf(c), c, hasBand, minTextPx))
|
|
1471
1663
|
continue;
|
|
1472
1664
|
// The head of an empty column: a splittable table is cut to the
|
|
1473
1665
|
// column and continues in the next slot — the compositor sets a
|
|
@@ -1516,9 +1708,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1516
1708
|
// An upright figure of a vertical page stands at the head of its tier
|
|
1517
1709
|
// (`placement.align`: `left` the top, `center`, `right` the foot).
|
|
1518
1710
|
const flush = rotated?.upright ? (f.align === 'center' ? 0.5 : f.align === 'right' ? 1 : 0) : rotated ? rotatedFlushEnd(page) : false;
|
|
1519
|
-
const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated, flush, aside, mirroredOf(page));
|
|
1711
|
+
const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated, flush, aside, mirroredOf(page), cap);
|
|
1520
1712
|
if (!built)
|
|
1521
1713
|
return 'skip';
|
|
1714
|
+
// Under an opener in its own column the band starts at that column's
|
|
1715
|
+
// head, the grid line the heads beside it stand on.
|
|
1716
|
+
y = openerHeads.get(first) ?? y;
|
|
1522
1717
|
// The caption beside the figure takes its band of the side column: a
|
|
1523
1718
|
// top float's caption is consumed from the stack's head (when the
|
|
1524
1719
|
// stack is still above it), a bottom float's cuts the column's foot.
|
|
@@ -1538,7 +1733,34 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1538
1733
|
asideCutBy.set(sideCol, f.resourceId);
|
|
1539
1734
|
}
|
|
1540
1735
|
}
|
|
1736
|
+
// Text wrap (#627): a one-column float at the head or foot of its
|
|
1737
|
+
// column, which running text may take back to run beside it — unless
|
|
1738
|
+
// the text left beside it would be too narrow, or the float too short
|
|
1739
|
+
// for lines beside it.
|
|
1740
|
+
const wrapGap = wrapGapPx(f.wrapGap, dpi, bodyStyle.fontSizePx, floatGapPx);
|
|
1741
|
+
const wraps = f.wrap !== undefined && !pageSpan && !side && targetCols.length === 1 && !rotated && !aside && !cut && !galleryFill
|
|
1742
|
+
&& !(position === 'bottom' && uncappedBottoms.has(first));
|
|
1743
|
+
const wrapRefused = !wraps ? undefined
|
|
1744
|
+
: first.bbox.width - width - wrapGap + 0.01 < minTextWidthPx(resolved.layout.wrap.minTextWidth, first.bbox.width, dpi, bodyStyle.fontSizePx)
|
|
1745
|
+
? 'tooNarrow'
|
|
1746
|
+
: Math.round(need / bodyStyle.lineHeightPx) < resolved.layout.wrap.minLinesBeside ? 'fewLines' : undefined;
|
|
1541
1747
|
for (const col of targetCols) {
|
|
1748
|
+
if (openerHeads.has(col)) {
|
|
1749
|
+
// The band is taken from the flow under the opener: the column
|
|
1750
|
+
// keeps its box (a trailing cap measures the band from the opener's
|
|
1751
|
+
// top in every column) and records where the band ends, which is
|
|
1752
|
+
// where its rule starts (`columnRuleSegments`). A float text wraps
|
|
1753
|
+
// round records nothing: the text placed next takes its band back
|
|
1754
|
+
// (see below) and the rule runs beside both.
|
|
1755
|
+
col.availableHeight = Math.max(0, col.availableHeight - leadOf(col) - need);
|
|
1756
|
+
if (!(wraps && !wrapRefused))
|
|
1757
|
+
col.headFloatFoot = columnCursorY(col);
|
|
1758
|
+
openerHeadFloats.set(col, columnCursorY(col));
|
|
1759
|
+
// What the opener left under it went into the band's gap.
|
|
1760
|
+
pendingSpacing = 0;
|
|
1761
|
+
inlineGapOwed = null;
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1542
1764
|
const r = { ...reservedOf(col) };
|
|
1543
1765
|
if (position === 'top') {
|
|
1544
1766
|
col.bbox.y += need;
|
|
@@ -1571,6 +1793,36 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1571
1793
|
if (galleryFill)
|
|
1572
1794
|
for (const col of targetCols)
|
|
1573
1795
|
col.availableHeight = 0;
|
|
1796
|
+
if (wraps && f.wrap) {
|
|
1797
|
+
if (wrapRefused) {
|
|
1798
|
+
textWrapNotes.push({ kind: 'textWrap', reason: wrapRefused, resourceId: f.resourceId, pageIndex: page.index });
|
|
1799
|
+
}
|
|
1800
|
+
else {
|
|
1801
|
+
const exWidth = wrapExclusionWidth(first.bbox.width, width, wrapGap);
|
|
1802
|
+
const underOpener = openerHeads.has(first);
|
|
1803
|
+
const exY = underOpener ? y : position === 'top' ? first.bbox.y - need : first.bbox.y + first.bbox.height;
|
|
1804
|
+
const ex = {
|
|
1805
|
+
x: f.wrap === 'left' ? first.bbox.x : first.bbox.x + first.bbox.width - exWidth,
|
|
1806
|
+
y: exY,
|
|
1807
|
+
width: exWidth,
|
|
1808
|
+
height: need,
|
|
1809
|
+
side: f.wrap,
|
|
1810
|
+
gap: exWidth - width,
|
|
1811
|
+
ownerId: built.block.id,
|
|
1812
|
+
};
|
|
1813
|
+
if (underOpener) {
|
|
1814
|
+
// The band was taken from the flow: the text placed next takes
|
|
1815
|
+
// it back and runs beside the float, as it does beside an inline
|
|
1816
|
+
// figure that wraps (`pendingWrap`).
|
|
1817
|
+
pendingWrap = { col: first, blocks: first.blocks.length, cursorY: columnCursorY(first), ex };
|
|
1818
|
+
}
|
|
1819
|
+
else {
|
|
1820
|
+
const list = floatWraps.get(first) ?? [];
|
|
1821
|
+
list.push({ position, need, ex });
|
|
1822
|
+
floatWraps.set(first, list);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1574
1826
|
commitFloatBlock(page, first, built, xLeft, y, width, f.firstBlockIdx);
|
|
1575
1827
|
floatsPlaced++;
|
|
1576
1828
|
return rest ? { rest } : 'placed';
|
|
@@ -1618,7 +1870,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1618
1870
|
* column float of its sequence still gets the page's foot once that one
|
|
1619
1871
|
* is set. A float that does not fit holds up the ones behind it in its
|
|
1620
1872
|
* numbering sequence (see `heldBack`), never the other sequence. */
|
|
1621
|
-
const
|
|
1873
|
+
const flushPendingFloatsIntoPage = (page) => {
|
|
1622
1874
|
if (pendingFloats.length === 0) {
|
|
1623
1875
|
flushSideBoxesIntoPage(page);
|
|
1624
1876
|
return;
|
|
@@ -1706,6 +1958,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1706
1958
|
i++;
|
|
1707
1959
|
continue;
|
|
1708
1960
|
}
|
|
1961
|
+
// After the text a side float keeps behind the side boxes fenced
|
|
1962
|
+
// before its citation that still wait (#639).
|
|
1963
|
+
if (isSide && drainingAfterText && pendingSideBoxes.some((b) => b.startIdx < f.firstBlockIdx)) {
|
|
1964
|
+
i++;
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1709
1967
|
// A page-span rest never shares the page of its previous slice.
|
|
1710
1968
|
if (isPageSpan && f.notBefore?.pageIndex === page.index) {
|
|
1711
1969
|
i++;
|
|
@@ -1823,7 +2081,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1823
2081
|
// refuse (a head-of-page float offered the foot of the current page);
|
|
1824
2082
|
// floated callouts keep their placement.
|
|
1825
2083
|
const asked = anyPosition && !f.rotate && !f.callout && f.position !== 'auto' ? { ...f, position: 'auto' } : f;
|
|
1826
|
-
let slots = enumerateCurrentPageSlots(page, cursor.columnIndex, asked, capKindOf);
|
|
2084
|
+
let slots = enumerateCurrentPageSlots(page, cursor.columnIndex, asked, capKindOf, openerHeadOf);
|
|
1827
2085
|
// The rest of a table cut on this page only takes the slots after
|
|
1828
2086
|
// its previous slice in reading order (never the foot of the column
|
|
1829
2087
|
// before it; a page-span rest waits for the next page).
|
|
@@ -1860,6 +2118,242 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1860
2118
|
i = settle(i, r);
|
|
1861
2119
|
}
|
|
1862
2120
|
};
|
|
2121
|
+
// --- Floats at the head of the page that cites them (#633) ---------------
|
|
2122
|
+
// A float that may head its citing page (`placement.citingPage`) is placed
|
|
2123
|
+
// as any other in the first pass; when its citing line lands on a page (a
|
|
2124
|
+
// column) whose head it would fit while the float went on to a later one,
|
|
2125
|
+
// the pass proposes that head (`settleCitingPageFloats`), and the next
|
|
2126
|
+
// pass opens the page with the float there, before any text, the text
|
|
2127
|
+
// above the reference moving down under it. The driver keeps the slot
|
|
2128
|
+
// while the citing line stays on that page (that column).
|
|
2129
|
+
/** The floats `PassHints.citingPageFloats` sets at the head of each page,
|
|
2130
|
+
* in first-reference order (the order of their numbers). */
|
|
2131
|
+
const citingHeadsByPage = new Map();
|
|
2132
|
+
if (citingPageFloats && citingPageFloats.size > 0) {
|
|
2133
|
+
for (const f of floatPlan) {
|
|
2134
|
+
const slot = citingPageFloats.get(f.resourceId);
|
|
2135
|
+
if (!slot || !f.citingPage)
|
|
2136
|
+
continue;
|
|
2137
|
+
const list = citingHeadsByPage.get(slot.pageIndex);
|
|
2138
|
+
if (list)
|
|
2139
|
+
list.push(f);
|
|
2140
|
+
else
|
|
2141
|
+
citingHeadsByPage.set(slot.pageIndex, [f]);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
/** The floats this pass set at the head of their citing page. */
|
|
2145
|
+
const citingHeadsSet = new Set();
|
|
2146
|
+
const floatPlanOrder = new Map(floatPlan.map((f, i) => [f.resourceId, i]));
|
|
2147
|
+
/** Whether every float of `f`'s numbering sequence first cited before it
|
|
2148
|
+
* is set already: figure 4 never heads a page while figure 3 waits. */
|
|
2149
|
+
const sequenceSetBefore = (f) => {
|
|
2150
|
+
const typeId = resourceById.get(f.resourceId)?.typeId;
|
|
2151
|
+
const at = floatPlanOrder.get(f.resourceId) ?? 0;
|
|
2152
|
+
for (let i = 0; i < at; i++) {
|
|
2153
|
+
const g = floatPlan[i];
|
|
2154
|
+
if (resourceById.get(g.resourceId)?.typeId !== typeId)
|
|
2155
|
+
continue;
|
|
2156
|
+
if (!enqueuedFloatIds.has(g.resourceId) || pendingFloats.some((p) => p.resourceId === g.resourceId))
|
|
2157
|
+
return false;
|
|
2158
|
+
}
|
|
2159
|
+
return true;
|
|
2160
|
+
};
|
|
2161
|
+
/** The text columns of `page`'s first band (no span block above them). */
|
|
2162
|
+
const firstBandTextColumns = (page) => page.columns.filter((c) => c.kind !== 'span' && c.kind !== 'side' && (c.band ?? 0) === 0);
|
|
2163
|
+
/** Whether a float takes the page's whole width on `page` (as
|
|
2164
|
+
* `flushFloatsIntoPage` reads it). */
|
|
2165
|
+
const spansPageOn = (page, f, textCols) => {
|
|
2166
|
+
const across = Math.min(f.columns ?? 1, textCols.length);
|
|
2167
|
+
return (f.span === 'page' || (across > 1 && across === textCols.length)) && (textCols.length > 1 || sideColumns(page).length > 0);
|
|
2168
|
+
};
|
|
2169
|
+
/** The columns a column float heading its citing column `columnIndex`
|
|
2170
|
+
* takes: that column, or the run of `columns` from it (ending at the
|
|
2171
|
+
* band's last column when fewer follow it). */
|
|
2172
|
+
const citingColumnsOf = (f, textCols, columnIndex) => {
|
|
2173
|
+
const across = Math.min(f.columns ?? 1, textCols.length);
|
|
2174
|
+
const at = textCols.findIndex((c) => c.index === columnIndex);
|
|
2175
|
+
if (at < 0)
|
|
2176
|
+
return undefined;
|
|
2177
|
+
const from = Math.max(0, Math.min(at, textCols.length - across));
|
|
2178
|
+
return textCols.slice(from, from + across);
|
|
2179
|
+
};
|
|
2180
|
+
/** Set the floats that head `page` (`PassHints.citingPageFloats`), after
|
|
2181
|
+
* the floats the page owes to earlier pages (cited before them, read
|
|
2182
|
+
* before them): each where its citing line lands, when its sequence is
|
|
2183
|
+
* set up to it and it fits within `layout.maxTopFraction` of the column,
|
|
2184
|
+
* the text room kept under it. One that does not is placed as usual. */
|
|
2185
|
+
const placeCitingHeads = (page) => {
|
|
2186
|
+
const heads = citingHeadsByPage.get(page.index);
|
|
2187
|
+
if (!heads)
|
|
2188
|
+
return;
|
|
2189
|
+
if (opensAfterBreak(doc.pages, page.index, forcedBreakPages) || page.openerBand || page.partInfo)
|
|
2190
|
+
return;
|
|
2191
|
+
const textCols = firstBandTextColumns(page);
|
|
2192
|
+
if (textCols.length === 0 || textCols.some((c) => c.blocks.length > 0))
|
|
2193
|
+
return;
|
|
2194
|
+
const maxTop = resolved.layout.maxTopFraction;
|
|
2195
|
+
for (const planned of heads) {
|
|
2196
|
+
const id = planned.resourceId;
|
|
2197
|
+
if (citingHeadsSet.has(id))
|
|
2198
|
+
continue;
|
|
2199
|
+
const at = pendingFloats.findIndex((p) => p.resourceId === id);
|
|
2200
|
+
// Placed already, or queued with its citing line placed (on an
|
|
2201
|
+
// earlier page): the usual slots are its.
|
|
2202
|
+
if (at >= 0 ? !awaitsCitation(pendingFloats[at]) : enqueuedFloatIds.has(id))
|
|
2203
|
+
continue;
|
|
2204
|
+
if (!sequenceSetBefore(planned))
|
|
2205
|
+
continue;
|
|
2206
|
+
const pageSpan = spansPageOn(page, planned, textCols);
|
|
2207
|
+
const cols = pageSpan ? [...textCols, ...sideColumns(page)] : citingColumnsOf(planned, textCols, citingPageFloats.get(id).columnIndex);
|
|
2208
|
+
if (!cols)
|
|
2209
|
+
continue;
|
|
2210
|
+
const f = { ...planned, refPageIndex: page.index };
|
|
2211
|
+
const probe = probeFloatBand(page, f, cols, 'top', pageSpan, false);
|
|
2212
|
+
if (!probe)
|
|
2213
|
+
continue;
|
|
2214
|
+
const fits = cols.filter((c) => c.kind !== 'side').every((c) => {
|
|
2215
|
+
const r = reservedOf(c);
|
|
2216
|
+
const full = c.bbox.height + r.top + r.bottom;
|
|
2217
|
+
return r.top + probe.need <= maxTop * full + 0.01 && probe.need <= c.availableHeight - minTextPx + 0.01;
|
|
2218
|
+
});
|
|
2219
|
+
if (!fits)
|
|
2220
|
+
continue;
|
|
2221
|
+
// A page opened for a block that cannot split keeps its room.
|
|
2222
|
+
if (freshPageHold && cols.includes(freshPageHold.col)
|
|
2223
|
+
&& availableAfterBand(freshPageHold.col, 'top', probe, false) < freshPageHold.px - FIT_EPS)
|
|
2224
|
+
continue;
|
|
2225
|
+
const r = placeFloatInColumns(page, f, cols, 'top', pageSpan, 'strict');
|
|
2226
|
+
if (r === 'defer' || r === 'skip')
|
|
2227
|
+
continue;
|
|
2228
|
+
enqueuedFloatIds.add(id);
|
|
2229
|
+
citationGates.delete(id);
|
|
2230
|
+
citingHeadsSet.add(id);
|
|
2231
|
+
if (at >= 0)
|
|
2232
|
+
pendingFloats.splice(at, 1);
|
|
2233
|
+
// A table cut to the head after all goes on as any rest does.
|
|
2234
|
+
if (typeof r === 'object')
|
|
2235
|
+
pendingFloats.push(r.rest);
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
/**
|
|
2239
|
+
* After the pass: the floats that may head their citing page and went on
|
|
2240
|
+
* to a later page (or column), with the head their citing line's page
|
|
2241
|
+
* (column) offers — proposals for the next pass — and the floats the
|
|
2242
|
+
* hints asked to head a page this pass did not keep there. A float is
|
|
2243
|
+
* proposed when its citing line lands in the first band of a page that
|
|
2244
|
+
* no explicit break opened (no opener, part or `:::pagebreak` above it),
|
|
2245
|
+
* as one slice (not a table split across pages), when the earlier floats
|
|
2246
|
+
* of its sequence went no later than that page, and when it fits within
|
|
2247
|
+
* `layout.maxTopFraction` of the column.
|
|
2248
|
+
*/
|
|
2249
|
+
const settleCitingPageFloats = () => {
|
|
2250
|
+
const proposals = new Map();
|
|
2251
|
+
const refused = new Map();
|
|
2252
|
+
const candidates = floatPlan.filter((f) => f.citingPage);
|
|
2253
|
+
if (candidates.length === 0)
|
|
2254
|
+
return { proposals, refused };
|
|
2255
|
+
const byId = new Map(candidates.map((f) => [f.resourceId, f]));
|
|
2256
|
+
// Where each one's citation landed: the first line that cites it, or
|
|
2257
|
+
// for a `::resource` directive the first line placed after it.
|
|
2258
|
+
const cited = new Map();
|
|
2259
|
+
const directives = candidates.filter((f) => contentBlocks[f.firstBlockIdx]?.type === 'resourceBlock');
|
|
2260
|
+
for (const page of doc.pages) {
|
|
2261
|
+
for (const col of page.columns) {
|
|
2262
|
+
for (const b of col.blocks) {
|
|
2263
|
+
for (const f of directives) {
|
|
2264
|
+
if (!cited.has(f.resourceId) && b.contentIndex !== undefined && b.contentIndex > f.firstBlockIdx && b.lines.length > 0) {
|
|
2265
|
+
cited.set(f.resourceId, { pageIndex: page.index, col });
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
for (const l of b.lines) {
|
|
2269
|
+
for (const seg of l.segments ?? []) {
|
|
2270
|
+
const id = seg.refResourceId;
|
|
2271
|
+
if (id !== undefined && byId.has(id) && !cited.has(id))
|
|
2272
|
+
cited.set(id, { pageIndex: page.index, col });
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
const landed = floatPagesOf(doc);
|
|
2279
|
+
const landedColumn = new Map();
|
|
2280
|
+
for (const page of doc.pages) {
|
|
2281
|
+
for (const fb of page.floats ?? []) {
|
|
2282
|
+
const id = fb.resourceBlock?.resource.id;
|
|
2283
|
+
if (id === undefined || !byId.has(id))
|
|
2284
|
+
continue;
|
|
2285
|
+
const l = landedColumn.get(id);
|
|
2286
|
+
if (l)
|
|
2287
|
+
l.slices++;
|
|
2288
|
+
else
|
|
2289
|
+
landedColumn.set(id, { columnIndex: fb.columnIndex ?? 0, slices: 1 });
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
const positionIn = (cols, index) => cols.findIndex((c) => c.index === index);
|
|
2293
|
+
for (const f of candidates) {
|
|
2294
|
+
const id = f.resourceId;
|
|
2295
|
+
const c = cited.get(id);
|
|
2296
|
+
const slot = citingPageFloats?.get(id);
|
|
2297
|
+
if (slot) {
|
|
2298
|
+
// Hinted: kept when it was set at the head and its citing line is
|
|
2299
|
+
// still under it, on that page, in a column it heads.
|
|
2300
|
+
if (!citingHeadsSet.has(id)) {
|
|
2301
|
+
refused.set(id, 'notSet');
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
const page = doc.pages[slot.pageIndex];
|
|
2305
|
+
let kept = !!page && c !== undefined && c.pageIndex === slot.pageIndex;
|
|
2306
|
+
if (kept && page) {
|
|
2307
|
+
const textCols = firstBandTextColumns(page);
|
|
2308
|
+
if (!spansPageOn(page, f, textCols)) {
|
|
2309
|
+
const cols = citingColumnsOf(f, textCols, slot.columnIndex);
|
|
2310
|
+
kept = cols !== undefined && cols.includes(c.col);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
if (!kept)
|
|
2314
|
+
refused.set(id, 'citationMoved');
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
const l = landedColumn.get(id);
|
|
2318
|
+
const landedPage = landed.get(id);
|
|
2319
|
+
if (!c || !l || landedPage === undefined || l.slices > 1)
|
|
2320
|
+
continue;
|
|
2321
|
+
const page = doc.pages[c.pageIndex];
|
|
2322
|
+
if (c.col.kind === 'span' || c.col.kind === 'side' || (c.col.band ?? 0) !== 0)
|
|
2323
|
+
continue;
|
|
2324
|
+
if (opensAfterBreak(doc.pages, c.pageIndex, forcedBreakPages) || page.openerBand || page.partInfo)
|
|
2325
|
+
continue;
|
|
2326
|
+
const textCols = firstBandTextColumns(page);
|
|
2327
|
+
const pageSpan = spansPageOn(page, f, textCols);
|
|
2328
|
+
const later = landedPage > c.pageIndex
|
|
2329
|
+
|| (!pageSpan && landedPage === c.pageIndex && positionIn(textCols, l.columnIndex) > positionIn(textCols, c.col.index));
|
|
2330
|
+
if (!later)
|
|
2331
|
+
continue;
|
|
2332
|
+
// The earlier floats of its sequence went no later than that page.
|
|
2333
|
+
const typeId = resourceById.get(id)?.typeId;
|
|
2334
|
+
const order = floatPlanOrder.get(id) ?? 0;
|
|
2335
|
+
if (floatPlan.slice(0, order).some((g) => resourceById.get(g.resourceId)?.typeId === typeId && (landed.get(g.resourceId) ?? Infinity) > c.pageIndex))
|
|
2336
|
+
continue;
|
|
2337
|
+
// It fits the head within `layout.maxTopFraction` of the column.
|
|
2338
|
+
const cols = pageSpan ? textCols : citingColumnsOf(f, textCols, c.col.index);
|
|
2339
|
+
if (!cols || cols.length === 0)
|
|
2340
|
+
continue;
|
|
2341
|
+
const slotWidth = pageSpan ? page.contentArea.width : cols[cols.length - 1].bbox.x + cols[cols.length - 1].bbox.width - cols[0].bbox.x;
|
|
2342
|
+
const width = f.widthFraction && f.widthFraction < 1 ? slotWidth * f.widthFraction : slotWidth;
|
|
2343
|
+
const m = measureFloat(id, width);
|
|
2344
|
+
if (!m || m.height + floatGapPx > resolved.layout.maxTopFraction * page.contentArea.height + 0.01)
|
|
2345
|
+
continue;
|
|
2346
|
+
proposals.set(id, { pageIndex: c.pageIndex, columnIndex: c.col.index });
|
|
2347
|
+
}
|
|
2348
|
+
return { proposals, refused };
|
|
2349
|
+
};
|
|
2350
|
+
/** Reserve the floats a freshly opened page owes: those waiting for a
|
|
2351
|
+
* page's bands, then those that head the page citing them (#633). */
|
|
2352
|
+
const flushFloatsIntoPage = (page) => {
|
|
2353
|
+
flushPendingFloatsIntoPage(page);
|
|
2354
|
+
if (citingHeadsByPage.size > 0)
|
|
2355
|
+
placeCitingHeads(page);
|
|
2356
|
+
};
|
|
1863
2357
|
/** Reserve floats on each freshly opened content page. Passed only to the
|
|
1864
2358
|
* content-flow column advances — parity / force-blank pages never get it. */
|
|
1865
2359
|
const onNewPage = (page) => flushFloatsIntoPage(page);
|
|
@@ -1908,15 +2402,28 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1908
2402
|
// its closing columns — whatever position it asked for: a chapter's
|
|
1909
2403
|
// last page with room under its text beats a page holding one table.
|
|
1910
2404
|
tryPlacePendingFloatsOnCurrentPage(false, undefined, true);
|
|
2405
|
+
// So does a waiting side box, for room in a side column of the pages
|
|
2406
|
+
// the flow opened after its fence.
|
|
2407
|
+
settlePendingSideBoxes();
|
|
1911
2408
|
let guard = 0;
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2409
|
+
drainingAfterText = true;
|
|
2410
|
+
try {
|
|
2411
|
+
// A page that sets a side box counts as progress like one that sets
|
|
2412
|
+
// a float (#639): up to postext 1.24 the loop stopped after the first
|
|
2413
|
+
// page of boxes and the rest of the queue was never set.
|
|
2414
|
+
while ((pendingFloats.length > 0 || pendingSideBoxes.length > 0) && guard++ < 1000) {
|
|
2415
|
+
const before = floatsPlaced;
|
|
2416
|
+
const boxesBefore = pendingSideBoxes.length;
|
|
2417
|
+
const startPageIndex = cursor.pageIndex;
|
|
2418
|
+
do {
|
|
2419
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2420
|
+
} while (cursor.pageIndex === startPageIndex);
|
|
2421
|
+
if (floatsPlaced === before && pendingSideBoxes.length === boxesBefore)
|
|
2422
|
+
break; // safety: no progress
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
finally {
|
|
2426
|
+
drainingAfterText = false;
|
|
1920
2427
|
}
|
|
1921
2428
|
};
|
|
1922
2429
|
/** Boxes that left the flow this pass (side column, float band, fixed),
|
|
@@ -2313,7 +2820,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2313
2820
|
blk.lines = resetLinePositions(raggedLooseLines(m.measured.lines, noteStyle.textAlign), noteStyle.lineHeightPx).map((line) => ({
|
|
2314
2821
|
...line,
|
|
2315
2822
|
bbox: createBoundingBox(line.bbox.x + x, line.bbox.y + y, line.bbox.width, line.bbox.height),
|
|
2316
|
-
...(line.measure ? { measure: { x: line.measure.x + x
|
|
2823
|
+
...(line.measure ? { measure: { ...line.measure, x: line.measure.x + x } } : {}),
|
|
2317
2824
|
baseline: line.baseline + y,
|
|
2318
2825
|
}));
|
|
2319
2826
|
const h = blk.lines.length * noteStyle.lineHeightPx;
|
|
@@ -2510,6 +3017,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2510
3017
|
* level snaps, or would but for the heading after it). */
|
|
2511
3018
|
const reserveOpenerBand = (headingCol, h, opener, pending = 0, gridText = true) => {
|
|
2512
3019
|
const page = doc.pages[cursor.pageIndex];
|
|
3020
|
+
if (opener)
|
|
3021
|
+
openerOwnColumn.set(headingCol, opener);
|
|
2513
3022
|
for (const otherCol of page.columns) {
|
|
2514
3023
|
if (otherCol === headingCol)
|
|
2515
3024
|
continue;
|
|
@@ -2539,6 +3048,47 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2539
3048
|
* the opener, its column, the band's foot, where text right under the
|
|
2540
3049
|
* opener starts and the float band the column held at its head then. */
|
|
2541
3050
|
const openerBandHeads = new Map();
|
|
3051
|
+
/** The column each page-span opener was set in (#639). */
|
|
3052
|
+
const openerOwnColumn = new Map();
|
|
3053
|
+
/** The opener's own columns a float took the head of, under the opener:
|
|
3054
|
+
* where the float's band ends, the flow's position right after it. */
|
|
3055
|
+
const openerHeadFloats = new Map();
|
|
3056
|
+
/**
|
|
3057
|
+
* The head of the opener's own column under its band (#639): the first
|
|
3058
|
+
* grid line at or under the opener, level with the heads the band's other
|
|
3059
|
+
* columns were given (`reserveOpenerBand`), while the column holds nothing
|
|
3060
|
+
* after the opener and the band has another text column beside it — a
|
|
3061
|
+
* single text column under its heading has no other head to be level
|
|
3062
|
+
* with, and keeps the rule every column with content has. `undefined`
|
|
3063
|
+
* for any other column, and under `layout.floatsUnderOpener: false`
|
|
3064
|
+
* (postext 1.24 and earlier: the opener's column offered no head slot,
|
|
3065
|
+
* so a float fenced right after the opener landed from column 2).
|
|
3066
|
+
*/
|
|
3067
|
+
const openerHeadOf = (col) => {
|
|
3068
|
+
if (!resolved.layout.floatsUnderOpener)
|
|
3069
|
+
return undefined;
|
|
3070
|
+
const opener = openerOwnColumn.get(col);
|
|
3071
|
+
if (!opener || col.blocks[col.blocks.length - 1] !== opener)
|
|
3072
|
+
return undefined;
|
|
3073
|
+
const page = doc.pages[opener.pageIndex];
|
|
3074
|
+
if (!page)
|
|
3075
|
+
return undefined;
|
|
3076
|
+
const beside = bandColumns(page, col.band ?? 0).filter((c) => c !== col && c.kind !== 'side' && c.kind !== 'span');
|
|
3077
|
+
if (beside.length === 0)
|
|
3078
|
+
return undefined;
|
|
3079
|
+
return gridUpOnPage(page, columnCursorY(col));
|
|
3080
|
+
};
|
|
3081
|
+
/** Whether the flow in `col` stands right under a float set at the head
|
|
3082
|
+
* of the opener's own column: the block placed next starts there as at
|
|
3083
|
+
* a column's head, with no space above it. */
|
|
3084
|
+
const underOpenerHeadFloat = (col) => {
|
|
3085
|
+
const foot = openerHeadFloats.get(col);
|
|
3086
|
+
return foot !== undefined && col.blocks[col.blocks.length - 1] === openerOwnColumn.get(col)
|
|
3087
|
+
&& Math.abs(columnCursorY(col) - foot) < 0.01;
|
|
3088
|
+
};
|
|
3089
|
+
/** Whether what is placed next in `col` opens it: the column is empty,
|
|
3090
|
+
* or holds an opener and the float set under it at its head. */
|
|
3091
|
+
const atColumnHead = (col) => col.blocks.length === 0 || underOpenerHeadFloat(col);
|
|
2542
3092
|
/**
|
|
2543
3093
|
* The room above the first block of a column headed by an opener band,
|
|
2544
3094
|
* relative to the column's head, or undefined for any other column (or
|
|
@@ -2566,7 +3116,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2566
3116
|
if (!head || col.blocks.length > 0 || reservedOf(col).top !== head.floatTop)
|
|
2567
3117
|
return undefined;
|
|
2568
3118
|
let target = head.textTop;
|
|
2569
|
-
|
|
3119
|
+
// (With a float under the opener in its own column, the first block
|
|
3120
|
+
// there stands under that float: nothing to be level with, #639.)
|
|
3121
|
+
if (level && !openerHeadFloats.has(head.openerCol)) {
|
|
2570
3122
|
const at = head.openerCol.blocks.indexOf(head.opener);
|
|
2571
3123
|
const next = at >= 0 ? head.openerCol.blocks.slice(at + 1).find((b) => !b.hidden) : undefined;
|
|
2572
3124
|
if (next && (next.type === 'heading' || next.type === 'mathDisplay')) {
|
|
@@ -2690,7 +3242,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2690
3242
|
/** `rollbackTrailingBlocks` that also drops a rolled-back heading's
|
|
2691
3243
|
* side-column obstacles: the heading is set again further on. */
|
|
2692
3244
|
const rollbackHeadings = (col) => {
|
|
2693
|
-
const popped = rollbackTrailingBlocks(col, doc.blocks, isFreeHeading);
|
|
3245
|
+
const popped = rollbackTrailingBlocks(col, doc.blocks, (b) => isFreeHeading(b) && !holdsHeadFloat(col, b));
|
|
2694
3246
|
for (const p of popped) {
|
|
2695
3247
|
const side = sideObstacleColumn.get(p);
|
|
2696
3248
|
if (!side)
|
|
@@ -2775,7 +3327,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2775
3327
|
return;
|
|
2776
3328
|
const band = currentBand(page, cursor);
|
|
2777
3329
|
const cols = bandColumns(page, band).filter((c) => c.bbox.height > 0.5);
|
|
2778
|
-
|
|
3330
|
+
// A band where text wraps round something (#627) is not levelled.
|
|
3331
|
+
if (cols.length < 2 || cols.some((c) => c.forcedBreak || (c.exclusions?.length ?? 0) > 0))
|
|
2779
3332
|
return;
|
|
2780
3333
|
if (activeCap && activeCap.spanIndex === boundaryIndex
|
|
2781
3334
|
&& activeCap.pageIndex === page.index && activeCap.band === band) {
|
|
@@ -2822,6 +3375,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2822
3375
|
const floatHeads = cols
|
|
2823
3376
|
.filter((c) => c.blocks.length === 0 && reservedOf(c).top > 0)
|
|
2824
3377
|
.map((c) => Math.ceil((c.bbox.y - top - 0.01) / baselineGrid));
|
|
3378
|
+
// So does one set under a page-span opener at the head of the opener's
|
|
3379
|
+
// own column (#639).
|
|
3380
|
+
for (const c of cols) {
|
|
3381
|
+
if (c.headFloatFoot !== undefined)
|
|
3382
|
+
floatHeads.push(Math.ceil((c.headFloatFoot - top - 0.01) / baselineGrid));
|
|
3383
|
+
}
|
|
2825
3384
|
bandCapProposals.set(boundaryIndex, {
|
|
2826
3385
|
kind: 'trailing',
|
|
2827
3386
|
startContentIndex: bandStart.contentIndex,
|
|
@@ -2845,7 +3404,15 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2845
3404
|
* page the boundary opens: they sit the drain out and stay pending (with
|
|
2846
3405
|
* no reference stamp, which the citing line sets), so they land after
|
|
2847
3406
|
* the line that cites them, in the new segment (EF-69). */
|
|
3407
|
+
/** Text wrap (#627): an inline figure that wraps, placed as a band, whose
|
|
3408
|
+
* band the next block takes back to run beside it when it can (see
|
|
3409
|
+
* `pipeline/textWrap.ts`). */
|
|
3410
|
+
let pendingWrap = null;
|
|
2848
3411
|
const closeFlowSegment = (boundaryIndex) => {
|
|
3412
|
+
// What text wrapped round in the column the segment ends in keeps its
|
|
3413
|
+
// room (#627).
|
|
3414
|
+
pendingWrap = null;
|
|
3415
|
+
settleColumnWraps(currentColumn(doc, cursor));
|
|
2849
3416
|
settleAwaitingSideBoxes(true);
|
|
2850
3417
|
releaseStaleCitationGates();
|
|
2851
3418
|
const ownFloats = [];
|
|
@@ -3000,13 +3567,18 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3000
3567
|
* keep-with-next rollbacks may pull along (a callout is one unbreakable
|
|
3001
3568
|
* unit; its children never leave it). */
|
|
3002
3569
|
const isFreeHeading = (b) => b.type === 'heading' && b.containerId === undefined;
|
|
3570
|
+
/** Whether `b` is the page-span opener of `col` with a float set under it
|
|
3571
|
+
* at the column's head (#639): the float follows it there, so it closes
|
|
3572
|
+
* no run of headings a block moving on could strand, and is never taken
|
|
3573
|
+
* on to the next column with one. */
|
|
3574
|
+
const holdsHeadFloat = (col, b) => openerHeadFloats.has(col) && openerOwnColumn.get(col) === b;
|
|
3003
3575
|
/** Free headings closing `col` (its trailing run), 0 unless keep-with-next
|
|
3004
3576
|
* is on: the headings a block moving on would strand. */
|
|
3005
3577
|
const trailingHeadingRun = (col) => {
|
|
3006
3578
|
if (!resolved.headings.keepWithNext || headingMayCloseColumn(col))
|
|
3007
3579
|
return 0;
|
|
3008
3580
|
let n = 0;
|
|
3009
|
-
for (let j = col.blocks.length - 1; j >= 0 && isFreeHeading(col.blocks[j]); j--)
|
|
3581
|
+
for (let j = col.blocks.length - 1; j >= 0 && isFreeHeading(col.blocks[j]) && !holdsHeadFloat(col, col.blocks[j]); j--)
|
|
3010
3582
|
n++;
|
|
3011
3583
|
return n;
|
|
3012
3584
|
};
|
|
@@ -3027,8 +3599,31 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3027
3599
|
* convert the laid-out box to absolute coordinates at the frame's placed
|
|
3028
3600
|
* origin, and push frame + children — in that order — to `doc.blocks`
|
|
3029
3601
|
* and to the column the frame landed in. */
|
|
3602
|
+
/** `:::columns` groups whose sub-columns are too narrow (#634), once a
|
|
3603
|
+
* group. */
|
|
3604
|
+
const columnsNotes = [];
|
|
3605
|
+
const narrowNoted = new Set();
|
|
3606
|
+
const noteNarrowGroups = (result, pageIndex) => {
|
|
3607
|
+
for (const g of result.groups ?? []) {
|
|
3608
|
+
if (!g.narrow || narrowNoted.has(g.contentIndex))
|
|
3609
|
+
continue;
|
|
3610
|
+
narrowNoted.add(g.contentIndex);
|
|
3611
|
+
const raw = contentBlocks[g.contentIndex];
|
|
3612
|
+
columnsNotes.push({
|
|
3613
|
+
kind: 'columnsTooNarrow',
|
|
3614
|
+
columns: g.columns,
|
|
3615
|
+
widthPx: g.columnWidth,
|
|
3616
|
+
...(raw ? { sourceStart: raw.sourceStart + bodyOffset, sourceEnd: raw.sourceEnd + bodyOffset } : {}),
|
|
3617
|
+
pageIndex,
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
for (const u of result.units)
|
|
3621
|
+
if (u.kind === 'box')
|
|
3622
|
+
noteNarrowGroups(u.box.result, pageIndex);
|
|
3623
|
+
};
|
|
3030
3624
|
const commitCallout = (result, startIdx, plan, col, range) => {
|
|
3031
3625
|
const frame = result.frame;
|
|
3626
|
+
noteNarrowGroups(result, cursor.pageIndex);
|
|
3032
3627
|
stampCalloutSource(frame, startIdx, plan, range);
|
|
3033
3628
|
offsetCalloutToAbsolute(result, frame.bbox.x, frame.bbox.y);
|
|
3034
3629
|
doc.blocks.push(frame);
|
|
@@ -3079,13 +3674,19 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3079
3674
|
});
|
|
3080
3675
|
const layoutRange = (from, to, width, frameId, continuation, mirrored = false) => {
|
|
3081
3676
|
let n = 0;
|
|
3677
|
+
// A fragment that opens inside a `:::columns` group starts at its
|
|
3678
|
+
// opening marker and one that ends inside it at its closing marker:
|
|
3679
|
+
// the group sets the part between the cut's positions (#634).
|
|
3680
|
+
const fromChild = from.group ? from.group.open : from.child;
|
|
3681
|
+
const closeOfGroup = (open) => groups.find(([gs]) => gs === open)?.[1] ?? children.length - 1;
|
|
3082
3682
|
// A cut inside child `to.child` includes that child (its first
|
|
3083
3683
|
// `to.line` lines); a cut at a child's head excludes it.
|
|
3084
|
-
const toChild = to.line > 0 ? to.child + 1 : to.child;
|
|
3684
|
+
const toChild = to.group ? closeOfGroup(to.group.open) + 1 : to.line > 0 ? to.child + 1 : to.child;
|
|
3685
|
+
const inSlice = (pos) => pos.map((p) => ({ ...p, child: p.child - fromChild }));
|
|
3085
3686
|
// Nested boxes still open at `from` (their closing markers leading
|
|
3086
3687
|
// the range close them): the fragment opens inside them.
|
|
3087
3688
|
const open = [];
|
|
3088
|
-
for (let k = 0; k <
|
|
3689
|
+
for (let k = 0; k < fromChild; k++) {
|
|
3089
3690
|
const c = children[k];
|
|
3090
3691
|
if (c.containerName !== 'callout')
|
|
3091
3692
|
continue;
|
|
@@ -3094,7 +3695,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3094
3695
|
else if (c.type === 'containerEnd')
|
|
3095
3696
|
open.pop();
|
|
3096
3697
|
}
|
|
3097
|
-
for (let k =
|
|
3698
|
+
for (let k = fromChild; k < children.length && open.length > 0; k++) {
|
|
3098
3699
|
const c = children[k];
|
|
3099
3700
|
if (c.type !== 'containerEnd' || c.containerId !== open[open.length - 1].block.containerId)
|
|
3100
3701
|
break;
|
|
@@ -3106,19 +3707,25 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3106
3707
|
...(contentBlocks[startIdx]?.direction ? { direction: contentBlocks[startIdx].direction } : {}),
|
|
3107
3708
|
continuation,
|
|
3108
3709
|
// Any range short of the box's end is a fragment that goes on.
|
|
3109
|
-
...(to.child < children.length || to.line > 0 ? { continues: true } : {}),
|
|
3110
|
-
children: children.slice(
|
|
3111
|
-
childStartIdx: startIdx + 1 +
|
|
3710
|
+
...(to.child < children.length || to.line > 0 || to.group ? { continues: true } : {}),
|
|
3711
|
+
children: children.slice(fromChild, toChild),
|
|
3712
|
+
childStartIdx: startIdx + 1 + fromChild,
|
|
3112
3713
|
width,
|
|
3113
|
-
|
|
3714
|
+
// A main-flow group's box sets its blocks as the text around it is
|
|
3715
|
+
// set: a styled section's or a part's body (#634).
|
|
3716
|
+
ctx: isFlowColumnsStyle(style.id)
|
|
3717
|
+
? (partPlan.byBlock[startIdx] ? partMeasureCtx : sectionMeasureCtx(sectionPlan.byBlock[startIdx]))
|
|
3718
|
+
: measureCtx,
|
|
3114
3719
|
resolved: calloutResolved,
|
|
3115
3720
|
containerId: plan.containerId,
|
|
3116
3721
|
frameId,
|
|
3117
3722
|
nextChildId: () => `${frameId}-c${n++}`,
|
|
3118
3723
|
paragraphContainers,
|
|
3119
|
-
...(from.line > 0 ? { lineFrom: from.line } : {}),
|
|
3120
|
-
...(from.line > 0 && from.widths ? { lineWidths: from.widths } : {}),
|
|
3121
|
-
...(to.line > 0 ? { lineTo: to.line } : {}),
|
|
3724
|
+
...(from.line > 0 && !from.group ? { lineFrom: from.line } : {}),
|
|
3725
|
+
...(from.line > 0 && !from.group && from.widths ? { lineWidths: from.widths } : {}),
|
|
3726
|
+
...(to.line > 0 && !to.group ? { lineTo: to.line } : {}),
|
|
3727
|
+
...(from.group ? { groupFrom: inSlice(from.group.pos) } : {}),
|
|
3728
|
+
...(to.group ? { groupTo: inSlice(to.group.pos) } : {}),
|
|
3122
3729
|
...(open.length > 0 ? { openNested: open } : {}),
|
|
3123
3730
|
mirrored,
|
|
3124
3731
|
stripBlock: calloutStripBlock,
|
|
@@ -3252,7 +3859,68 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3252
3859
|
});
|
|
3253
3860
|
};
|
|
3254
3861
|
collect(full.units, minLines, 0);
|
|
3255
|
-
|
|
3862
|
+
// Inside a `:::columns` group of the box (#634): the cut the group
|
|
3863
|
+
// offers for the room, where each of its sub-columns (each stream, in
|
|
3864
|
+
// a parallel group) ends when the galley fills them in turn; at least
|
|
3865
|
+
// one stream goes on and one moves on. A group of a nested box is
|
|
3866
|
+
// never cut, nor any group under 1.24's rules (`layout.flowColumns`).
|
|
3867
|
+
if (resolved.layout.flowColumns) {
|
|
3868
|
+
const base = from.group ? from.group.open : from.child;
|
|
3869
|
+
const childMin = Math.min(Math.max(1, minLines), resolved.layout.boxChildSplitMinLines);
|
|
3870
|
+
for (const g of full.groups ?? []) {
|
|
3871
|
+
const h = roomPx - tail - g.top;
|
|
3872
|
+
if (h <= 0.5)
|
|
3873
|
+
continue;
|
|
3874
|
+
const reopened = from.group?.open === g.open + base ? from.group : undefined;
|
|
3875
|
+
const pos = [];
|
|
3876
|
+
let moved = false;
|
|
3877
|
+
let left = false;
|
|
3878
|
+
let used = 0;
|
|
3879
|
+
g.streams.forEach((items, s) => {
|
|
3880
|
+
const end = { child: (g.ends[s] ?? g.close) + base, line: 0 };
|
|
3881
|
+
if (items.length === 0) {
|
|
3882
|
+
pos.push(end);
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
const breaks = galleyBreaks(items, childMin);
|
|
3886
|
+
const fill = fillGalley(items, breaks, g.flow === 'snake' ? g.columns : 1, h);
|
|
3887
|
+
if (fill.end) {
|
|
3888
|
+
moved = true;
|
|
3889
|
+
used = Math.max(used, fill.used);
|
|
3890
|
+
pos.push(end);
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
left = true;
|
|
3894
|
+
if (fill.cuts.length === 0) {
|
|
3895
|
+
// This stream takes nothing in the fragment: it stays where it is.
|
|
3896
|
+
pos.push(reopened?.pos[s] ?? { child: items[0].child + base, line: 0 });
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
moved = true;
|
|
3900
|
+
used = Math.max(used, fill.used);
|
|
3901
|
+
const b = breaks[fill.cuts[fill.cuts.length - 1]];
|
|
3902
|
+
const it = items[b.item];
|
|
3903
|
+
if (b.line === 0) {
|
|
3904
|
+
pos.push({ child: it.child + base, line: 0 });
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
// The sub-column widths the paragraph's lines are set at.
|
|
3908
|
+
const before = it.lineBase > 0 ? reopened?.pos[s]?.widths : undefined;
|
|
3909
|
+
const lastW = before?.[before.length - 1];
|
|
3910
|
+
const widths = !before || before.length === 0
|
|
3911
|
+
? [{ fromLine: 0, width: g.columnWidth }]
|
|
3912
|
+
: Math.abs(lastW.width - g.columnWidth) <= 0.5 ? before : [...before, { fromLine: it.lineBase, width: g.columnWidth }];
|
|
3913
|
+
pos.push({ child: it.child + base, line: it.lineBase + b.line, widths });
|
|
3914
|
+
});
|
|
3915
|
+
if (!moved || !left)
|
|
3916
|
+
continue;
|
|
3917
|
+
candidates.push({
|
|
3918
|
+
cut: { child: Math.min(...pos.map((p) => p.child)), line: 0, group: { open: g.open + base, pos } },
|
|
3919
|
+
bottom: g.top + used,
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
const insideGroup = (cut) => !cut.group && L.groups.some(([gs, ge]) => (cut.line === 0 ? gs < cut.child && cut.child <= ge : gs < cut.child && cut.child < ge));
|
|
3256
3924
|
const viable = candidates
|
|
3257
3925
|
.filter((c) => !insideGroup(c.cut))
|
|
3258
3926
|
.sort((a, b) => b.bottom - a.bottom);
|
|
@@ -3953,9 +4621,27 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3953
4621
|
const cutBy = asideCutBy.get(side);
|
|
3954
4622
|
if (cutBy !== undefined)
|
|
3955
4623
|
captionUnderProposals.add(cutBy);
|
|
4624
|
+
// Taller than an empty side column: set anyway, and reported (#639).
|
|
4625
|
+
const overflowPx = y - used + result.totalHeight - side.availableHeight;
|
|
4626
|
+
if (overflowPx > 0.5) {
|
|
4627
|
+
(doc.warnings ??= []).push({
|
|
4628
|
+
kind: 'calloutOverflow',
|
|
4629
|
+
pageIndex: page.index,
|
|
4630
|
+
columnIndex: side.index,
|
|
4631
|
+
sourceStart: contentBlocks[startIdx].sourceStart + bodyOffset,
|
|
4632
|
+
sourceEnd: contentBlocks[plan.endIdx].sourceEnd + bodyOffset,
|
|
4633
|
+
overflowPx,
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
3956
4636
|
}
|
|
3957
4637
|
const frame = result.frame;
|
|
3958
4638
|
stampCalloutSource(frame, startIdx, plan);
|
|
4639
|
+
if (drainingAfterText) {
|
|
4640
|
+
setAfterText.set(frame, {
|
|
4641
|
+
...(frame.sourceStart !== undefined ? { sourceStart: frame.sourceStart } : {}),
|
|
4642
|
+
...(frame.sourceEnd !== undefined ? { sourceEnd: frame.sourceEnd } : {}),
|
|
4643
|
+
});
|
|
4644
|
+
}
|
|
3959
4645
|
frame.pageIndex = page.index;
|
|
3960
4646
|
frame.columnIndex = side.index;
|
|
3961
4647
|
offsetCalloutToAbsolute(result, side.bbox.x, y);
|
|
@@ -3995,7 +4681,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3995
4681
|
return true;
|
|
3996
4682
|
}
|
|
3997
4683
|
if (!trySideBox(page, side, { startIdx, plan, style }, refY, 'strict')) {
|
|
3998
|
-
pendingSideBoxes.push({ startIdx, plan, style });
|
|
4684
|
+
pendingSideBoxes.push({ startIdx, plan, style, pageIndex: page.index });
|
|
3999
4685
|
}
|
|
4000
4686
|
return true;
|
|
4001
4687
|
};
|
|
@@ -4064,7 +4750,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4064
4750
|
placed = laterSide !== undefined && trySideBox(later, laterSide, w.box, undefined, 'fresh');
|
|
4065
4751
|
}
|
|
4066
4752
|
if (!placed)
|
|
4067
|
-
pendingSideBoxes.push(w.box);
|
|
4753
|
+
pendingSideBoxes.push({ ...w.box, pageIndex: cursor.pageIndex });
|
|
4068
4754
|
}
|
|
4069
4755
|
};
|
|
4070
4756
|
/** Set the waiting side boxes in the side column of a freshly opened
|
|
@@ -4081,6 +4767,31 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4081
4767
|
pendingSideBoxes.shift();
|
|
4082
4768
|
}
|
|
4083
4769
|
};
|
|
4770
|
+
/** Where the text ends (#639): before a waiting side box takes a page
|
|
4771
|
+
* after it, the side columns of the pages the flow opened after its
|
|
4772
|
+
* fence that still hold it whole, in order (the page of the fence had no
|
|
4773
|
+
* room at or under it). A try that sets nothing gives its block id back,
|
|
4774
|
+
* so the ids after it stay what they were. */
|
|
4775
|
+
const settlePendingSideBoxes = () => {
|
|
4776
|
+
while (pendingSideBoxes.length > 0) {
|
|
4777
|
+
const box = pendingSideBoxes[0];
|
|
4778
|
+
let placed = false;
|
|
4779
|
+
for (let p = box.pageIndex + 1; p <= cursor.pageIndex && !placed; p++) {
|
|
4780
|
+
const page = doc.pages[p];
|
|
4781
|
+
for (const side of sideColumns(page)) {
|
|
4782
|
+
const idBefore = blockIdCounter;
|
|
4783
|
+
if (trySideBox(page, side, box, undefined, 'strict')) {
|
|
4784
|
+
placed = true;
|
|
4785
|
+
break;
|
|
4786
|
+
}
|
|
4787
|
+
blockIdCounter = idBefore;
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
if (!placed)
|
|
4791
|
+
return;
|
|
4792
|
+
pendingSideBoxes.shift();
|
|
4793
|
+
}
|
|
4794
|
+
};
|
|
4084
4795
|
/**
|
|
4085
4796
|
* Place a `:::callout` inline at the current column width as one atomic
|
|
4086
4797
|
* unit: the frame block followed by its children in the same column. The
|
|
@@ -4102,6 +4813,239 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4102
4813
|
* boxes) still fall back to this inline placement for v1 — the frame's
|
|
4103
4814
|
* `callout.placement` records the request.
|
|
4104
4815
|
*/
|
|
4816
|
+
/**
|
|
4817
|
+
* A box set `here` that text wraps round (#627): laid out whole at its
|
|
4818
|
+
* share of the column, at its side, level with the next line's top, then
|
|
4819
|
+
* set as a band the next running text takes back to run beside it (see
|
|
4820
|
+
* `pendingWrap`). It never splits: too tall for the room left, it moves
|
|
4821
|
+
* on to the next column (a `textWrap` warning says so). Frame and
|
|
4822
|
+
* children go to the page's floats, where a floated box goes. Returns
|
|
4823
|
+
* false, the box set in the flow as any, when the text beside it would
|
|
4824
|
+
* be too narrow or the box too short for lines beside it.
|
|
4825
|
+
*/
|
|
4826
|
+
const placeCalloutWrapped = (startIdx, plan, style, L, frameId, wrap) => {
|
|
4827
|
+
const settings = resolved.layout.wrap;
|
|
4828
|
+
const gap = wrapGapPx(wrap.gap, dpi, bodyStyle.fontSizePx, floatGapPx);
|
|
4829
|
+
const note = (reason) => {
|
|
4830
|
+
const start = contentBlocks[startIdx];
|
|
4831
|
+
textWrapNotes.push({ kind: 'textWrap', reason, box: style.id, sourceStart: start.sourceStart + bodyOffset, sourceEnd: start.sourceEnd + bodyOffset, pageIndex: cursor.pageIndex });
|
|
4832
|
+
};
|
|
4833
|
+
let col = currentColumn(doc, cursor);
|
|
4834
|
+
if (col.bbox.width * (1 - wrap.width) - gap + 0.01 < minTextWidthPx(settings.minTextWidth, col.bbox.width, dpi, bodyStyle.fontSizePx)) {
|
|
4835
|
+
note('tooNarrow');
|
|
4836
|
+
return false;
|
|
4837
|
+
}
|
|
4838
|
+
for (let attempt = 0;; attempt++) {
|
|
4839
|
+
col = currentColumn(doc, cursor);
|
|
4840
|
+
const page = doc.pages[cursor.pageIndex];
|
|
4841
|
+
const width = col.bbox.width * wrap.width;
|
|
4842
|
+
const result = L.layoutRange(CUT_START, L.end, width, frameId, false, mirroredOf(page));
|
|
4843
|
+
if (attempt === 0 && Math.ceil((result.totalHeight + gap - 0.01) / bodyStyle.lineHeightPx) < settings.minLinesBeside) {
|
|
4844
|
+
note('fewLines');
|
|
4845
|
+
return false;
|
|
4846
|
+
}
|
|
4847
|
+
const spacing = atColumnHead(col) ? 0 : pendingSpacing;
|
|
4848
|
+
if (result.totalHeight > col.availableHeight - spacing + 0.01 && col.blocks.length > 0 && attempt < 4) {
|
|
4849
|
+
if (attempt === 0)
|
|
4850
|
+
note('moved');
|
|
4851
|
+
pendingSpacing = 0;
|
|
4852
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
4853
|
+
continue;
|
|
4854
|
+
}
|
|
4855
|
+
const top = columnCursorY(col) + spacing;
|
|
4856
|
+
const x = wrap.side === 'left' ? col.bbox.x : col.bbox.x + col.bbox.width - width;
|
|
4857
|
+
const frame = result.frame;
|
|
4858
|
+
stampCalloutSource(frame, startIdx, plan);
|
|
4859
|
+
frame.pageIndex = page.index;
|
|
4860
|
+
frame.columnIndex = col.index;
|
|
4861
|
+
offsetCalloutToAbsolute(result, x, top);
|
|
4862
|
+
const floats = (page.floats ??= []);
|
|
4863
|
+
doc.blocks.push(frame);
|
|
4864
|
+
floats.push(frame);
|
|
4865
|
+
for (const child of result.children) {
|
|
4866
|
+
child.pageIndex = page.index;
|
|
4867
|
+
child.columnIndex = col.index;
|
|
4868
|
+
doc.blocks.push(child);
|
|
4869
|
+
floats.push(child);
|
|
4870
|
+
}
|
|
4871
|
+
const bottomRel = Math.ceil((top - col.bbox.y + result.totalHeight + gap - 0.01) / baselineGrid) * baselineGrid;
|
|
4872
|
+
col.availableHeight = Math.max(0, Math.min(col.availableHeight, col.bbox.height - bottomRel));
|
|
4873
|
+
pendingSpacing = 0;
|
|
4874
|
+
inlineGapOwed = null;
|
|
4875
|
+
const exWidth = wrapExclusionWidth(col.bbox.width, width, gap);
|
|
4876
|
+
pendingWrap = {
|
|
4877
|
+
col,
|
|
4878
|
+
blocks: col.blocks.length,
|
|
4879
|
+
cursorY: columnCursorY(col),
|
|
4880
|
+
ex: {
|
|
4881
|
+
x: wrap.side === 'left' ? col.bbox.x : col.bbox.x + col.bbox.width - exWidth,
|
|
4882
|
+
y: top,
|
|
4883
|
+
width: exWidth,
|
|
4884
|
+
height: col.bbox.y + bottomRel - top,
|
|
4885
|
+
side: wrap.side,
|
|
4886
|
+
gap: exWidth - width,
|
|
4887
|
+
ownerId: frame.id,
|
|
4888
|
+
},
|
|
4889
|
+
};
|
|
4890
|
+
return true;
|
|
4891
|
+
}
|
|
4892
|
+
};
|
|
4893
|
+
/**
|
|
4894
|
+
* An inline table (`::resource` placed `here`) that does not fit the room
|
|
4895
|
+
* left in its column (#634), by its style's `overflow` when
|
|
4896
|
+
* `splitInline` is on: `'split'` cuts it between rows with the rules of a
|
|
4897
|
+
* floated table (`chooseTableSlice`: breakable edges, group heads, the
|
|
4898
|
+
* closing slice's three rows, header repeat, caption suffix, marker,
|
|
4899
|
+
* note on the last slice), the first slice keeping the float gap above
|
|
4900
|
+
* it and at least the header and {@link MIN_HEAD_ROWS} rows (fewer, and
|
|
4901
|
+
* the whole table starts in the next column, as before); every
|
|
4902
|
+
* continuation opens at the head of the next column, laid out at that
|
|
4903
|
+
* column's width, before the text after the table. `'clip'` keeps the
|
|
4904
|
+
* leading rows of a table taller than a column that fit the head of
|
|
4905
|
+
* one, `'hide'` leaves such a table out. Returns `'whole'` when the
|
|
4906
|
+
* table is placed as one block by the caller (it fits, or the rule does
|
|
4907
|
+
* not apply), `'dropped'`, or `'placed'`: every slice is placed and the
|
|
4908
|
+
* cursor sits in the column of the last one, which the caller snaps
|
|
4909
|
+
* under. `blk` is the first slice's block, filled in here.
|
|
4910
|
+
*/
|
|
4911
|
+
const MIN_HEAD_ROWS = 2;
|
|
4912
|
+
const placeInlineTableSlices = (raw, blockIdx, blk, ctx, styleOverride, resource, wholeHeight, spacingBefore) => {
|
|
4913
|
+
const style = pickTableStyle(resolved, resource.table?.styleId);
|
|
4914
|
+
if (!style.splitInline || pageIsVertical(doc.pages[cursor.pageIndex]))
|
|
4915
|
+
return 'whole';
|
|
4916
|
+
const rowCount = resource.table?.model.rows.length ?? 0;
|
|
4917
|
+
const first = currentColumn(doc, cursor);
|
|
4918
|
+
const room = first.availableHeight - (first.blocks.length === 0 ? 0 : spacingBefore);
|
|
4919
|
+
if (wholeHeight <= room + FIT_EPS)
|
|
4920
|
+
return 'whole';
|
|
4921
|
+
const overflow = style.overflow;
|
|
4922
|
+
// `'clip'` and `'hide'` concern a table no column holds; a shorter one
|
|
4923
|
+
// moves whole to the next column, as before.
|
|
4924
|
+
const taller = wholeHeight > contentArea.height + FIT_EPS;
|
|
4925
|
+
if (overflow !== 'split' && !taller)
|
|
4926
|
+
return 'whole';
|
|
4927
|
+
if (overflow === 'hide')
|
|
4928
|
+
return 'dropped';
|
|
4929
|
+
const measureAt = (width, slice) => measureContentBlock(raw, blockIdx, width, ctx, { styleOverride, ...(slice ? { tableSlice: slice } : {}) });
|
|
4930
|
+
const metricsMemo = new Map();
|
|
4931
|
+
const metricsAt = (width) => {
|
|
4932
|
+
const key = width.toFixed(2);
|
|
4933
|
+
let m = metricsMemo.get(key);
|
|
4934
|
+
if (m === undefined)
|
|
4935
|
+
metricsMemo.set(key, (m = measureAt(width)?.tableRows ?? null));
|
|
4936
|
+
return m;
|
|
4937
|
+
};
|
|
4938
|
+
const firstMetrics = metricsAt(first.bbox.width);
|
|
4939
|
+
if (!firstMetrics || rowCount === 0)
|
|
4940
|
+
return 'whole';
|
|
4941
|
+
const headerRows = firstMetrics.headerRowCount;
|
|
4942
|
+
// Too few rows to leave a head and a tail worth the cut.
|
|
4943
|
+
if (overflow === 'split' && rowCount - headerRows < MIN_HEAD_ROWS + MIN_TAIL_ROWS)
|
|
4944
|
+
return 'whole';
|
|
4945
|
+
/** The rows a slice from `startRow` must carry at least, and its height
|
|
4946
|
+
* at `width` (caption and marker included): what a page opened for it
|
|
4947
|
+
* keeps clear of the floats it flushes (EF-160). */
|
|
4948
|
+
const floorOf = (startRow) => (startRow === 0
|
|
4949
|
+
? Math.min(rowCount, headerRows + (overflow === 'split' ? MIN_HEAD_ROWS : 1))
|
|
4950
|
+
: Math.max(startRow, headerRows) + 1);
|
|
4951
|
+
const minHeightAt = (width, startRow) => {
|
|
4952
|
+
const floor = floorOf(startRow);
|
|
4953
|
+
const probe = measureAt(width, { startRow, endRow: floor, continues: floor < rowCount && overflow === 'split' });
|
|
4954
|
+
return probe?.measured.totalHeight ?? 0;
|
|
4955
|
+
};
|
|
4956
|
+
let startRow = 0;
|
|
4957
|
+
let part = 0;
|
|
4958
|
+
let moves = 0;
|
|
4959
|
+
let block = blk;
|
|
4960
|
+
for (;;) {
|
|
4961
|
+
enterBand(blockIdx, part);
|
|
4962
|
+
let col = currentColumn(doc, cursor);
|
|
4963
|
+
if (col.kind === 'span') {
|
|
4964
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(minHeightAt(col.bbox.width, startRow)));
|
|
4965
|
+
continue;
|
|
4966
|
+
}
|
|
4967
|
+
const width = col.bbox.width;
|
|
4968
|
+
const metrics = metricsAt(width) ?? firstMetrics;
|
|
4969
|
+
const spacing = part === 0 && col.blocks.length > 0 ? spacingBefore : 0;
|
|
4970
|
+
const avail = col.availableHeight - spacing;
|
|
4971
|
+
const restSlice = startRow > 0 ? { startRow, endRow: rowCount, continues: false } : undefined;
|
|
4972
|
+
const rest = measureAt(width, restSlice);
|
|
4973
|
+
if (!rest?.resourceBlock)
|
|
4974
|
+
return part === 0 ? 'whole' : 'placed';
|
|
4975
|
+
let placed = rest;
|
|
4976
|
+
let slice = null;
|
|
4977
|
+
if (rest.measured.totalHeight > avail + FIT_EPS) {
|
|
4978
|
+
const atHead = col.blocks.length === 0;
|
|
4979
|
+
// `'clip'` keeps the rows the head of a column holds.
|
|
4980
|
+
if (overflow === 'clip' && !atHead && moves < 4) {
|
|
4981
|
+
moves++;
|
|
4982
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(minHeightAt(width, startRow)));
|
|
4983
|
+
continue;
|
|
4984
|
+
}
|
|
4985
|
+
const firstBody = startRow > 0 ? Math.max(startRow, headerRows) : headerRows;
|
|
4986
|
+
const probe = measureAt(width, { startRow, endRow: firstBody + 1, continues: true });
|
|
4987
|
+
const overhead = probe?.resourceBlock ? probe.measured.totalHeight - probe.resourceBlock.bodyRect.height : 0;
|
|
4988
|
+
// An empty column of full height (or a run of short ones behind)
|
|
4989
|
+
// takes the smallest slice even when it overflows: a fresh page
|
|
4990
|
+
// always progresses.
|
|
4991
|
+
const force = atHead && (col.bbox.height >= contentArea.height - baselineGrid || moves >= 4);
|
|
4992
|
+
slice = chooseTableSlice(metrics, startRow, avail - overhead, {
|
|
4993
|
+
floor: floorOf(startRow),
|
|
4994
|
+
force,
|
|
4995
|
+
split: overflow === 'split',
|
|
4996
|
+
fits: (s) => (measureAt(width, s)?.measured.totalHeight ?? Infinity) <= avail + FIT_EPS,
|
|
4997
|
+
});
|
|
4998
|
+
if (!slice) {
|
|
4999
|
+
// Not even the head rows fit: the (rest of the) table starts in
|
|
5000
|
+
// the next column.
|
|
5001
|
+
moves++;
|
|
5002
|
+
pendingSpacing = 0;
|
|
5003
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(minHeightAt(width, startRow)));
|
|
5004
|
+
continue;
|
|
5005
|
+
}
|
|
5006
|
+
const m = measureAt(width, slice);
|
|
5007
|
+
if (!m?.resourceBlock)
|
|
5008
|
+
return part === 0 ? 'whole' : 'placed';
|
|
5009
|
+
placed = m;
|
|
5010
|
+
}
|
|
5011
|
+
// The slice's block: the first one is the caller's.
|
|
5012
|
+
if (part > 0) {
|
|
5013
|
+
block = createVDTBlock(`block-${blockIdCounter++}`, 'resource', blk.fontString, blk.color, blk.textAlign);
|
|
5014
|
+
block.contentIndex = blockIdx;
|
|
5015
|
+
stampBlockExtras(block, raw);
|
|
5016
|
+
block.dirty = false;
|
|
5017
|
+
block.snappedToGrid = false;
|
|
5018
|
+
block.sourceStart = blk.sourceStart;
|
|
5019
|
+
block.sourceEnd = blk.sourceEnd;
|
|
5020
|
+
}
|
|
5021
|
+
const rb = placed.resourceBlock;
|
|
5022
|
+
const height = placed.measured.totalHeight;
|
|
5023
|
+
block.resourceBlock = rb;
|
|
5024
|
+
block.lines = [{
|
|
5025
|
+
text: '',
|
|
5026
|
+
bbox: { x: 0, y: 0, width: rb.bodyRect.width, height },
|
|
5027
|
+
baseline: 0,
|
|
5028
|
+
hyphenated: false,
|
|
5029
|
+
segments: [],
|
|
5030
|
+
isLastLine: true,
|
|
5031
|
+
}];
|
|
5032
|
+
col = currentColumn(doc, cursor);
|
|
5033
|
+
if (spacing > 0)
|
|
5034
|
+
col.availableHeight -= spacing;
|
|
5035
|
+
placeBlockInColumn(block, height, col, cursor);
|
|
5036
|
+
offsetResourceBlockToAbsolute(rb, block.bbox.x, block.bbox.y);
|
|
5037
|
+
doc.blocks.push(block);
|
|
5038
|
+
enterBand(blockIdx, part);
|
|
5039
|
+
if (!slice?.continues)
|
|
5040
|
+
return 'placed';
|
|
5041
|
+
// The rest opens the next column, with no gap above it.
|
|
5042
|
+
startRow = slice.endRow;
|
|
5043
|
+
part++;
|
|
5044
|
+
moves = 0;
|
|
5045
|
+
pendingSpacing = 0;
|
|
5046
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(minHeightAt(width, startRow)));
|
|
5047
|
+
}
|
|
5048
|
+
};
|
|
4105
5049
|
const placeCalloutInline = (startIdx, plan) => {
|
|
4106
5050
|
const style = pickCalloutStyle(calloutStyles, plan.attrs.type);
|
|
4107
5051
|
const firstFrameId = `block-${blockIdCounter++}`;
|
|
@@ -4130,7 +5074,17 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4130
5074
|
const inFlow = tallerThanColumn(L.layoutRange(CUT_START, L.end, width, 'float-probe', false))
|
|
4131
5075
|
&& splitCalloutFragment(L, CUT_START, width, contentArea.height, 'float-probe', false, style.splitMinLines) !== null;
|
|
4132
5076
|
if (!inFlow) {
|
|
4133
|
-
|
|
5077
|
+
const wrap = pageIsVertical(page) ? undefined : resolveCalloutWrap(plan.attrs, resolved.layout.wrap);
|
|
5078
|
+
enqueueCalloutFloat(startIdx, plan, style, L, placement, span, wrap);
|
|
5079
|
+
calloutsOutOfFlow.add(startIdx);
|
|
5080
|
+
return undefined;
|
|
5081
|
+
}
|
|
5082
|
+
}
|
|
5083
|
+
// A box text wraps round (#627): at its side of the column, the text
|
|
5084
|
+
// after it beside it.
|
|
5085
|
+
if (placement === 'here' && span === 'column' && !pageIsVertical(doc.pages[cursor.pageIndex])) {
|
|
5086
|
+
const wrap = resolveCalloutWrap(plan.attrs, resolved.layout.wrap);
|
|
5087
|
+
if (wrap && placeCalloutWrapped(startIdx, plan, style, L, firstFrameId, wrap)) {
|
|
4134
5088
|
calloutsOutOfFlow.add(startIdx);
|
|
4135
5089
|
return undefined;
|
|
4136
5090
|
}
|
|
@@ -4174,7 +5128,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4174
5128
|
const balanceBefore = curCol.blocks.length > 0 || reservedOf(curCol).top > 0
|
|
4175
5129
|
? (balanceExtraPx?.get(balanceKey(startIdx, part)) ?? 0)
|
|
4176
5130
|
: 0;
|
|
4177
|
-
const spacing = (curCol
|
|
5131
|
+
const spacing = (atColumnHead(curCol) ? 0 : Math.max(pendingBeforeSelfSnapping(startIdx), result.marginTopPx)) + balanceBefore;
|
|
4178
5132
|
const roomPx = curCol.availableHeight - spacing;
|
|
4179
5133
|
let fragment = null;
|
|
4180
5134
|
if (result.totalHeight > roomPx + 0.01) {
|
|
@@ -4256,7 +5210,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4256
5210
|
const to = fragment ? fragment.to : L.end;
|
|
4257
5211
|
if (part > 0 || fragment)
|
|
4258
5212
|
markFragment(placed, part, fragment !== null);
|
|
4259
|
-
const spacingBefore = (curCol
|
|
5213
|
+
const spacingBefore = (atColumnHead(curCol) ? 0 : Math.max(pendingBeforeSelfSnapping(startIdx), placed.marginTopPx)) + balanceBefore;
|
|
4260
5214
|
if (balanceBefore > 0) {
|
|
4261
5215
|
addBalanceExtra(curCol, balanceBefore);
|
|
4262
5216
|
placed.frame.balancing = { levers: ['trailingCallout'], spaceAbove: balanceBefore };
|
|
@@ -4469,6 +5423,29 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4469
5423
|
releaseStaleCitationGates(blockIdx);
|
|
4470
5424
|
tryPlacePendingFloatsOnCurrentPage(spanBoxAt(blockIdx), blockIdx);
|
|
4471
5425
|
enqueueFloatsFor(blockIdx);
|
|
5426
|
+
// Text wrap (#627): running text after a wrapped figure takes the band
|
|
5427
|
+
// the figure was set in back, and runs beside it; anything else that
|
|
5428
|
+
// takes room from the flow goes under what text wraps round first.
|
|
5429
|
+
// Markers and the `::resource` line of a floated resource leave it.
|
|
5430
|
+
if (!transparentToWrap(rawBlock) && !(rawBlock.type === 'resourceBlock' && rawBlock.resourceId !== undefined && floatedIds.has(rawBlock.resourceId))) {
|
|
5431
|
+
const wrapCol = currentColumn(doc, cursor);
|
|
5432
|
+
const beside = runsBesideWrap(rawBlock);
|
|
5433
|
+
if (pendingWrap) {
|
|
5434
|
+
const { col, blocks, cursorY, ex } = pendingWrap;
|
|
5435
|
+
pendingWrap = null;
|
|
5436
|
+
if (beside && col === wrapCol && col.blocks.length === blocks && Math.abs(columnCursorY(col) - cursorY) < 0.01) {
|
|
5437
|
+
// The cursor goes back to the figure's top; the text starts there.
|
|
5438
|
+
col.availableHeight += columnCursorY(col) - ex.y;
|
|
5439
|
+
pendingSpacing = 0;
|
|
5440
|
+
inlineGapOwed = null;
|
|
5441
|
+
openColumnWrap(col, ex, 'top');
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
if (!beside && settleColumnWraps(wrapCol)) {
|
|
5445
|
+
pendingSpacing = 0;
|
|
5446
|
+
inlineGapOwed = null;
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
4472
5449
|
// --- Directives ----------------------------------------------------
|
|
4473
5450
|
// (A comic page, `:::page`, and a strip, `:::strip`, are placed below,
|
|
4474
5451
|
// once a closed part or paper run has taken its page break.)
|
|
@@ -4862,14 +5839,14 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4862
5839
|
const figureGrowPx = rawBlock.type === 'resourceBlock' ? balanceExtraPx?.get(flexFigureKey(blockIdx)) ?? 0 : 0;
|
|
4863
5840
|
const figureDelta = figureGrowPx > 0 ? { figureHeightDelta: figureGrowPx } : {};
|
|
4864
5841
|
let measuredBlock = tryLoose
|
|
4865
|
-
? measureLooseParagraph(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, styleOverride, looseLines, trackingLadder, looseOutcome)
|
|
5842
|
+
? measureLooseParagraph(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, styleOverride, looseLines, trackingLadder, looseOutcome, looseOffGrid)
|
|
4866
5843
|
: measureContentBlock(rawBlock, blockIdx, measureWidth, blockMeasureCtx, { styleOverride, ...upright, ...figureDelta });
|
|
4867
5844
|
// An inline picture with a safe area a little too tall for the room
|
|
4868
5845
|
// left in its column is cropped within its safe area to stay there
|
|
4869
5846
|
// rather than move on and leave the column short (#442).
|
|
4870
5847
|
if (measuredBlock?.kind.vdtType === 'resource' && measuredBlock.resourceBlock?.bodyFlex) {
|
|
4871
5848
|
const flex = measuredBlock.resourceBlock.bodyFlex;
|
|
4872
|
-
const room = col.availableHeight - (col
|
|
5849
|
+
const room = col.availableHeight - (atColumnHead(col) ? 0 : Math.max(pendingSpacing, floatGapPx));
|
|
4873
5850
|
const over = measuredBlock.measured.totalHeight - room;
|
|
4874
5851
|
if (over > 0.01 && room > 0 && over <= flex.shrink + 0.01) {
|
|
4875
5852
|
const cropped = measureContentBlock(rawBlock, blockIdx, measureWidth, blockMeasureCtx, {
|
|
@@ -4885,8 +5862,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4885
5862
|
// the page empty. Never below MIN_INLINE_FIGURE_SCALE of its size.
|
|
4886
5863
|
if (measuredBlock?.kind.vdtType === 'resource' && resolved.layout.fitFiguresToPage) {
|
|
4887
5864
|
const rb = measuredBlock.resourceBlock;
|
|
4888
|
-
const room = col.availableHeight - (col
|
|
4889
|
-
if (rb && !rb.table && !rb.rotation && rb.bodyRect.height > 0 && measuredBlock.measured.totalHeight > room && room > 0) {
|
|
5865
|
+
const room = col.availableHeight - (atColumnHead(col) ? 0 : Math.max(pendingSpacing, floatGapPx));
|
|
5866
|
+
if (rb && !rb.table && !rb.rotation && rb.bodyRect.height > 0 && measuredBlock.measured.totalHeight > room + FIT_EPS && room > 0) {
|
|
4890
5867
|
let width = rb.bodyRect.width;
|
|
4891
5868
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
4892
5869
|
const current = measuredBlock.resourceBlock;
|
|
@@ -4901,12 +5878,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4901
5878
|
if (!smaller)
|
|
4902
5879
|
break;
|
|
4903
5880
|
measuredBlock = smaller;
|
|
4904
|
-
if (smaller.measured.totalHeight <= room)
|
|
5881
|
+
if (smaller.measured.totalHeight <= room + FIT_EPS)
|
|
4905
5882
|
break;
|
|
4906
5883
|
}
|
|
4907
5884
|
// Still too tall at the smallest acceptable size: back to full size
|
|
4908
5885
|
// and on to the next column, as without the option.
|
|
4909
|
-
if (measuredBlock.measured.totalHeight > room) {
|
|
5886
|
+
if (measuredBlock.measured.totalHeight > room + FIT_EPS) {
|
|
4910
5887
|
measuredBlock = measureContentBlock(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, { styleOverride });
|
|
4911
5888
|
}
|
|
4912
5889
|
}
|
|
@@ -4962,22 +5939,104 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4962
5939
|
segments: [],
|
|
4963
5940
|
isLastLine: true,
|
|
4964
5941
|
}];
|
|
5942
|
+
// Text wrap (#627): a picture the text after it may run beside, at
|
|
5943
|
+
// its side of the column — unless the text left beside it would be
|
|
5944
|
+
// too narrow, or the picture too short to set lines beside.
|
|
5945
|
+
const wrapSettings = blockMeasureCtx.resolved.layout.wrap;
|
|
5946
|
+
const wrapSpec = kind.resource && !pageIsVertical(blockPage)
|
|
5947
|
+
? resolveResourceWrap(kind.resource, kind.resourceType, wrapSettings)
|
|
5948
|
+
: undefined;
|
|
5949
|
+
// Vertical text keeps its figures' bands whole (horizontal wrap only).
|
|
5950
|
+
if (kind.resource && pageIsVertical(blockPage) && resolveResourceWrap(kind.resource, kind.resourceType, wrapSettings)) {
|
|
5951
|
+
textWrapNotes.push({
|
|
5952
|
+
kind: 'textWrap', reason: 'verticalText', resourceId: kind.resource.id,
|
|
5953
|
+
sourceStart: rawBlock.sourceStart + bodyOffset, sourceEnd: rawBlock.sourceEnd + bodyOffset, pageIndex: cursor.pageIndex,
|
|
5954
|
+
});
|
|
5955
|
+
}
|
|
5956
|
+
let wrapGap = 0;
|
|
5957
|
+
let wraps = false;
|
|
5958
|
+
if (wrapSpec) {
|
|
5959
|
+
wrapGap = wrapGapPx(wrapSpec.gap, dpi, bodyStyle.fontSizePx, floatGapPx);
|
|
5960
|
+
const textWidth = col.bbox.width * (1 - wrapSpec.width) - wrapGap;
|
|
5961
|
+
const linesBeside = Math.ceil((groupHeight + wrapGap - 0.01) / bodyStyle.lineHeightPx);
|
|
5962
|
+
const reason = textWidth + 0.01 < minTextWidthPx(wrapSettings.minTextWidth, col.bbox.width, dpi, bodyStyle.fontSizePx)
|
|
5963
|
+
? 'tooNarrow'
|
|
5964
|
+
: linesBeside < wrapSettings.minLinesBeside ? 'fewLines' : undefined;
|
|
5965
|
+
if (reason) {
|
|
5966
|
+
textWrapNotes.push({
|
|
5967
|
+
kind: 'textWrap', reason, resourceId: kind.resource.id,
|
|
5968
|
+
sourceStart: rawBlock.sourceStart + bodyOffset, sourceEnd: rawBlock.sourceEnd + bodyOffset, pageIndex: cursor.pageIndex,
|
|
5969
|
+
});
|
|
5970
|
+
}
|
|
5971
|
+
else {
|
|
5972
|
+
wraps = true;
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
4965
5975
|
// An inline resource keeps the float gap (a line) above it, as a
|
|
4966
|
-
// float would, unless the block before asked for more.
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
5976
|
+
// float would, unless the block before asked for more. A wrapped one
|
|
5977
|
+
// stands level with the next line's top instead.
|
|
5978
|
+
const spacingBefore = wraps ? pendingSpacing : Math.max(pendingSpacing, floatGapPx);
|
|
5979
|
+
// An inline table that does not fit the room left in its column is
|
|
5980
|
+
// cut between rows (#634), by the rules of a floated one.
|
|
5981
|
+
const sliced = !wraps && !resourceBlock.rotation && resourceBlock.table && kind.resource
|
|
5982
|
+
? placeInlineTableSlices(rawBlock, blockIdx, blk, blockMeasureCtx, styleOverride, kind.resource, groupHeight, spacingBefore)
|
|
5983
|
+
: 'whole';
|
|
5984
|
+
if (sliced === 'dropped') {
|
|
5985
|
+
flushPendingNumberingAtBoundary();
|
|
5986
|
+
continue;
|
|
5987
|
+
}
|
|
5988
|
+
const columnBefore = col;
|
|
5989
|
+
if (sliced === 'whole') {
|
|
5990
|
+
enterBand(blockIdx, 0);
|
|
5991
|
+
placeAtomicBlock(blk, groupHeight, spacingBefore, cursor, doc, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(groupHeight));
|
|
5992
|
+
enterBand(blockIdx, 0);
|
|
5993
|
+
// Too tall for the room left in its column, a wrapped picture moved
|
|
5994
|
+
// on with its anchor: say so.
|
|
5995
|
+
if (wraps && currentColumn(doc, cursor) !== columnBefore && columnBefore.blocks.length > 0) {
|
|
5996
|
+
textWrapNotes.push({
|
|
5997
|
+
kind: 'textWrap', reason: 'moved', resourceId: kind.resource.id,
|
|
5998
|
+
sourceStart: rawBlock.sourceStart + bodyOffset, sourceEnd: rawBlock.sourceEnd + bodyOffset, pageIndex: cursor.pageIndex,
|
|
5999
|
+
});
|
|
6000
|
+
}
|
|
6001
|
+
// `placeBlockInColumn` (inside placeAtomicBlock) shifts `blk.lines`; the
|
|
6002
|
+
// resource's own caption/table lines live on `resourceBlock` and must be
|
|
6003
|
+
// offset to absolute page coordinates here using the placed bbox origin.
|
|
6004
|
+
offsetResourceBlockToAbsolute(resourceBlock, blk.bbox.x, blk.bbox.y);
|
|
6005
|
+
const grown = figureGrowPx > 0 ? resourceBlock.bodyFlex?.delta ?? 0 : 0;
|
|
6006
|
+
if (grown > 0.01) {
|
|
6007
|
+
blk.balancing = { levers: ['flexFigure'], spaceAbove: 0, bodyGrowth: grown };
|
|
6008
|
+
addBalanceExtra(currentColumn(doc, cursor), grown);
|
|
6009
|
+
}
|
|
6010
|
+
doc.blocks.push(blk);
|
|
6011
|
+
// A wrapped picture (#627): the flow goes on under it and its gap, on
|
|
6012
|
+
// a grid line, unless the next block takes the band back to run
|
|
6013
|
+
// beside it (see `pendingWrap`).
|
|
6014
|
+
if (wraps && wrapSpec) {
|
|
6015
|
+
const rCol = currentColumn(doc, cursor);
|
|
6016
|
+
const top = blk.bbox.y;
|
|
6017
|
+
const bottomRel = Math.ceil((top - rCol.bbox.y + groupHeight + wrapGap - 0.01) / baselineGrid) * baselineGrid;
|
|
6018
|
+
rCol.availableHeight = Math.max(0, Math.min(rCol.availableHeight, rCol.bbox.height - bottomRel));
|
|
6019
|
+
pendingSpacing = 0;
|
|
6020
|
+
inlineGapOwed = null;
|
|
6021
|
+
const exWidth = wrapExclusionWidth(rCol.bbox.width, rCol.bbox.width * wrapSpec.width, wrapGap);
|
|
6022
|
+
pendingWrap = {
|
|
6023
|
+
col: rCol,
|
|
6024
|
+
blocks: rCol.blocks.length,
|
|
6025
|
+
cursorY: columnCursorY(rCol),
|
|
6026
|
+
ex: {
|
|
6027
|
+
x: wrapSpec.side === 'left' ? rCol.bbox.x : rCol.bbox.x + rCol.bbox.width - exWidth,
|
|
6028
|
+
y: top,
|
|
6029
|
+
width: exWidth,
|
|
6030
|
+
height: rCol.bbox.y + bottomRel - top,
|
|
6031
|
+
side: wrapSpec.side,
|
|
6032
|
+
gap: exWidth - rCol.bbox.width * wrapSpec.width,
|
|
6033
|
+
ownerId: blk.id,
|
|
6034
|
+
},
|
|
6035
|
+
};
|
|
6036
|
+
flushPendingNumberingAtBoundary();
|
|
6037
|
+
continue;
|
|
6038
|
+
}
|
|
4979
6039
|
}
|
|
4980
|
-
doc.blocks.push(blk);
|
|
4981
6040
|
// Snap the flow position after the resource to the baseline grid (the
|
|
4982
6041
|
// group height is arbitrary), baking in at least marginBottom — same
|
|
4983
6042
|
// convention as snapped headings — so the following text lands back on
|
|
@@ -5045,7 +6104,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5045
6104
|
// `bulletXOffsetInColumn` is `indentPx` for unordered/task, and
|
|
5046
6105
|
// `indentPx + (maxNumberWidth - thisNumberWidth)` for ordered — giving
|
|
5047
6106
|
// the right-aligned separator.
|
|
5048
|
-
|
|
6107
|
+
// Beside a wrapped picture on the left (#627) the marker stands past
|
|
6108
|
+
// it, with its text.
|
|
6109
|
+
blk.bulletOffsetX = blk.bbox.x + bulletXOffsetInColumn + lineInsetsAt(wrapInsets, 0).start;
|
|
5049
6110
|
if (listBullet.separatorText !== undefined) {
|
|
5050
6111
|
blk.separatorText = listBullet.separatorText;
|
|
5051
6112
|
blk.separatorFontString = listBullet.separatorFontString;
|
|
@@ -5143,6 +6204,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5143
6204
|
* default opener paints across the page. */
|
|
5144
6205
|
let brokenForWidth = measureWidth;
|
|
5145
6206
|
let laterWidths = [];
|
|
6207
|
+
/** Text wrap (#627): the lines of the block set short beside what text
|
|
6208
|
+
* wraps round, as its lines are now set (paragraph line numbers). */
|
|
6209
|
+
let wrapInsets = [];
|
|
6210
|
+
const wrapsBeside = canSplit && runsBesideWrap(rawBlock);
|
|
5146
6211
|
const rebreaksForColumn = !tryLoose && rawBlock.toc === undefined && rawBlock.index === undefined
|
|
5147
6212
|
&& (canSplit || (vdtType === 'heading' && !opensDefaultOpener(rawBlock)));
|
|
5148
6213
|
/** Measure the block again for the columns it is set in (see
|
|
@@ -5150,6 +6215,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5150
6215
|
const measureForColumns = (extra) => measureContentBlock(rawBlock, blockIdx, brokenForWidth, blockMeasureCtx, {
|
|
5151
6216
|
styleOverride,
|
|
5152
6217
|
...(laterWidths.length > 0 ? { restColumnWidths: laterWidths } : {}),
|
|
6218
|
+
...(wrapInsets.length > 0 ? { lineInsets: wrapInsets } : {}),
|
|
5153
6219
|
...extra,
|
|
5154
6220
|
});
|
|
5155
6221
|
// "Keep with next" for colon-introduced lists: a paragraph ending in `:`
|
|
@@ -5169,6 +6235,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5169
6235
|
while (remainingLines.length > 0) {
|
|
5170
6236
|
enterBand(blockIdx, partIndex);
|
|
5171
6237
|
const curCol = currentColumn(doc, cursor);
|
|
6238
|
+
// A wrapped float at the head or foot of this column (#627): running
|
|
6239
|
+
// text takes its band back to run beside it.
|
|
6240
|
+
if (wrapsBeside)
|
|
6241
|
+
openFloatWraps(curCol);
|
|
5172
6242
|
// The rest of the block goes into a column of another width than the
|
|
5173
6243
|
// one it was broken for (EF-157): break it again for this one. Up to
|
|
5174
6244
|
// postext 1.4 it kept the lines of the column it started in, and a
|
|
@@ -5183,6 +6253,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5183
6253
|
capInfo = again.dropCap;
|
|
5184
6254
|
brokenForWidth = curCol.bbox.width;
|
|
5185
6255
|
laterWidths = [];
|
|
6256
|
+
wrapInsets = [];
|
|
5186
6257
|
letterSpacingPx = again.letterSpacingPx;
|
|
5187
6258
|
remainingLines = [...raggedLooseLines(again.measured.lines, style.textAlign)];
|
|
5188
6259
|
paragraphLines = remainingLines;
|
|
@@ -5201,8 +6272,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5201
6272
|
const keepBreaks = paragraphBreaks && paragraphBreaks.at.length >= placed
|
|
5202
6273
|
? { path: paragraphBreaks.path, at: paragraphBreaks.at.slice(0, placed) }
|
|
5203
6274
|
: undefined;
|
|
6275
|
+
const keptInsets = insetsBefore(wrapInsets, placed);
|
|
5204
6276
|
const again = measureContentBlock(rawBlock, blockIdx, brokenForWidth, blockMeasureCtx, {
|
|
5205
6277
|
styleOverride, restColumnWidths: steps, ...(keepBreaks ? { keepBreaks } : {}),
|
|
6278
|
+
...(keptInsets.length > 0 ? { lineInsets: keptInsets } : {}),
|
|
5206
6279
|
});
|
|
5207
6280
|
const lines = again && (again.letterSpacingPx ?? 0) === (letterSpacingPx ?? 0)
|
|
5208
6281
|
? [...raggedLooseLines(again.measured.lines, style.textAlign)]
|
|
@@ -5210,6 +6283,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5210
6283
|
if (lines && lines.length > placed
|
|
5211
6284
|
&& paragraphLines.slice(0, placed).every((l, i) => lines[i].text === l.text && lines[i].hyphenated === l.hyphenated)) {
|
|
5212
6285
|
laterWidths = steps;
|
|
6286
|
+
wrapInsets = keptInsets;
|
|
5213
6287
|
paragraphLines = lines;
|
|
5214
6288
|
paragraphBreaks = again.measured.breaks;
|
|
5215
6289
|
remainingLines = lines.slice(placed);
|
|
@@ -5230,7 +6304,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5230
6304
|
/** Column balancing: the levers applied to this placement, stamped
|
|
5231
6305
|
* on the block it places (`VDTBlock.balancing`). */
|
|
5232
6306
|
let balancing;
|
|
5233
|
-
|
|
6307
|
+
// (Right under a float at the head of the opener's own column, #639,
|
|
6308
|
+
// the block starts as at a column's head: no space above it.)
|
|
6309
|
+
if (!isFirstInColumn && !underOpenerHeadFloat(curCol)) {
|
|
5234
6310
|
spacingBefore = pendingSpacing;
|
|
5235
6311
|
if (vdtType === 'heading' || vdtType === 'mathDisplay') {
|
|
5236
6312
|
spacingBefore = Math.max(partIndex === 0 ? pendingBeforeSelfSnapping(blockIdx) : spacingBefore, style.marginTopPx);
|
|
@@ -5321,6 +6397,53 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5321
6397
|
if (partIndex === 0 && capInfo && capInfo.rise > 0.5) {
|
|
5322
6398
|
spacingBefore += Math.ceil((capInfo.rise - 0.5) / baselineGrid) * baselineGrid;
|
|
5323
6399
|
}
|
|
6400
|
+
// Text wrap (#627): the lines this block would set beside what text
|
|
6401
|
+
// wraps round in this column are broken short of it; the lines it
|
|
6402
|
+
// already placed elsewhere keep their breaks. A block that cannot
|
|
6403
|
+
// keep them (a break trace of the other measuring path) goes under
|
|
6404
|
+
// what it would have run beside instead.
|
|
6405
|
+
if (wrapsBeside) {
|
|
6406
|
+
const placed = paragraphLines.length - remainingLines.length;
|
|
6407
|
+
const y0 = columnCursorY(curCol) + spacingBefore;
|
|
6408
|
+
const wanted = wrapInsetSteps(curCol, y0, style.lineHeightPx, placed);
|
|
6409
|
+
if (!sameInsetsFrom(wrapInsets, wanted, placed)) {
|
|
6410
|
+
const steps = [...insetsBefore(wrapInsets, placed), ...wanted];
|
|
6411
|
+
const keepBreaks = placed > 0 && paragraphBreaks && paragraphBreaks.at.length >= placed
|
|
6412
|
+
? { path: paragraphBreaks.path, at: paragraphBreaks.at.slice(0, placed) }
|
|
6413
|
+
: undefined;
|
|
6414
|
+
const again = placed > 0 && !keepBreaks ? null : measureContentBlock(rawBlock, blockIdx, brokenForWidth, blockMeasureCtx, {
|
|
6415
|
+
styleOverride,
|
|
6416
|
+
...(laterWidths.length > 0 ? { restColumnWidths: laterWidths } : {}),
|
|
6417
|
+
...(steps.length > 0 ? { lineInsets: steps } : {}),
|
|
6418
|
+
...(keepBreaks ? { keepBreaks } : {}),
|
|
6419
|
+
});
|
|
6420
|
+
const lines = again && (placed === 0 || (again.letterSpacingPx ?? 0) === (letterSpacingPx ?? 0))
|
|
6421
|
+
? [...raggedLooseLines(again.measured.lines, style.textAlign)]
|
|
6422
|
+
: undefined;
|
|
6423
|
+
if (lines && lines.length > placed
|
|
6424
|
+
&& paragraphLines.slice(0, placed).every((l, i) => lines[i].text === l.text && lines[i].hyphenated === l.hyphenated)) {
|
|
6425
|
+
wrapInsets = steps;
|
|
6426
|
+
paragraphLines = lines;
|
|
6427
|
+
paragraphBreaks = again.measured.breaks;
|
|
6428
|
+
remainingLines = lines.slice(placed);
|
|
6429
|
+
if (placed === 0) {
|
|
6430
|
+
capInfo = again.dropCap;
|
|
6431
|
+
letterSpacingPx = again.letterSpacingPx;
|
|
6432
|
+
guardedEnds = [];
|
|
6433
|
+
columnEndHyphenRetried = false;
|
|
6434
|
+
}
|
|
6435
|
+
regateCitedFloats(blockIdx, lines);
|
|
6436
|
+
}
|
|
6437
|
+
else if (wanted.length > 0) {
|
|
6438
|
+
if (balancing && balancing.spaceAbove > 0) {
|
|
6439
|
+
balanceExtraInColumn.set(curCol, (balanceExtraInColumn.get(curCol) ?? 0) - balancing.spaceAbove);
|
|
6440
|
+
}
|
|
6441
|
+
settleColumnWraps(curCol);
|
|
6442
|
+
pendingSpacing = 0;
|
|
6443
|
+
continue;
|
|
6444
|
+
}
|
|
6445
|
+
}
|
|
6446
|
+
}
|
|
5324
6447
|
// A loose paragraph's extra line is balancing height too (it lands
|
|
5325
6448
|
// whole in this column — loose candidates are never split parts).
|
|
5326
6449
|
const looseTracking = looseOutcome.get(blockIdx);
|
|
@@ -5418,7 +6541,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5418
6541
|
// the tail follows the list naturally in the next column.
|
|
5419
6542
|
if (endsWithColon
|
|
5420
6543
|
&& partIndex === 0
|
|
5421
|
-
|
|
6544
|
+
// The tolerance of the fit check below (#635): a paragraph that
|
|
6545
|
+
// fits by a hair of drift is one this rule reads too.
|
|
6546
|
+
&& totalRemainHeight <= effectiveAvailable + FIT_EPS
|
|
5422
6547
|
&& curCol.blocks.length > 0) {
|
|
5423
6548
|
const usedHeight = (curCol.bbox.height - curCol.availableHeight) + spacingBefore;
|
|
5424
6549
|
const paragraphBottom = usedHeight + totalRemainHeight;
|
|
@@ -5442,7 +6567,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5442
6567
|
// there, by the rules that split it — a two-line item the orphan
|
|
5443
6568
|
// and widow rules keep whole needs both. `'line'` keeps the 1.4
|
|
5444
6569
|
// check, one line.
|
|
5445
|
-
if (availableAfter < minSpaceForList
|
|
6570
|
+
if (availableAfter < minSpaceForList - FIT_EPS
|
|
5446
6571
|
|| (resolved.bodyText.colonListRoom === 'item'
|
|
5447
6572
|
&& !firstListItemStarts(blockIdx, availableAfter - effectiveGap, curCol.bbox.width, blockMeasureCtx))) {
|
|
5448
6573
|
const effectiveWidowMin = resolved.bodyText.avoidWidows
|
|
@@ -5644,7 +6769,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5644
6769
|
// A `:::space` between the heading and its text needs room too.
|
|
5645
6770
|
const minSpaceAfter = minLinesNeeded * bodyStyle.lineHeightPx
|
|
5646
6771
|
+ spaceLinesAfter(contentBlocks, blockIdx) * baselineGrid;
|
|
5647
|
-
|
|
6772
|
+
// The room is a difference of grid sums and the need a product:
|
|
6773
|
+
// a head whose text ends on the last line of the column is short
|
|
6774
|
+
// by floating-point drift alone (#635), and stays.
|
|
6775
|
+
if (remainAfterHeading < minSpaceAfter - FIT_EPS) {
|
|
5648
6776
|
if (curCol.blocks.length === 0)
|
|
5649
6777
|
shortColumnMoves++;
|
|
5650
6778
|
// Roll back any immediately-preceding heading blocks in this
|
|
@@ -5747,7 +6875,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5747
6875
|
// its margin and grid snap would carry the block past the foot: it
|
|
5748
6876
|
// takes the rest of the column instead, as a design reaching past
|
|
5749
6877
|
// the foot does (the text after it opens the next column or page).
|
|
5750
|
-
if (vdtType === 'heading' && effectiveRemainHeight > totalRemainHeight && h > curCol.availableHeight) {
|
|
6878
|
+
if (vdtType === 'heading' && effectiveRemainHeight > totalRemainHeight && h > curCol.availableHeight + FIT_EPS) {
|
|
5751
6879
|
h = Math.max(effectiveRemainHeight, curCol.availableHeight);
|
|
5752
6880
|
}
|
|
5753
6881
|
placeBlockInColumn(blk, h, curCol, cursor);
|
|
@@ -6139,6 +7267,25 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
6139
7267
|
// End of the document: level the closing band and place any floats still
|
|
6140
7268
|
// pending (referenced on the last page) on pages appended after it.
|
|
6141
7269
|
closeFlowSegment(contentBlocks.length);
|
|
7270
|
+
// Nothing leaves the layout unseen (#639): a box or a float still in a
|
|
7271
|
+
// queue here is on no page, and the build says so.
|
|
7272
|
+
const unplaced = [];
|
|
7273
|
+
const sourceOf = (from, to) => {
|
|
7274
|
+
const first = contentBlocks[from];
|
|
7275
|
+
const last = contentBlocks[to];
|
|
7276
|
+
return first && last ? { sourceStart: first.sourceStart + bodyOffset, sourceEnd: last.sourceEnd + bodyOffset } : {};
|
|
7277
|
+
};
|
|
7278
|
+
for (const box of [...awaitingSideBoxes.map((w) => w.box), ...pendingSideBoxes]) {
|
|
7279
|
+
unplaced.push({ kind: 'unplaced', ...sourceOf(box.startIdx, box.plan.endIdx) });
|
|
7280
|
+
}
|
|
7281
|
+
for (const f of pendingFloats) {
|
|
7282
|
+
const box = f.callout ? calloutFloats.get(f.callout.startIdx) : undefined;
|
|
7283
|
+
unplaced.push(box
|
|
7284
|
+
? { kind: 'unplaced', ...sourceOf(f.callout.startIdx, box.plan.endIdx) }
|
|
7285
|
+
: { kind: 'unplaced', resourceId: f.resourceId, ...sourceOf(f.firstBlockIdx, f.firstBlockIdx) });
|
|
7286
|
+
}
|
|
7287
|
+
if (unplaced.length > 0)
|
|
7288
|
+
unplacedByDoc.set(doc, unplaced);
|
|
6142
7289
|
// The notes, in the room the flow reserved for them.
|
|
6143
7290
|
reserveLeftoverNotes();
|
|
6144
7291
|
setColumnNotes();
|
|
@@ -6255,11 +7402,24 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
6255
7402
|
joiningSpacingWarnings.set(doc, joiningLetterSpacingWarnings(doc, joiningLetterSpacing));
|
|
6256
7403
|
if (dropCapNotes.size > 0)
|
|
6257
7404
|
dropCapWarningsByDoc.set(doc, dropCapWarnings(doc, dropCapNotes));
|
|
6258
|
-
|
|
7405
|
+
// Pictures scaled to the room of their slot (#626), at the block citing them.
|
|
7406
|
+
const shrunk = floatShrinkWarnings(doc, (idx) => {
|
|
7407
|
+
const b = contentBlocks[idx];
|
|
7408
|
+
return b ? { start: b.sourceStart + bodyOffset, end: b.sourceEnd + bodyOffset } : undefined;
|
|
7409
|
+
});
|
|
7410
|
+
if (shrunk.length > 0)
|
|
7411
|
+
floatShrinkWarningsByDoc.set(doc, shrunk);
|
|
7412
|
+
if (textWrapNotes.length > 0 || columnsNotes.length > 0)
|
|
7413
|
+
textWrapWarningsByDoc.set(doc, [...textWrapNotes, ...columnsNotes]);
|
|
7414
|
+
const { proposals: citingPageProposals, refused: citingPageRefused } = settleCitingPageFloats();
|
|
7415
|
+
return { doc, forcedBreakPages, bandCapProposals, spanPlacedInBand, bandCapsApplied, looseOutcome, looseOffGrid, captionUnderProposals, citingPageProposals, citingPageRefused };
|
|
6259
7416
|
}
|
|
6260
7417
|
/** Passes a document printing its own contents gets at most, beyond the
|
|
6261
7418
|
* first, for the page labels it prints to settle. */
|
|
6262
7419
|
const MAX_TOC_ROUNDS = 3;
|
|
7420
|
+
/** Rounds of proposals for floats heading their citing page (#633), each a
|
|
7421
|
+
* placement pass, before the band caps run. */
|
|
7422
|
+
const MAX_CITING_PAGE_ROUNDS = 3;
|
|
6263
7423
|
export function buildDocument(content, config, cache, options) {
|
|
6264
7424
|
return drainPasses(buildDocumentGen(content, config, cache, options));
|
|
6265
7425
|
}
|
|
@@ -6303,6 +7463,12 @@ export async function buildDocumentAsync(content, config, cache, options) {
|
|
|
6303
7463
|
}
|
|
6304
7464
|
/** The build as a generator: one `yield` after every placement pass. */
|
|
6305
7465
|
export function* buildDocumentGen(content, config, cache, options) {
|
|
7466
|
+
// Faces that arrived since the last build drop what was measured in
|
|
7467
|
+
// their families, and a config changed in place resolves again (#629).
|
|
7468
|
+
const fontSet = options?.fontSet === undefined ? defaultFontSet() : options.fontSet;
|
|
7469
|
+
if (fontSet)
|
|
7470
|
+
syncFontSet(fontSet);
|
|
7471
|
+
validateConfigCache(config);
|
|
6306
7472
|
const doc = yield* buildDocumentRounds(content, config, cache, options);
|
|
6307
7473
|
// References and markup the source names wrongly: read from the source
|
|
6308
7474
|
// once, located on the pages of the finished layout. Kept apart from
|
|
@@ -6315,7 +7481,12 @@ export function* buildDocumentGen(content, config, cache, options) {
|
|
|
6315
7481
|
// cap; words of a joining script that run past their line, and styles
|
|
6316
7482
|
// whose letter-spacing such words do not take.
|
|
6317
7483
|
// Line numbers set over a float of the side column (#621).
|
|
6318
|
-
const loose = [...cjkLooseLineWarnings(doc), ...wordOverflowWarnings(doc), ...(joiningSpacingWarnings.get(doc) ?? []), ...(dropCapWarningsByDoc.get(doc) ?? []), ...lineNumberWarnings(doc), ...codeOverflowWarnings(doc)];
|
|
7484
|
+
const loose = [...cjkLooseLineWarnings(doc), ...wordOverflowWarnings(doc), ...(joiningSpacingWarnings.get(doc) ?? []), ...(dropCapWarningsByDoc.get(doc) ?? []), ...lineNumberWarnings(doc), ...codeOverflowWarnings(doc), ...(floatShrinkWarningsByDoc.get(doc) ?? []), ...(textWrapWarningsByDoc.get(doc) ?? [])];
|
|
7485
|
+
// Side boxes and side floats set on pages after the text, and what the
|
|
7486
|
+
// layout could place nowhere (#639).
|
|
7487
|
+
loose.push(...afterTextWarnings(doc), ...(unplacedByDoc.get(doc) ?? []));
|
|
7488
|
+
// Design texts cut to fit their width (#628).
|
|
7489
|
+
loose.push(...designTruncationWarnings(doc));
|
|
6319
7490
|
// Comic panels whose cell cannot hold their picture's safe area (#556).
|
|
6320
7491
|
for (const page of doc.pages) {
|
|
6321
7492
|
if (page.comic)
|
|
@@ -6341,8 +7512,12 @@ export function* buildDocumentGen(content, config, cache, options) {
|
|
|
6341
7512
|
const harakat = annotateArabicMarks(doc);
|
|
6342
7513
|
// The rules over `*…*` runs under `bodyText.emphasis: 'overline'`.
|
|
6343
7514
|
overlineEmphasis(doc);
|
|
6344
|
-
|
|
6345
|
-
|
|
7515
|
+
// Faces the text was set in that the font set could not give (#629).
|
|
7516
|
+
const fallbacks = fontSet && resolveDebugConfig(config?.debug).warnings.missingFont
|
|
7517
|
+
? fontFallbacks(documentFontFaces(doc), fontSet).map((f) => ({ kind: 'fontFallback', family: f.family, weight: f.weight, style: f.style, reason: f.reason }))
|
|
7518
|
+
: [];
|
|
7519
|
+
if (located.length > 0 || loose.length > 0 || annotations.length > 0 || harakat.length > 0 || fallbacks.length > 0) {
|
|
7520
|
+
doc.contentWarnings = [...located, ...loose, ...annotations, ...harakat, ...fallbacks];
|
|
6346
7521
|
}
|
|
6347
7522
|
// Config values the build replaced (an unknown number format, a font
|
|
6348
7523
|
// stack, a side column no column width can take): walked once per build,
|
|
@@ -6360,6 +7535,35 @@ const indexWarnings = new WeakMap();
|
|
|
6360
7535
|
const joiningSpacingWarnings = new WeakMap();
|
|
6361
7536
|
/** The `dropCap` warnings of the last pass, per document (#623). */
|
|
6362
7537
|
const dropCapWarningsByDoc = new WeakMap();
|
|
7538
|
+
/** The `floatShrunk` warnings of a build (#626), found by its last pass. */
|
|
7539
|
+
const floatShrinkWarningsByDoc = new WeakMap();
|
|
7540
|
+
/** The `textWrap` warnings of a build (#627) and its `columnsTooNarrow`
|
|
7541
|
+
* ones (#634), found by its last pass. */
|
|
7542
|
+
const textWrapWarningsByDoc = new WeakMap();
|
|
7543
|
+
/** What a pass set in the side column of a page opened after the text of
|
|
7544
|
+
* its chapter (#639): a side box's frame, a side float's block, with the
|
|
7545
|
+
* source range an `afterText` warning points at. Read from the finished
|
|
7546
|
+
* layout, so the blocks a splice keeps bring their note along. */
|
|
7547
|
+
const setAfterText = new WeakMap();
|
|
7548
|
+
/** The `unplaced` warnings of a build (#639): what its last pass left in
|
|
7549
|
+
* its queues. */
|
|
7550
|
+
const unplacedByDoc = new WeakMap();
|
|
7551
|
+
/** An `afterText` warning for each side box and side float set after the
|
|
7552
|
+
* text (`setAfterText`) on a page whose text columns hold nothing: one a
|
|
7553
|
+
* barrier box then shared with them holds text again. */
|
|
7554
|
+
function afterTextWarnings(doc) {
|
|
7555
|
+
const out = [];
|
|
7556
|
+
for (const page of doc.pages) {
|
|
7557
|
+
if (!page.floats || page.columns.some((c) => c.kind !== 'side' && c.blocks.length > 0))
|
|
7558
|
+
continue;
|
|
7559
|
+
for (const block of page.floats) {
|
|
7560
|
+
const note = setAfterText.get(block);
|
|
7561
|
+
if (note)
|
|
7562
|
+
out.push({ kind: 'afterText', ...note, pageIndex: page.index });
|
|
7563
|
+
}
|
|
7564
|
+
}
|
|
7565
|
+
return out;
|
|
7566
|
+
}
|
|
6363
7567
|
/** `doc` with the page of each of its index marks (`doc.indexMarks`). */
|
|
6364
7568
|
function withIndexMarks(doc, content) {
|
|
6365
7569
|
const { content: body, contentOffset } = extractFrontmatter(content.markdown);
|
|
@@ -6378,6 +7582,10 @@ function withIndexMarks(doc, content) {
|
|
|
6378
7582
|
/** {@link buildDocumentGen} before the document-wide warnings: the
|
|
6379
7583
|
* contents rounds around the balanced build. */
|
|
6380
7584
|
function* buildDocumentRounds(content, config, cache, options) {
|
|
7585
|
+
// The floats heading their citing page (#633) carry over from one round
|
|
7586
|
+
// of the contents or of the note numbers to the next: the layout barely
|
|
7587
|
+
// moves between them, and each round would otherwise settle them again.
|
|
7588
|
+
const citing = { hints: new Map(), refused: new Set() };
|
|
6381
7589
|
// A document printing its own table of contents (`:::toc` with no
|
|
6382
7590
|
// host-supplied outline) is laid out with the page labels of the previous
|
|
6383
7591
|
// build until they no longer change: the contents' own length moves what
|
|
@@ -6394,18 +7602,18 @@ function* buildDocumentRounds(content, config, cache, options) {
|
|
|
6394
7602
|
};
|
|
6395
7603
|
if (hasTocDirective(parsed) || hasIndexDirective(parsed) || pageRefs()) {
|
|
6396
7604
|
let outline = computeOutline(parsed, resolveAllConfig(config), content.continuation?.headings, content.continuation?.statementCounters);
|
|
6397
|
-
let doc = withIndexMarks(yield* buildDocumentNumbered({ ...content, outline }, config, cache, options, 0), content);
|
|
7605
|
+
let doc = withIndexMarks(yield* buildDocumentNumbered({ ...content, outline }, config, cache, options, 0, citing), content);
|
|
6398
7606
|
for (let round = 0; round < MAX_TOC_ROUNDS; round++) {
|
|
6399
7607
|
const after = outlineFromDoc(doc, outline);
|
|
6400
7608
|
if (sameOutline(after, outline))
|
|
6401
7609
|
break;
|
|
6402
7610
|
outline = after;
|
|
6403
|
-
doc = withIndexMarks(yield* buildDocumentNumbered({ ...content, outline }, config, cache, options, round + 1), content);
|
|
7611
|
+
doc = withIndexMarks(yield* buildDocumentNumbered({ ...content, outline }, config, cache, options, round + 1, citing), content);
|
|
6404
7612
|
}
|
|
6405
7613
|
return doc;
|
|
6406
7614
|
}
|
|
6407
7615
|
}
|
|
6408
|
-
return withIndexMarks(yield* buildDocumentNumbered(content, config, cache, options), content);
|
|
7616
|
+
return withIndexMarks(yield* buildDocumentNumbered(content, config, cache, options, 0, citing), content);
|
|
6409
7617
|
}
|
|
6410
7618
|
/** The number each note's marker prints in `doc` (its first marker). */
|
|
6411
7619
|
function printedFootnoteNumbers(doc) {
|
|
@@ -6435,8 +7643,8 @@ const MAX_FOOTNOTE_ROUNDS = 3;
|
|
|
6435
7643
|
* width whatever its number, so the second build reflows nothing and the
|
|
6436
7644
|
* numbers hold; a decimal one may move a line (9 → 10), hence the bound.
|
|
6437
7645
|
*/
|
|
6438
|
-
function* buildDocumentNumbered(content, config, cache, options, tocRound = 0) {
|
|
6439
|
-
let doc = yield* buildDocumentBalanced(content, config, cache, options, tocRound);
|
|
7646
|
+
function* buildDocumentNumbered(content, config, cache, options, tocRound = 0, citing) {
|
|
7647
|
+
let doc = yield* buildDocumentBalanced(content, config, cache, options, tocRound, undefined, citing);
|
|
6440
7648
|
const f = doc.config.footnotes;
|
|
6441
7649
|
if ((f.placement !== 'column' && f.placement !== 'spread') || (f.numbering !== 'page' && f.numbering !== 'column' && f.numbering !== 'spread'))
|
|
6442
7650
|
return doc;
|
|
@@ -6454,12 +7662,15 @@ function* buildDocumentNumbered(content, config, cache, options, tocRound = 0) {
|
|
|
6454
7662
|
next.set(id, number);
|
|
6455
7663
|
if (sameFootnoteNumbers(next, used))
|
|
6456
7664
|
break;
|
|
6457
|
-
doc = yield* buildDocumentBalanced(content, config, cache, options, tocRound, next);
|
|
7665
|
+
doc = yield* buildDocumentBalanced(content, config, cache, options, tocRound, next, citing);
|
|
6458
7666
|
}
|
|
6459
7667
|
return doc;
|
|
6460
7668
|
}
|
|
6461
7669
|
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
6462
|
-
function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, footnoteNumbers
|
|
7670
|
+
function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, footnoteNumbers,
|
|
7671
|
+
/** Floats heading their citing page settled by an earlier round of the
|
|
7672
|
+
* same build (#633): seeds the hints and the refusals. */
|
|
7673
|
+
citing = { hints: new Map(), refused: new Set() }) {
|
|
6463
7674
|
// Each pass reports its own progress, numbered in build order.
|
|
6464
7675
|
let passIndex = 0;
|
|
6465
7676
|
const onProgress = options?.onProgress;
|
|
@@ -6470,12 +7681,28 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6470
7681
|
// Side captions moved under their figure (see `PassHints.captionUnder`):
|
|
6471
7682
|
// once a pass asks for one, every later pass keeps it.
|
|
6472
7683
|
const captionUnder = new Set();
|
|
7684
|
+
// Loose paragraphs any pass found would gain their line only off the
|
|
7685
|
+
// character grid (#632): the columns they leave short say so.
|
|
7686
|
+
const looseOffGrid = new Set();
|
|
7687
|
+
// Floats heading the page or column that cites them (#633,
|
|
7688
|
+
// `PassHints.citingPageFloats`): once a pass keeps one there, every later
|
|
7689
|
+
// pass opens that page with it; a float refused once (its citing line
|
|
7690
|
+
// left the page, or it could not be set there) is never proposed again.
|
|
7691
|
+
const citingPage = citing.hints;
|
|
7692
|
+
const citingRefused = citing.refused;
|
|
6473
7693
|
const runPass = (hints) => {
|
|
6474
7694
|
passIndex++;
|
|
6475
7695
|
const started = onPass ? now() : 0;
|
|
6476
|
-
const result = buildDocumentPass(content, config, cache, passOptions, {
|
|
7696
|
+
const result = buildDocumentPass(content, config, cache, passOptions, {
|
|
7697
|
+
...hints,
|
|
7698
|
+
captionUnder,
|
|
7699
|
+
...(footnoteNumbers ? { footnoteNumbers } : {}),
|
|
7700
|
+
...(citingPage.size > 0 ? { citingPageFloats: new Map(citingPage) } : {}),
|
|
7701
|
+
});
|
|
6477
7702
|
for (const id of result.captionUnderProposals)
|
|
6478
7703
|
captionUnder.add(id);
|
|
7704
|
+
for (const k of result.looseOffGrid)
|
|
7705
|
+
looseOffGrid.add(k);
|
|
6479
7706
|
onPass?.({ pass: passIndex, tocRound, ms: now() - started, pages: result.doc.pages.length });
|
|
6480
7707
|
return result;
|
|
6481
7708
|
};
|
|
@@ -6492,12 +7719,55 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6492
7719
|
if (captionUnder.size === before)
|
|
6493
7720
|
break;
|
|
6494
7721
|
}
|
|
7722
|
+
// --- Floats at the head of their citing page (#633) --------------------
|
|
7723
|
+
// A float that may head the page (column) its citing line lands on and
|
|
7724
|
+
// went on to a later one is proposed there; the next pass opens that
|
|
7725
|
+
// page with it. A pass that could not keep one there (the text it pushed
|
|
7726
|
+
// down took the citing line to the next page) drops it for good. A few
|
|
7727
|
+
// rounds at most, as the side captions get; then any hint still refused
|
|
7728
|
+
// is dropped, each pass with fewer, until every one left holds.
|
|
7729
|
+
/** Drop the hints `r` could not keep; whether there were any. */
|
|
7730
|
+
const dropRefusedCiting = (r) => {
|
|
7731
|
+
let dropped = false;
|
|
7732
|
+
for (const id of r.citingPageRefused.keys()) {
|
|
7733
|
+
citingRefused.add(id);
|
|
7734
|
+
if (citingPage.delete(id))
|
|
7735
|
+
dropped = true;
|
|
7736
|
+
}
|
|
7737
|
+
return dropped;
|
|
7738
|
+
};
|
|
7739
|
+
for (let round = 0; round < MAX_CITING_PAGE_ROUNDS; round++) {
|
|
7740
|
+
let changed = dropRefusedCiting(initial);
|
|
7741
|
+
for (const [id, slot] of initial.citingPageProposals) {
|
|
7742
|
+
if (citingRefused.has(id) || citingPage.has(id))
|
|
7743
|
+
continue;
|
|
7744
|
+
citingPage.set(id, slot);
|
|
7745
|
+
changed = true;
|
|
7746
|
+
}
|
|
7747
|
+
if (!changed)
|
|
7748
|
+
break;
|
|
7749
|
+
initial = runPass();
|
|
7750
|
+
yield;
|
|
7751
|
+
}
|
|
7752
|
+
while (dropRefusedCiting(initial)) {
|
|
7753
|
+
initial = runPass();
|
|
7754
|
+
yield;
|
|
7755
|
+
}
|
|
6495
7756
|
// --- Band caps (page-span blocks mid-page) -----------------------------
|
|
6496
7757
|
// A span block that arrived in an uneven band proposes a cap; the driver
|
|
6497
7758
|
// re-places the document with it (and grows / drops caps whose band
|
|
6498
7759
|
// overflowed) before balancing runs. Documents without such blocks get
|
|
6499
7760
|
// their first pass back untouched — no extra pass.
|
|
6500
|
-
|
|
7761
|
+
let bands = yield* resolveBandCapsGen(initial, (bandCaps) => runPass({ bandCaps }));
|
|
7762
|
+
// A cap that took a citing line off the page its float heads: drop that
|
|
7763
|
+
// float's hint and settle the caps again without it.
|
|
7764
|
+
while (dropRefusedCiting(bands.result)) {
|
|
7765
|
+
initial = runPass();
|
|
7766
|
+
yield;
|
|
7767
|
+
const passes = bands.passCount;
|
|
7768
|
+
bands = yield* resolveBandCapsGen(initial, (bandCaps) => runPass({ bandCaps }));
|
|
7769
|
+
bands = { ...bands, passCount: bands.passCount + passes + 1 };
|
|
7770
|
+
}
|
|
6501
7771
|
let best = bands.result;
|
|
6502
7772
|
const bandCaps = bands.bandCaps;
|
|
6503
7773
|
let passCount = bands.passCount;
|
|
@@ -6523,8 +7793,20 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6523
7793
|
// balances exactly as its chapters would one by one, instead of one
|
|
6524
7794
|
// cascade anywhere costing every page of the book a pass.
|
|
6525
7795
|
const balancing = best.doc.config.headings.balancing;
|
|
6526
|
-
|
|
7796
|
+
const onGrid = best.doc.config.cjk.grid.enabled;
|
|
7797
|
+
// Why the columns of a page on a character grid may end short (#632):
|
|
7798
|
+
// balancing off by the grid's default (vertical text has it off by its
|
|
7799
|
+
// own), or its whole-line levers kept out.
|
|
7800
|
+
const gridBalancing = onGrid
|
|
7801
|
+
? (!balancing.enabled
|
|
7802
|
+
? (config?.headings?.balancing?.enabled === undefined && best.doc.config.layout.writingMode !== 'vertical-rl' ? { off: true } : undefined)
|
|
7803
|
+
: (balancing.gridLines === 'off' ? { gridLines: 'off' } : undefined))
|
|
7804
|
+
: undefined;
|
|
7805
|
+
if (!balancing.enabled) {
|
|
7806
|
+
if (gridBalancing)
|
|
7807
|
+
best.doc.gridBalancing = gridBalancing;
|
|
6527
7808
|
return best.doc;
|
|
7809
|
+
}
|
|
6528
7810
|
/** Levers accepted so far, over every segment (keyed by content index /
|
|
6529
7811
|
* `balanceKey`; each key belongs to exactly one segment). */
|
|
6530
7812
|
const applied = { lines: new Map(), loose: new Map() };
|
|
@@ -6644,6 +7926,11 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6644
7926
|
if (!sameContent(blocks, next.doc.blocks))
|
|
6645
7927
|
return best;
|
|
6646
7928
|
const doc = { ...best.doc, pages, blocks, ...(warnings.length > 0 ? { warnings } : { warnings: undefined }) };
|
|
7929
|
+
// It holds what `next` holds: what `next` placed nowhere is still
|
|
7930
|
+
// missing from it (#639).
|
|
7931
|
+
const unplaced = unplacedByDoc.get(next.doc);
|
|
7932
|
+
if (unplaced)
|
|
7933
|
+
unplacedByDoc.set(doc, unplaced);
|
|
6647
7934
|
const pageBest = pageOfContent(best.doc);
|
|
6648
7935
|
const pageNext = pageOfContent(next.doc);
|
|
6649
7936
|
const mergeMap = (a, b) => {
|
|
@@ -6673,7 +7960,10 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6673
7960
|
spanPlacedInBand: mergeSet(best.spanPlacedInBand, next.spanPlacedInBand),
|
|
6674
7961
|
bandCapsApplied: mergeSet(best.bandCapsApplied, next.bandCapsApplied),
|
|
6675
7962
|
looseOutcome: mergeMap(best.looseOutcome, next.looseOutcome),
|
|
7963
|
+
looseOffGrid: new Set([...best.looseOffGrid, ...next.looseOffGrid]),
|
|
6676
7964
|
captionUnderProposals: new Set([...best.captionUnderProposals, ...next.captionUnderProposals]),
|
|
7965
|
+
citingPageProposals: best.citingPageProposals,
|
|
7966
|
+
citingPageRefused: best.citingPageRefused,
|
|
6677
7967
|
};
|
|
6678
7968
|
};
|
|
6679
7969
|
/**
|
|
@@ -6743,6 +8033,7 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6743
8033
|
failedLoose,
|
|
6744
8034
|
failedLines,
|
|
6745
8035
|
closingBox: balancing.closingBox,
|
|
8036
|
+
...(onGrid && balancing.gridLines === 'off' ? { gridLines: false } : {}),
|
|
6746
8037
|
});
|
|
6747
8038
|
// The levers newly proposed, by segment; those of a segment that is
|
|
6748
8039
|
// done (plateaued, out of attempts) are withdrawn from the pass.
|
|
@@ -6808,7 +8099,10 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6808
8099
|
// a levelled closing band spills past its cut) is a regression —
|
|
6809
8100
|
// capped columns without their box are not a layout we may keep.
|
|
6810
8101
|
const capsDelivered = [...bandCaps.keys()].every((i) => !inRange(s.range, capPage.get(i)) || next.spanPlacedInBand.has(i));
|
|
6811
|
-
|
|
8102
|
+
// So is one that loses a float heading its citing page (#633): the
|
|
8103
|
+
// levers pushed its citing line off it.
|
|
8104
|
+
const citingKept = ![...next.citingPageRefused.keys()].some((id) => inRange(s.range, citingPage.get(id)?.pageIndex));
|
|
8105
|
+
const score = capsDelivered && citingKept && wholeIntact(next, s, si === segments.length - 1)
|
|
6812
8106
|
? gapLinesIn(nextGaps, s.range)
|
|
6813
8107
|
: Infinity;
|
|
6814
8108
|
// Loose paragraphs that gained no line at any tracking rung are
|
|
@@ -6891,7 +8185,9 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6891
8185
|
const frozen = hintsFrom(applied.lines, applied.loose);
|
|
6892
8186
|
const trailing = yield* resolveTrailingCapsGen(best, bandCaps, (caps) => runPass({ ...frozen, bandCaps: caps, floatPages: floatPagesOf(best.doc) }));
|
|
6893
8187
|
passCount += trailing.passCount;
|
|
6894
|
-
|
|
8188
|
+
// A cap that takes a citing line off the page its float heads (#633)
|
|
8189
|
+
// is not kept.
|
|
8190
|
+
if (trailing.result !== best && trailing.result.citingPageRefused.size === 0) {
|
|
6895
8191
|
best = trailing.result;
|
|
6896
8192
|
for (const [i, cap] of trailing.caps)
|
|
6897
8193
|
bandCaps.set(i, cap);
|
|
@@ -6907,8 +8203,41 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6907
8203
|
}
|
|
6908
8204
|
best.doc.iterationCount = passCount;
|
|
6909
8205
|
best.doc.converged = segments.every((s) => s.stable || s.bestScore === 0);
|
|
8206
|
+
if (onGrid)
|
|
8207
|
+
markGridRefusals(best.doc, best.forcedBreakPages, looseOffGrid, gridBalancing);
|
|
6910
8208
|
return best.doc;
|
|
6911
8209
|
}
|
|
8210
|
+
/**
|
|
8211
|
+
* Column balancing on a character grid (#632): stamp the document with why
|
|
8212
|
+
* its balancing is limited (`VDTDocument.gridBalancing`) and every column
|
|
8213
|
+
* still short with the levers the grid kept from it
|
|
8214
|
+
* (`VDTColumn.gridRefused`): the loose paragraphs that would have gained
|
|
8215
|
+
* their line off the grid (`offGrid`), and the whole-line levers under
|
|
8216
|
+
* `headings.balancing.gridLines: 'off'`.
|
|
8217
|
+
*/
|
|
8218
|
+
function markGridRefusals(doc, forcedBreakPages, offGrid, gridBalancing) {
|
|
8219
|
+
if (gridBalancing)
|
|
8220
|
+
doc.gridBalancing = gridBalancing;
|
|
8221
|
+
const noLines = gridBalancing?.gridLines === 'off';
|
|
8222
|
+
for (const gap of collectColumnGaps(doc, forcedBreakPages)) {
|
|
8223
|
+
if (gap.gapLines < 1)
|
|
8224
|
+
continue;
|
|
8225
|
+
const refused = new Set();
|
|
8226
|
+
for (const c of gap.candidates) {
|
|
8227
|
+
if (noLines && GRID_LINE_LEVERS.includes(c.kind))
|
|
8228
|
+
refused.add(c.kind);
|
|
8229
|
+
else if (c.kind === 'looseParagraph' && offGrid.has(c.contentIndex))
|
|
8230
|
+
refused.add(c.kind);
|
|
8231
|
+
}
|
|
8232
|
+
if (refused.size === 0)
|
|
8233
|
+
continue;
|
|
8234
|
+
const col = doc.pages[gap.pageIndex]?.columns[gap.columnIndex];
|
|
8235
|
+
if (col)
|
|
8236
|
+
col.gridRefused = BALANCE_LEVER_ORDER.filter((l) => refused.has(l));
|
|
8237
|
+
}
|
|
8238
|
+
}
|
|
8239
|
+
/** The balancing levers in the order `BalanceLever` lists them. */
|
|
8240
|
+
const BALANCE_LEVER_ORDER = ['trailingCallout', 'flexFigure', 'heading', 'listEnd', 'afterDisplay', 'afterFloat', 'looseParagraph'];
|
|
6912
8241
|
/** Whether two lines set one bayt of a poem (`VDTLine.verse`), which a
|
|
6913
8242
|
* column never cuts. */
|
|
6914
8243
|
function sameBayt(a, b) {
|