postext 1.24.0 → 1.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- 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/contextDefaultsBundle.test.d.ts +2 -0
- package/dist/__tests__/bundle/contextDefaultsBundle.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/contextDefaultsBundle.test.js +104 -0
- package/dist/__tests__/bundle/contextDefaultsBundle.test.js.map +1 -0
- package/dist/__tests__/bundle/designOverflowPin.test.js +4 -2
- package/dist/__tests__/bundle/designOverflowPin.test.js.map +1 -1
- 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 +72 -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__/defaults/stripContextDefaults.test.d.ts +2 -0
- package/dist/__tests__/defaults/stripContextDefaults.test.d.ts.map +1 -0
- package/dist/__tests__/defaults/stripContextDefaults.test.js +265 -0
- package/dist/__tests__/defaults/stripContextDefaults.test.js.map +1 -0
- package/dist/__tests__/design/cjkDesignDash.test.d.ts +2 -0
- package/dist/__tests__/design/cjkDesignDash.test.d.ts.map +1 -0
- package/dist/__tests__/design/cjkDesignDash.test.js +395 -0
- package/dist/__tests__/design/cjkDesignDash.test.js.map +1 -0
- 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__/exports.test.js +49 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/hardBreaks.test.js +1 -1
- package/dist/__tests__/headingInlineMarks.test.js +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__/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/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/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/compoundBreaks.test.js +1 -1
- package/dist/__tests__/pipeline/dashBreaks.test.js +1 -1
- 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/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/inlineFigureSpace.test.js +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/raggedOptimal.test.js +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__/prepareFonts.test.d.ts +2 -0
- package/dist/__tests__/prepareFonts.test.d.ts.map +1 -0
- package/dist/__tests__/prepareFonts.test.js +548 -0
- package/dist/__tests__/prepareFonts.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__/verseLines.test.js +1 -1
- package/dist/__tests__/vertical/measurePaint.test.js +16 -0
- package/dist/__tests__/vertical/measurePaint.test.js.map +1 -1
- 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.map +1 -1
- package/dist/bundle/codec.js +37 -4
- package/dist/bundle/codec.js.map +1 -1
- package/dist/bundle/configVersion.d.ts +127 -10
- package/dist/bundle/configVersion.d.ts.map +1 -1
- package/dist/bundle/configVersion.js +260 -9
- package/dist/bundle/configVersion.js.map +1 -1
- package/dist/bundle/index.d.ts +3 -3
- package/dist/bundle/index.d.ts.map +1 -1
- package/dist/bundle/index.js +2 -2
- package/dist/bundle/index.js.map +1 -1
- package/dist/bundle/manifest.d.ts.map +1 -1
- package/dist/bundle/manifest.js +3 -2
- package/dist/bundle/manifest.js.map +1 -1
- package/dist/bundle/types.d.ts +4 -0
- package/dist/bundle/types.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.js +18 -3
- package/dist/canvas-backend/headerFooter.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 +1 -1
- package/dist/configWarnings.js.map +1 -1
- package/dist/defaults/bodyText.d.ts +2 -1
- package/dist/defaults/bodyText.d.ts.map +1 -1
- package/dist/defaults/bodyText.js +13 -2
- package/dist/defaults/bodyText.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/headings.d.ts +22 -2
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +34 -2
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/index.d.ts +11 -2
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +34 -9
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/indexConfig.d.ts +6 -2
- package/dist/defaults/indexConfig.d.ts.map +1 -1
- package/dist/defaults/indexConfig.js +10 -5
- package/dist/defaults/indexConfig.js.map +1 -1
- package/dist/defaults/layout.d.ts +5 -0
- package/dist/defaults/layout.d.ts.map +1 -1
- package/dist/defaults/layout.js +32 -0
- package/dist/defaults/layout.js.map +1 -1
- package/dist/defaults/page.d.ts.map +1 -1
- package/dist/defaults/page.js +8 -2
- package/dist/defaults/page.js.map +1 -1
- package/dist/defaults/tableStyle.d.ts.map +1 -1
- package/dist/defaults/tableStyle.js +6 -0
- package/dist/defaults/tableStyle.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 +88 -0
- package/dist/design/cjkText.js.map +1 -0
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +103 -5
- package/dist/design/layout.js.map +1 -1
- package/dist/design/richText.d.ts +28 -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 +101 -0
- package/dist/fonts/faces.d.ts.map +1 -0
- package/dist/fonts/faces.js +282 -0
- package/dist/fonts/faces.js.map +1 -0
- package/dist/fonts/fontSet.d.ts +37 -0
- package/dist/fonts/fontSet.d.ts.map +1 -0
- package/dist/fonts/fontSet.js +181 -0
- package/dist/fonts/fontSet.js.map +1 -0
- package/dist/fonts/prepare.d.ts +125 -0
- package/dist/fonts/prepare.d.ts.map +1 -0
- package/dist/fonts/prepare.js +380 -0
- package/dist/fonts/prepare.js.map +1 -0
- package/dist/html-backend.d.ts +17 -0
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +98 -4
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +25 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.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 +119 -7
- 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/index.d.ts +4 -1
- package/dist/measure/index.d.ts.map +1 -1
- package/dist/measure/index.js +3 -1
- package/dist/measure/index.js.map +1 -1
- package/dist/measure/types.d.ts +4 -0
- package/dist/measure/types.d.ts.map +1 -1
- 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 +1065 -193
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
- package/dist/pipeline/buildBlockKind.js +20 -6
- 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 +3 -2
- 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 +22 -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 +16 -0
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +51 -9
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/contentWarnings.d.ts.map +1 -1
- package/dist/pipeline/contentWarnings.js +28 -0
- package/dist/pipeline/contentWarnings.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +16 -2
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js +12 -4
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/floatSlots.d.ts +9 -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/measureContentBlock.d.ts +13 -1
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +20 -5
- 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 +5 -2
- package/dist/pipeline/placement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +26 -0
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +53 -3
- package/dist/pipeline/resourceLayout.js.map +1 -1
- 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/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/types.d.ts +145 -8
- 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 +127 -3
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.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
|
@@ -15,7 +15,10 @@ import { stampCentralBaselines } from './verticalMetrics';
|
|
|
15
15
|
import { orientLinesToFrames } from './mirrorFrame';
|
|
16
16
|
import { verseMarginTopPx } from './buildBlockKind';
|
|
17
17
|
import { verseLinesSettings, versesLineByLine } from './verseLines';
|
|
18
|
-
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';
|
|
19
22
|
import { asciiDigits } from '../arabicNumerals';
|
|
20
23
|
import { getMeasureDirection, setMeasureDirection, shiftLineX } from '../measure/bidiLines';
|
|
21
24
|
import { createHeadingLevelResolver, deriveSectionGeometryConfig, sectionWritingMode, deriveSectionMeasureContext, headingIsHidden, headingIsNumbered, headingMarksFor, headingStyleOf, numberTitlesFor, planHeadingSections, } from './headingStyles';
|
|
@@ -25,12 +28,13 @@ import { bookCitationContexts, needsCitationContext } from '../citations/context
|
|
|
25
28
|
import { anchorTargetsOf, hasAnchorRefs, printsAnchorPages } from './crossRefs';
|
|
26
29
|
import { applyStatementNumbering, numberStatements, outlineLookup } from './statementNumbering';
|
|
27
30
|
import { SPAN_EMBED_STYLE_ID, isPageSpanStrip, spanEmbedStyle, wrapPageSpanEmbeds } from './spanEmbeds';
|
|
31
|
+
import { flowColumnsStyles, isFlowColumnsStyle, wrapFlowColumns } from './flowColumns';
|
|
28
32
|
import { anchorOutline, computeOutline, hasIndexDirective, hasTocDirective, headingNumberingOptions, headingTemplatesOf, outlineFromDoc, sameOutline } from './outline';
|
|
29
33
|
import { expandTocDirectives } from './toc';
|
|
30
34
|
import { expandIndexDirectives, locateIndexMarks } from './indexDirective';
|
|
31
35
|
import { resolveBodyStyle, resolveBlockquoteStyle, resolveParagraphStyle } from './styles';
|
|
32
36
|
import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, listBulletPosition, listItemGapPx, listItemSpacingPx, mirrorListMarker, } from './lists';
|
|
33
|
-
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';
|
|
34
38
|
import { chooseParagraphSplit } from './orphanWidow';
|
|
35
39
|
import { layoutComicPage, comicPageLayoutWarnings } from '../comics/layoutPage';
|
|
36
40
|
import { comicStripExtent, comicStripOffset, comicStripPlacement, comicStripWidth, layoutComicStrip } from '../comics/strip';
|
|
@@ -41,7 +45,8 @@ import { containerStyle, paragraphStyleIdOf, planParagraphContainers } from './p
|
|
|
41
45
|
import { appendChapterEndNotes, footnoteIdsOfLines, footnoteParagraphStyle, noteContentBlock, noteNumberPositionOf, numberFootnotes, footnoteFormatOf, numberFootnotesByPlacement, sameFootnoteNumbers, splitFootnoteDefinitions, withFootnoteStyle, } from './footnotes';
|
|
42
46
|
import { planParts, derivePartMeasureContext } from './parts';
|
|
43
47
|
import { layoutCallout, offsetBlock, offsetCalloutToAbsolute, pickCalloutStyle, planCallouts, resolveCalloutAttrs, } from './calloutLayout';
|
|
44
|
-
import {
|
|
48
|
+
import { fillGalley, galleyBreaks } from './columnsGroup';
|
|
49
|
+
import { chooseTableSlice, layoutCaptionText, layoutResourceBlock, MIN_TAIL_ROWS } from './resourceLayout';
|
|
45
50
|
import { computeFloatPlan, floatedResourceIds, floatColumnCount, } from './floatPlacement';
|
|
46
51
|
import { enumerateCurrentPageSlots, measureFloatBand, floatGapAbove, floatGapBelow, measureSideStack, clearSideObstacles, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
|
|
47
52
|
import { computeHeadingContext, computeResourceNumberingState, } from './resourceNumbering';
|
|
@@ -55,12 +60,13 @@ import { buildLineNumbers, lineNumberWarnings } from './lineNumbers';
|
|
|
55
60
|
import { buildHeadersAndFooters, defaultOpenerTitle, headingDesignBoxes, measureDefaultOpenerHeight, measureHeadingDesign } from './headerFooter';
|
|
56
61
|
import { flowColorValues } from './partPalette';
|
|
57
62
|
import { chapterNumberCounter, leadingBoldText } from './placeholders';
|
|
58
|
-
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';
|
|
59
64
|
import { applyBandCap, uncapBand, columnBottom, bandCapLines, bandTop, bareBandTop, resolveBandCapsGen, drainPasses, resolveTrailingCapsGen, bandCapLinesAroundZone, } from './bandCaps';
|
|
60
65
|
import { raggedLooseLines } from './raggedLines';
|
|
61
66
|
import { cjkLooseLineWarnings, collectContentWarnings, designTruncationWarnings, dropCapWarnings, floatShrinkWarnings, joiningLetterSpacingWarnings, locateContentWarnings, wordOverflowWarnings } from './contentWarnings';
|
|
62
67
|
import { dropCapSinkAfter } from './dropCap';
|
|
63
68
|
import { mostlyJoiningScript } from '../measure/joining';
|
|
69
|
+
import { composesAsCjk } from '../measure/cjkCompose';
|
|
64
70
|
import { annotateDocument } from '../cjkMarks';
|
|
65
71
|
import { annotateArabicMarks } from '../arabicMarks';
|
|
66
72
|
import { literalBreaksFor } from './hardBreaks';
|
|
@@ -71,9 +77,6 @@ import { paperByBlock, stampPagePaper } from './paper';
|
|
|
71
77
|
import { headingLineSpanBand } from './lineSpan';
|
|
72
78
|
import { columnCursorY, insetsBefore, minTextWidthPx, openColumnWrap, resolveCalloutWrap, resolveResourceWrap, runsBesideWrap, sameInsetsFrom, settleColumnWraps, transparentToWrap, wrapGapPx, wrapInsetSteps } from './textWrap';
|
|
73
79
|
import { lineInsetsAt } from '../measure/types';
|
|
74
|
-
/** Tolerance for "does this block fit" checks against a column's free
|
|
75
|
-
* height, absorbing floating-point drift between grid multiples. */
|
|
76
|
-
const FIT_EPS = 0.01;
|
|
77
80
|
/** Room (px) under closing boxes a balancing pass must close to be kept on
|
|
78
81
|
* that ground alone (see `boxRoomIn`). */
|
|
79
82
|
const BOX_ROOM_EPS_PX = 0.5;
|
|
@@ -101,6 +104,16 @@ function floatPagesOf(doc) {
|
|
|
101
104
|
}
|
|
102
105
|
return out;
|
|
103
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
|
+
}
|
|
104
117
|
/** `{chapterNumber}` at each content block: the value of the last level-1
|
|
105
118
|
* heading at or before it ('' before the first), by the counter the
|
|
106
119
|
* running heads use over the placed blocks (`computeChapterNumbers`) —
|
|
@@ -148,7 +161,7 @@ function chapterNumbersOfContent(blocks, resolved, prefixes, values, ordinalOffs
|
|
|
148
161
|
* line less (EF-129: asked for one more, it could only match the target by
|
|
149
162
|
* falling back to its optimum, unchecked).
|
|
150
163
|
*/
|
|
151
|
-
function measureLooseParagraph(rawBlock, blockIdx, columnWidth, ctx, styleOverride, extraLines, trackingLadder, looseOutcome) {
|
|
164
|
+
function measureLooseParagraph(rawBlock, blockIdx, columnWidth, ctx, styleOverride, extraLines, trackingLadder, looseOutcome, offGrid) {
|
|
152
165
|
const base = measureContentBlock(rawBlock, blockIdx, columnWidth, ctx, { styleOverride });
|
|
153
166
|
if (!base)
|
|
154
167
|
return null;
|
|
@@ -165,25 +178,39 @@ function measureLooseParagraph(rawBlock, blockIdx, columnWidth, ctx, styleOverri
|
|
|
165
178
|
// A paragraph mostly in a joining script (Arabic…) is not tracked: its
|
|
166
179
|
// words take no letter-spacing (`measure/joining.ts`), so only the first
|
|
167
180
|
// rung, word spacing alone, can gain it the line.
|
|
168
|
-
|
|
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 } : {};
|
|
169
189
|
const maxWordSpacing = ctx.resolved.bodyText.maxWordSpacing + 1e-9;
|
|
170
190
|
// A CJK line spread past its tracking cap (`cjkLoose`) is past the
|
|
171
191
|
// limit too.
|
|
172
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);
|
|
173
197
|
for (const tracking of ladder) {
|
|
174
198
|
const loose = measureContentBlock(rawBlock, blockIdx, columnWidth, ctx, {
|
|
175
199
|
styleOverride,
|
|
176
200
|
looseness,
|
|
177
201
|
trackingEm: tracking > 0 ? tracking / 1000 : undefined,
|
|
202
|
+
...gridCap,
|
|
178
203
|
});
|
|
179
|
-
if (loose
|
|
180
|
-
&& loose.measured.lines.length === target
|
|
181
|
-
&& !loose.measured.lastLineRunt
|
|
182
|
-
&& withinLimit(loose.measured.lines)) {
|
|
204
|
+
if (gains(loose)) {
|
|
183
205
|
looseOutcome.set(blockIdx, tracking);
|
|
184
206
|
return loose;
|
|
185
207
|
}
|
|
186
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
|
+
}
|
|
187
214
|
looseOutcome.set(blockIdx, null);
|
|
188
215
|
return base;
|
|
189
216
|
}
|
|
@@ -220,7 +247,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
220
247
|
}
|
|
221
248
|
}
|
|
222
249
|
function placeDocumentPass(content, config, cache, options, hints) {
|
|
223
|
-
const { balanceExtraPx, balanceLooseness, balanceLooseBudget, bandCaps, captionUnder, floatPages } = hints;
|
|
250
|
+
const { balanceExtraPx, balanceLooseness, balanceLooseBudget, bandCaps, captionUnder, floatPages, citingPageFloats } = hints;
|
|
224
251
|
const captionUnderProposals = new Set();
|
|
225
252
|
/** Side columns whose foot a figure's side caption has cut, by the figure. */
|
|
226
253
|
const asideCutBy = new WeakMap();
|
|
@@ -247,6 +274,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
247
274
|
trackingLadder.push(balancingCfg.maxTracking);
|
|
248
275
|
}
|
|
249
276
|
const looseOutcome = new Map();
|
|
277
|
+
const looseOffGrid = new Set();
|
|
250
278
|
// Lines gained so far per column budget group (see `LooseBudget`).
|
|
251
279
|
const looseGained = new Map();
|
|
252
280
|
// Level configs with heading-style overrides merged in (`{style="…"}`).
|
|
@@ -400,7 +428,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
400
428
|
// An inline `::resource` placed `here` with `span: 'page'` spans the
|
|
401
429
|
// page where it stands (#535): each goes in a frameless page-span box,
|
|
402
430
|
// set by the span-block path like any other.
|
|
403
|
-
|
|
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 ?? []));
|
|
404
435
|
// Code listings (#624): each in a box of the code style, which the
|
|
405
436
|
// callout machinery places and splits.
|
|
406
437
|
const codeBoxes = wrapCodeBlocks(spanEmbeds.blocks, resolved);
|
|
@@ -445,13 +476,18 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
445
476
|
const syntheticBoxes = [
|
|
446
477
|
...(spanEmbeds.wrapped ? [spanEmbedStyle(bodyStyle.lineHeightPx, resolved.layout.inlineResourceGap !== 'above')] : []),
|
|
447
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
|
+
: []),
|
|
448
484
|
];
|
|
449
485
|
const calloutStyles = syntheticBoxes.length > 0
|
|
450
486
|
? [...resolved.calloutStyles, ...resolveCalloutStylesConfig(syntheticBoxes, resolved.bodyText, resolved.headings, resolved.unorderedLists, config?.locale)]
|
|
451
487
|
: resolved.calloutStyles;
|
|
452
488
|
// A box nested in another looks its style up in the configuration it is
|
|
453
489
|
// laid out with: the listings' box among them.
|
|
454
|
-
const calloutResolved = codeBoxes.wrapped ? { ...resolved, calloutStyles } : resolved;
|
|
490
|
+
const calloutResolved = codeBoxes.wrapped || flowGroups.wrapped ? { ...resolved, calloutStyles } : resolved;
|
|
455
491
|
const blockquoteStyle = resolveBlockquoteStyle(resolved);
|
|
456
492
|
const listLevelIndentsPx = computeLevelIndentsPx(resolved, bodyStyle.fontSizePx);
|
|
457
493
|
const orderedMetrics = computeOrderedListRunMetrics(contentBlocks, resolved, bodyStyle.fontSizePx);
|
|
@@ -482,7 +518,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
482
518
|
// it asks for is not applied there. Decided per block, by the writing
|
|
483
519
|
// mode of the styled section the reference sits in (a horizontal
|
|
484
520
|
// appendix of a vertical book turns its figures as asked).
|
|
485
|
-
const floatPlan = computeFloatPlan(contentBlocks, resources, resourceTypes, incorporated, (blockIdx) => sectionWritingMode(sectionPlan, resolved, blockIdx) === 'vertical-rl', resolved.layout.floatShrink, resolved.layout.wrap);
|
|
521
|
+
const floatPlan = computeFloatPlan(contentBlocks, resources, resourceTypes, incorporated, (blockIdx) => sectionWritingMode(sectionPlan, resolved, blockIdx) === 'vertical-rl', resolved.layout.floatShrink, resolved.layout.wrap, resolved.layout.floatsAtCitingPage);
|
|
486
522
|
const floatedIds = floatedResourceIds(floatPlan, incorporated, resources, resourceTypes);
|
|
487
523
|
const floatsByFirstBlock = new Map();
|
|
488
524
|
for (const f of floatPlan) {
|
|
@@ -499,6 +535,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
499
535
|
* page: they take the side column of the next page the flow opens, in
|
|
500
536
|
* order (see `placeCalloutSide`). */
|
|
501
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;
|
|
502
541
|
/** `span: 'side'` boxes of a style with `sideAtColumnEnd: 'after'`
|
|
503
542
|
* (EF-161), and the side boxes fenced behind them, waiting for the text
|
|
504
543
|
* after their fence to land (see `settleAwaitingSideBoxes`). Each keeps
|
|
@@ -884,8 +923,6 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
884
923
|
};
|
|
885
924
|
const floatGapPx = bodyStyle.lineHeightPx;
|
|
886
925
|
const minTextPx = bodyStyle.lineHeightPx * 3;
|
|
887
|
-
/** Fewest body rows the closing slice of a split table carries. */
|
|
888
|
-
const MIN_TAIL_ROWS = 3;
|
|
889
926
|
/** Offset a resolved resource block's caption/table geometry from
|
|
890
927
|
* block-relative to absolute page coordinates (mirrors inline placement). */
|
|
891
928
|
const offsetResourceBlockToAbsolute = (rb, ox, oy) => {
|
|
@@ -1244,28 +1281,6 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1244
1281
|
// A top band rounds up to the grid: the tallest float it holds within
|
|
1245
1282
|
// `avail` is the grid multiple below it, less the gap.
|
|
1246
1283
|
const hMax = Math.floor((avail + 0.01) / baselineGrid) * baselineGrid - floatGapPx;
|
|
1247
|
-
let end = planTableSlice(metrics, startRow, (rotated ? width : hMax) - overhead);
|
|
1248
|
-
const floor = firstBody + 1;
|
|
1249
|
-
if (end < floor) {
|
|
1250
|
-
// Nothing fits. A fresh page carries the smallest slice anyway (it
|
|
1251
|
-
// overflows, as a dominating figure would) rather than stall the
|
|
1252
|
-
// queue; a slot of the current page is simply not this table's.
|
|
1253
|
-
if (mode === 'strict')
|
|
1254
|
-
return 'none';
|
|
1255
|
-
end = floor;
|
|
1256
|
-
}
|
|
1257
|
-
// A last page holding a row or two under a repeated header reads as a
|
|
1258
|
-
// stranded tail: give the closing slice at least `MIN_TAIL_ROWS` rows by
|
|
1259
|
-
// handing some back from this one (at a breakable edge, not under a
|
|
1260
|
-
// group head), when this slice can spare them.
|
|
1261
|
-
const tail = rowCount - end;
|
|
1262
|
-
if (overflow === 'split' && tail > 0 && tail < MIN_TAIL_ROWS) {
|
|
1263
|
-
let e = rowCount - MIN_TAIL_ROWS;
|
|
1264
|
-
while (e > floor && (!(metrics.breakableAfter[e - 1] ?? true) || metrics.groupHeaderRow[e - 1]))
|
|
1265
|
-
e--;
|
|
1266
|
-
if (e >= floor && e >= end - MIN_TAIL_ROWS)
|
|
1267
|
-
end = e;
|
|
1268
|
-
}
|
|
1269
1284
|
const fits = (slice) => {
|
|
1270
1285
|
const measure = measureFloat(f.resourceId, width, slice, rotated);
|
|
1271
1286
|
if (!measure)
|
|
@@ -1275,23 +1290,18 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1275
1290
|
const { need } = measureFloatBand(position, measure, targetCols, contentArea, baselineGrid, floatGapPx, (c) => trueBottom(c, uncappedBottoms));
|
|
1276
1291
|
return need <= avail + 0.01;
|
|
1277
1292
|
};
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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,
|
|
1282
1301
|
});
|
|
1283
|
-
|
|
1284
|
-
// The caption may wrap differently with its suffix, the closing slice
|
|
1285
|
-
// carries the note instead of the marker: verify, backing off a row at
|
|
1286
|
-
// a time (over breakable edges) when the band still overflows.
|
|
1287
|
-
for (let guard = 0; guard < 8 && end > floor && !fits(slice); guard++) {
|
|
1288
|
-
do
|
|
1289
|
-
end--;
|
|
1290
|
-
while (end > floor && !(metrics.breakableAfter[end - 1] ?? true));
|
|
1291
|
-
slice = sliceFor(end);
|
|
1292
|
-
}
|
|
1293
|
-
if (mode === 'strict' && !fits(slice))
|
|
1302
|
+
if (!slice)
|
|
1294
1303
|
return 'none';
|
|
1304
|
+
const end = slice.endRow;
|
|
1295
1305
|
const rest = slice.continues ? { ...f, startRow: end } : undefined;
|
|
1296
1306
|
return rest ? { slice, rest } : { slice };
|
|
1297
1307
|
};
|
|
@@ -1495,6 +1505,25 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1495
1505
|
let cut = false;
|
|
1496
1506
|
/** A rotated block wider than the band (its upright height). */
|
|
1497
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
|
+
};
|
|
1498
1527
|
if (side) {
|
|
1499
1528
|
// The side column's stack: the float goes under what the column holds
|
|
1500
1529
|
// when the rest of the column takes it; otherwise it waits for the
|
|
@@ -1534,6 +1563,13 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1534
1563
|
return 'skip';
|
|
1535
1564
|
first.availableHeight = Math.max(0, first.availableHeight - need);
|
|
1536
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
|
+
}
|
|
1537
1573
|
floatsPlaced++;
|
|
1538
1574
|
return 'placed';
|
|
1539
1575
|
}
|
|
@@ -1607,7 +1643,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1607
1643
|
// to another band, as `fitsStrict` keeps it); the loop below checks
|
|
1608
1644
|
// the band as for any float.
|
|
1609
1645
|
if (f.shrink === 'slot' && measure.body && !rotated) {
|
|
1610
|
-
const room = Math.min(...targetCols.map((c) => c.availableHeight - (columnHasFloatBand(page, c) ? minTextPx : 0)));
|
|
1646
|
+
const room = Math.min(...targetCols.map((c) => c.availableHeight - leadOf(c) - (columnHasFloatBand(page, c) ? minTextPx : 0)));
|
|
1611
1647
|
if (need > room + 0.01) {
|
|
1612
1648
|
const s = shrinkToRoom(f, width, aside, measure, room, position === 'top', bandOf);
|
|
1613
1649
|
if (!s?.fits)
|
|
@@ -1623,7 +1659,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1623
1659
|
continue;
|
|
1624
1660
|
}
|
|
1625
1661
|
const hasBand = columnHasFloatBand(page, c);
|
|
1626
|
-
if (fitsStrict(need, c, hasBand, minTextPx))
|
|
1662
|
+
if (fitsStrict(need + leadOf(c), c, hasBand, minTextPx))
|
|
1627
1663
|
continue;
|
|
1628
1664
|
// The head of an empty column: a splittable table is cut to the
|
|
1629
1665
|
// column and continues in the next slot — the compositor sets a
|
|
@@ -1675,6 +1711,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1675
1711
|
const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated, flush, aside, mirroredOf(page), cap);
|
|
1676
1712
|
if (!built)
|
|
1677
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;
|
|
1678
1717
|
// The caption beside the figure takes its band of the side column: a
|
|
1679
1718
|
// top float's caption is consumed from the stack's head (when the
|
|
1680
1719
|
// stack is still above it), a bottom float's cuts the column's foot.
|
|
@@ -1694,7 +1733,34 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1694
1733
|
asideCutBy.set(sideCol, f.resourceId);
|
|
1695
1734
|
}
|
|
1696
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;
|
|
1697
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
|
+
}
|
|
1698
1764
|
const r = { ...reservedOf(col) };
|
|
1699
1765
|
if (position === 'top') {
|
|
1700
1766
|
col.bbox.y += need;
|
|
@@ -1727,38 +1793,34 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1727
1793
|
if (galleryFill)
|
|
1728
1794
|
for (const col of targetCols)
|
|
1729
1795
|
col.availableHeight = 0;
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
// for lines beside it.
|
|
1734
|
-
if (f.wrap && !pageSpan && !side && targetCols.length === 1 && !rotated && !aside && !cut && !galleryFill
|
|
1735
|
-
&& !(position === 'bottom' && uncappedBottoms.has(first))) {
|
|
1736
|
-
const gap = wrapGapPx(f.wrapGap, dpi, bodyStyle.fontSizePx, floatGapPx);
|
|
1737
|
-
const settings = resolved.layout.wrap;
|
|
1738
|
-
const reason = first.bbox.width - width - gap + 0.01 < minTextWidthPx(settings.minTextWidth, first.bbox.width, dpi, bodyStyle.fontSizePx)
|
|
1739
|
-
? 'tooNarrow'
|
|
1740
|
-
: Math.round(need / bodyStyle.lineHeightPx) < settings.minLinesBeside ? 'fewLines' : undefined;
|
|
1741
|
-
if (reason) {
|
|
1742
|
-
textWrapNotes.push({ kind: 'textWrap', reason, resourceId: f.resourceId, pageIndex: page.index });
|
|
1796
|
+
if (wraps && f.wrap) {
|
|
1797
|
+
if (wrapRefused) {
|
|
1798
|
+
textWrapNotes.push({ kind: 'textWrap', reason: wrapRefused, resourceId: f.resourceId, pageIndex: page.index });
|
|
1743
1799
|
}
|
|
1744
1800
|
else {
|
|
1745
|
-
const exWidth = wrapExclusionWidth(first.bbox.width, width,
|
|
1746
|
-
const
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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
|
+
}
|
|
1762
1824
|
}
|
|
1763
1825
|
}
|
|
1764
1826
|
commitFloatBlock(page, first, built, xLeft, y, width, f.firstBlockIdx);
|
|
@@ -1808,7 +1870,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1808
1870
|
* column float of its sequence still gets the page's foot once that one
|
|
1809
1871
|
* is set. A float that does not fit holds up the ones behind it in its
|
|
1810
1872
|
* numbering sequence (see `heldBack`), never the other sequence. */
|
|
1811
|
-
const
|
|
1873
|
+
const flushPendingFloatsIntoPage = (page) => {
|
|
1812
1874
|
if (pendingFloats.length === 0) {
|
|
1813
1875
|
flushSideBoxesIntoPage(page);
|
|
1814
1876
|
return;
|
|
@@ -1896,6 +1958,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
1896
1958
|
i++;
|
|
1897
1959
|
continue;
|
|
1898
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
|
+
}
|
|
1899
1967
|
// A page-span rest never shares the page of its previous slice.
|
|
1900
1968
|
if (isPageSpan && f.notBefore?.pageIndex === page.index) {
|
|
1901
1969
|
i++;
|
|
@@ -2013,7 +2081,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2013
2081
|
// refuse (a head-of-page float offered the foot of the current page);
|
|
2014
2082
|
// floated callouts keep their placement.
|
|
2015
2083
|
const asked = anyPosition && !f.rotate && !f.callout && f.position !== 'auto' ? { ...f, position: 'auto' } : f;
|
|
2016
|
-
let slots = enumerateCurrentPageSlots(page, cursor.columnIndex, asked, capKindOf);
|
|
2084
|
+
let slots = enumerateCurrentPageSlots(page, cursor.columnIndex, asked, capKindOf, openerHeadOf);
|
|
2017
2085
|
// The rest of a table cut on this page only takes the slots after
|
|
2018
2086
|
// its previous slice in reading order (never the foot of the column
|
|
2019
2087
|
// before it; a page-span rest waits for the next page).
|
|
@@ -2050,6 +2118,242 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2050
2118
|
i = settle(i, r);
|
|
2051
2119
|
}
|
|
2052
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
|
+
};
|
|
2053
2357
|
/** Reserve floats on each freshly opened content page. Passed only to the
|
|
2054
2358
|
* content-flow column advances — parity / force-blank pages never get it. */
|
|
2055
2359
|
const onNewPage = (page) => flushFloatsIntoPage(page);
|
|
@@ -2098,15 +2402,28 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2098
2402
|
// its closing columns — whatever position it asked for: a chapter's
|
|
2099
2403
|
// last page with room under its text beats a page holding one table.
|
|
2100
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();
|
|
2101
2408
|
let guard = 0;
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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;
|
|
2110
2427
|
}
|
|
2111
2428
|
};
|
|
2112
2429
|
/** Boxes that left the flow this pass (side column, float band, fixed),
|
|
@@ -2700,6 +3017,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2700
3017
|
* level snaps, or would but for the heading after it). */
|
|
2701
3018
|
const reserveOpenerBand = (headingCol, h, opener, pending = 0, gridText = true) => {
|
|
2702
3019
|
const page = doc.pages[cursor.pageIndex];
|
|
3020
|
+
if (opener)
|
|
3021
|
+
openerOwnColumn.set(headingCol, opener);
|
|
2703
3022
|
for (const otherCol of page.columns) {
|
|
2704
3023
|
if (otherCol === headingCol)
|
|
2705
3024
|
continue;
|
|
@@ -2729,6 +3048,47 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2729
3048
|
* the opener, its column, the band's foot, where text right under the
|
|
2730
3049
|
* opener starts and the float band the column held at its head then. */
|
|
2731
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);
|
|
2732
3092
|
/**
|
|
2733
3093
|
* The room above the first block of a column headed by an opener band,
|
|
2734
3094
|
* relative to the column's head, or undefined for any other column (or
|
|
@@ -2756,7 +3116,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2756
3116
|
if (!head || col.blocks.length > 0 || reservedOf(col).top !== head.floatTop)
|
|
2757
3117
|
return undefined;
|
|
2758
3118
|
let target = head.textTop;
|
|
2759
|
-
|
|
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)) {
|
|
2760
3122
|
const at = head.openerCol.blocks.indexOf(head.opener);
|
|
2761
3123
|
const next = at >= 0 ? head.openerCol.blocks.slice(at + 1).find((b) => !b.hidden) : undefined;
|
|
2762
3124
|
if (next && (next.type === 'heading' || next.type === 'mathDisplay')) {
|
|
@@ -2880,7 +3242,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
2880
3242
|
/** `rollbackTrailingBlocks` that also drops a rolled-back heading's
|
|
2881
3243
|
* side-column obstacles: the heading is set again further on. */
|
|
2882
3244
|
const rollbackHeadings = (col) => {
|
|
2883
|
-
const popped = rollbackTrailingBlocks(col, doc.blocks, isFreeHeading);
|
|
3245
|
+
const popped = rollbackTrailingBlocks(col, doc.blocks, (b) => isFreeHeading(b) && !holdsHeadFloat(col, b));
|
|
2884
3246
|
for (const p of popped) {
|
|
2885
3247
|
const side = sideObstacleColumn.get(p);
|
|
2886
3248
|
if (!side)
|
|
@@ -3013,6 +3375,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3013
3375
|
const floatHeads = cols
|
|
3014
3376
|
.filter((c) => c.blocks.length === 0 && reservedOf(c).top > 0)
|
|
3015
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
|
+
}
|
|
3016
3384
|
bandCapProposals.set(boundaryIndex, {
|
|
3017
3385
|
kind: 'trailing',
|
|
3018
3386
|
startContentIndex: bandStart.contentIndex,
|
|
@@ -3199,13 +3567,18 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3199
3567
|
* keep-with-next rollbacks may pull along (a callout is one unbreakable
|
|
3200
3568
|
* unit; its children never leave it). */
|
|
3201
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;
|
|
3202
3575
|
/** Free headings closing `col` (its trailing run), 0 unless keep-with-next
|
|
3203
3576
|
* is on: the headings a block moving on would strand. */
|
|
3204
3577
|
const trailingHeadingRun = (col) => {
|
|
3205
3578
|
if (!resolved.headings.keepWithNext || headingMayCloseColumn(col))
|
|
3206
3579
|
return 0;
|
|
3207
3580
|
let n = 0;
|
|
3208
|
-
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--)
|
|
3209
3582
|
n++;
|
|
3210
3583
|
return n;
|
|
3211
3584
|
};
|
|
@@ -3226,8 +3599,31 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3226
3599
|
* convert the laid-out box to absolute coordinates at the frame's placed
|
|
3227
3600
|
* origin, and push frame + children — in that order — to `doc.blocks`
|
|
3228
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
|
+
};
|
|
3229
3624
|
const commitCallout = (result, startIdx, plan, col, range) => {
|
|
3230
3625
|
const frame = result.frame;
|
|
3626
|
+
noteNarrowGroups(result, cursor.pageIndex);
|
|
3231
3627
|
stampCalloutSource(frame, startIdx, plan, range);
|
|
3232
3628
|
offsetCalloutToAbsolute(result, frame.bbox.x, frame.bbox.y);
|
|
3233
3629
|
doc.blocks.push(frame);
|
|
@@ -3278,13 +3674,19 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3278
3674
|
});
|
|
3279
3675
|
const layoutRange = (from, to, width, frameId, continuation, mirrored = false) => {
|
|
3280
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;
|
|
3281
3682
|
// A cut inside child `to.child` includes that child (its first
|
|
3282
3683
|
// `to.line` lines); a cut at a child's head excludes it.
|
|
3283
|
-
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 }));
|
|
3284
3686
|
// Nested boxes still open at `from` (their closing markers leading
|
|
3285
3687
|
// the range close them): the fragment opens inside them.
|
|
3286
3688
|
const open = [];
|
|
3287
|
-
for (let k = 0; k <
|
|
3689
|
+
for (let k = 0; k < fromChild; k++) {
|
|
3288
3690
|
const c = children[k];
|
|
3289
3691
|
if (c.containerName !== 'callout')
|
|
3290
3692
|
continue;
|
|
@@ -3293,7 +3695,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3293
3695
|
else if (c.type === 'containerEnd')
|
|
3294
3696
|
open.pop();
|
|
3295
3697
|
}
|
|
3296
|
-
for (let k =
|
|
3698
|
+
for (let k = fromChild; k < children.length && open.length > 0; k++) {
|
|
3297
3699
|
const c = children[k];
|
|
3298
3700
|
if (c.type !== 'containerEnd' || c.containerId !== open[open.length - 1].block.containerId)
|
|
3299
3701
|
break;
|
|
@@ -3305,19 +3707,25 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3305
3707
|
...(contentBlocks[startIdx]?.direction ? { direction: contentBlocks[startIdx].direction } : {}),
|
|
3306
3708
|
continuation,
|
|
3307
3709
|
// Any range short of the box's end is a fragment that goes on.
|
|
3308
|
-
...(to.child < children.length || to.line > 0 ? { continues: true } : {}),
|
|
3309
|
-
children: children.slice(
|
|
3310
|
-
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,
|
|
3311
3713
|
width,
|
|
3312
|
-
|
|
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,
|
|
3313
3719
|
resolved: calloutResolved,
|
|
3314
3720
|
containerId: plan.containerId,
|
|
3315
3721
|
frameId,
|
|
3316
3722
|
nextChildId: () => `${frameId}-c${n++}`,
|
|
3317
3723
|
paragraphContainers,
|
|
3318
|
-
...(from.line > 0 ? { lineFrom: from.line } : {}),
|
|
3319
|
-
...(from.line > 0 && from.widths ? { lineWidths: from.widths } : {}),
|
|
3320
|
-
...(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) } : {}),
|
|
3321
3729
|
...(open.length > 0 ? { openNested: open } : {}),
|
|
3322
3730
|
mirrored,
|
|
3323
3731
|
stripBlock: calloutStripBlock,
|
|
@@ -3451,7 +3859,68 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
3451
3859
|
});
|
|
3452
3860
|
};
|
|
3453
3861
|
collect(full.units, minLines, 0);
|
|
3454
|
-
|
|
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));
|
|
3455
3924
|
const viable = candidates
|
|
3456
3925
|
.filter((c) => !insideGroup(c.cut))
|
|
3457
3926
|
.sort((a, b) => b.bottom - a.bottom);
|
|
@@ -4152,9 +4621,27 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4152
4621
|
const cutBy = asideCutBy.get(side);
|
|
4153
4622
|
if (cutBy !== undefined)
|
|
4154
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
|
+
}
|
|
4155
4636
|
}
|
|
4156
4637
|
const frame = result.frame;
|
|
4157
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
|
+
}
|
|
4158
4645
|
frame.pageIndex = page.index;
|
|
4159
4646
|
frame.columnIndex = side.index;
|
|
4160
4647
|
offsetCalloutToAbsolute(result, side.bbox.x, y);
|
|
@@ -4194,7 +4681,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4194
4681
|
return true;
|
|
4195
4682
|
}
|
|
4196
4683
|
if (!trySideBox(page, side, { startIdx, plan, style }, refY, 'strict')) {
|
|
4197
|
-
pendingSideBoxes.push({ startIdx, plan, style });
|
|
4684
|
+
pendingSideBoxes.push({ startIdx, plan, style, pageIndex: page.index });
|
|
4198
4685
|
}
|
|
4199
4686
|
return true;
|
|
4200
4687
|
};
|
|
@@ -4263,7 +4750,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4263
4750
|
placed = laterSide !== undefined && trySideBox(later, laterSide, w.box, undefined, 'fresh');
|
|
4264
4751
|
}
|
|
4265
4752
|
if (!placed)
|
|
4266
|
-
pendingSideBoxes.push(w.box);
|
|
4753
|
+
pendingSideBoxes.push({ ...w.box, pageIndex: cursor.pageIndex });
|
|
4267
4754
|
}
|
|
4268
4755
|
};
|
|
4269
4756
|
/** Set the waiting side boxes in the side column of a freshly opened
|
|
@@ -4280,6 +4767,31 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4280
4767
|
pendingSideBoxes.shift();
|
|
4281
4768
|
}
|
|
4282
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
|
+
};
|
|
4283
4795
|
/**
|
|
4284
4796
|
* Place a `:::callout` inline at the current column width as one atomic
|
|
4285
4797
|
* unit: the frame block followed by its children in the same column. The
|
|
@@ -4332,7 +4844,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4332
4844
|
note('fewLines');
|
|
4333
4845
|
return false;
|
|
4334
4846
|
}
|
|
4335
|
-
const spacing = col
|
|
4847
|
+
const spacing = atColumnHead(col) ? 0 : pendingSpacing;
|
|
4336
4848
|
if (result.totalHeight > col.availableHeight - spacing + 0.01 && col.blocks.length > 0 && attempt < 4) {
|
|
4337
4849
|
if (attempt === 0)
|
|
4338
4850
|
note('moved');
|
|
@@ -4378,6 +4890,162 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4378
4890
|
return true;
|
|
4379
4891
|
}
|
|
4380
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
|
+
};
|
|
4381
5049
|
const placeCalloutInline = (startIdx, plan) => {
|
|
4382
5050
|
const style = pickCalloutStyle(calloutStyles, plan.attrs.type);
|
|
4383
5051
|
const firstFrameId = `block-${blockIdCounter++}`;
|
|
@@ -4460,7 +5128,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4460
5128
|
const balanceBefore = curCol.blocks.length > 0 || reservedOf(curCol).top > 0
|
|
4461
5129
|
? (balanceExtraPx?.get(balanceKey(startIdx, part)) ?? 0)
|
|
4462
5130
|
: 0;
|
|
4463
|
-
const spacing = (curCol
|
|
5131
|
+
const spacing = (atColumnHead(curCol) ? 0 : Math.max(pendingBeforeSelfSnapping(startIdx), result.marginTopPx)) + balanceBefore;
|
|
4464
5132
|
const roomPx = curCol.availableHeight - spacing;
|
|
4465
5133
|
let fragment = null;
|
|
4466
5134
|
if (result.totalHeight > roomPx + 0.01) {
|
|
@@ -4542,7 +5210,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
4542
5210
|
const to = fragment ? fragment.to : L.end;
|
|
4543
5211
|
if (part > 0 || fragment)
|
|
4544
5212
|
markFragment(placed, part, fragment !== null);
|
|
4545
|
-
const spacingBefore = (curCol
|
|
5213
|
+
const spacingBefore = (atColumnHead(curCol) ? 0 : Math.max(pendingBeforeSelfSnapping(startIdx), placed.marginTopPx)) + balanceBefore;
|
|
4546
5214
|
if (balanceBefore > 0) {
|
|
4547
5215
|
addBalanceExtra(curCol, balanceBefore);
|
|
4548
5216
|
placed.frame.balancing = { levers: ['trailingCallout'], spaceAbove: balanceBefore };
|
|
@@ -5171,14 +5839,14 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5171
5839
|
const figureGrowPx = rawBlock.type === 'resourceBlock' ? balanceExtraPx?.get(flexFigureKey(blockIdx)) ?? 0 : 0;
|
|
5172
5840
|
const figureDelta = figureGrowPx > 0 ? { figureHeightDelta: figureGrowPx } : {};
|
|
5173
5841
|
let measuredBlock = tryLoose
|
|
5174
|
-
? measureLooseParagraph(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, styleOverride, looseLines, trackingLadder, looseOutcome)
|
|
5842
|
+
? measureLooseParagraph(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, styleOverride, looseLines, trackingLadder, looseOutcome, looseOffGrid)
|
|
5175
5843
|
: measureContentBlock(rawBlock, blockIdx, measureWidth, blockMeasureCtx, { styleOverride, ...upright, ...figureDelta });
|
|
5176
5844
|
// An inline picture with a safe area a little too tall for the room
|
|
5177
5845
|
// left in its column is cropped within its safe area to stay there
|
|
5178
5846
|
// rather than move on and leave the column short (#442).
|
|
5179
5847
|
if (measuredBlock?.kind.vdtType === 'resource' && measuredBlock.resourceBlock?.bodyFlex) {
|
|
5180
5848
|
const flex = measuredBlock.resourceBlock.bodyFlex;
|
|
5181
|
-
const room = col.availableHeight - (col
|
|
5849
|
+
const room = col.availableHeight - (atColumnHead(col) ? 0 : Math.max(pendingSpacing, floatGapPx));
|
|
5182
5850
|
const over = measuredBlock.measured.totalHeight - room;
|
|
5183
5851
|
if (over > 0.01 && room > 0 && over <= flex.shrink + 0.01) {
|
|
5184
5852
|
const cropped = measureContentBlock(rawBlock, blockIdx, measureWidth, blockMeasureCtx, {
|
|
@@ -5194,8 +5862,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5194
5862
|
// the page empty. Never below MIN_INLINE_FIGURE_SCALE of its size.
|
|
5195
5863
|
if (measuredBlock?.kind.vdtType === 'resource' && resolved.layout.fitFiguresToPage) {
|
|
5196
5864
|
const rb = measuredBlock.resourceBlock;
|
|
5197
|
-
const room = col.availableHeight - (col
|
|
5198
|
-
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) {
|
|
5199
5867
|
let width = rb.bodyRect.width;
|
|
5200
5868
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5201
5869
|
const current = measuredBlock.resourceBlock;
|
|
@@ -5210,12 +5878,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5210
5878
|
if (!smaller)
|
|
5211
5879
|
break;
|
|
5212
5880
|
measuredBlock = smaller;
|
|
5213
|
-
if (smaller.measured.totalHeight <= room)
|
|
5881
|
+
if (smaller.measured.totalHeight <= room + FIT_EPS)
|
|
5214
5882
|
break;
|
|
5215
5883
|
}
|
|
5216
5884
|
// Still too tall at the smallest acceptable size: back to full size
|
|
5217
5885
|
// and on to the next column, as without the option.
|
|
5218
|
-
if (measuredBlock.measured.totalHeight > room) {
|
|
5886
|
+
if (measuredBlock.measured.totalHeight > room + FIT_EPS) {
|
|
5219
5887
|
measuredBlock = measureContentBlock(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, { styleOverride });
|
|
5220
5888
|
}
|
|
5221
5889
|
}
|
|
@@ -5308,56 +5976,67 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5308
5976
|
// float would, unless the block before asked for more. A wrapped one
|
|
5309
5977
|
// stands level with the next line's top instead.
|
|
5310
5978
|
const spacingBefore = wraps ? pendingSpacing : Math.max(pendingSpacing, floatGapPx);
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
if (wraps && currentColumn(doc, cursor) !== columnBefore && columnBefore.blocks.length > 0) {
|
|
5318
|
-
textWrapNotes.push({
|
|
5319
|
-
kind: 'textWrap', reason: 'moved', resourceId: kind.resource.id,
|
|
5320
|
-
sourceStart: rawBlock.sourceStart + bodyOffset, sourceEnd: rawBlock.sourceEnd + bodyOffset, pageIndex: cursor.pageIndex,
|
|
5321
|
-
});
|
|
5322
|
-
}
|
|
5323
|
-
// `placeBlockInColumn` (inside placeAtomicBlock) shifts `blk.lines`; the
|
|
5324
|
-
// resource's own caption/table lines live on `resourceBlock` and must be
|
|
5325
|
-
// offset to absolute page coordinates here using the placed bbox origin.
|
|
5326
|
-
offsetResourceBlockToAbsolute(resourceBlock, blk.bbox.x, blk.bbox.y);
|
|
5327
|
-
const grown = figureGrowPx > 0 ? resourceBlock.bodyFlex?.delta ?? 0 : 0;
|
|
5328
|
-
if (grown > 0.01) {
|
|
5329
|
-
blk.balancing = { levers: ['flexFigure'], spaceAbove: 0, bodyGrowth: grown };
|
|
5330
|
-
addBalanceExtra(currentColumn(doc, cursor), grown);
|
|
5331
|
-
}
|
|
5332
|
-
doc.blocks.push(blk);
|
|
5333
|
-
// A wrapped picture (#627): the flow goes on under it and its gap, on
|
|
5334
|
-
// a grid line, unless the next block takes the band back to run
|
|
5335
|
-
// beside it (see `pendingWrap`).
|
|
5336
|
-
if (wraps && wrapSpec) {
|
|
5337
|
-
const rCol = currentColumn(doc, cursor);
|
|
5338
|
-
const top = blk.bbox.y;
|
|
5339
|
-
const bottomRel = Math.ceil((top - rCol.bbox.y + groupHeight + wrapGap - 0.01) / baselineGrid) * baselineGrid;
|
|
5340
|
-
rCol.availableHeight = Math.max(0, Math.min(rCol.availableHeight, rCol.bbox.height - bottomRel));
|
|
5341
|
-
pendingSpacing = 0;
|
|
5342
|
-
inlineGapOwed = null;
|
|
5343
|
-
const exWidth = wrapExclusionWidth(rCol.bbox.width, rCol.bbox.width * wrapSpec.width, wrapGap);
|
|
5344
|
-
pendingWrap = {
|
|
5345
|
-
col: rCol,
|
|
5346
|
-
blocks: rCol.blocks.length,
|
|
5347
|
-
cursorY: columnCursorY(rCol),
|
|
5348
|
-
ex: {
|
|
5349
|
-
x: wrapSpec.side === 'left' ? rCol.bbox.x : rCol.bbox.x + rCol.bbox.width - exWidth,
|
|
5350
|
-
y: top,
|
|
5351
|
-
width: exWidth,
|
|
5352
|
-
height: rCol.bbox.y + bottomRel - top,
|
|
5353
|
-
side: wrapSpec.side,
|
|
5354
|
-
gap: exWidth - rCol.bbox.width * wrapSpec.width,
|
|
5355
|
-
ownerId: blk.id,
|
|
5356
|
-
},
|
|
5357
|
-
};
|
|
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') {
|
|
5358
5985
|
flushPendingNumberingAtBoundary();
|
|
5359
5986
|
continue;
|
|
5360
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
|
+
}
|
|
6039
|
+
}
|
|
5361
6040
|
// Snap the flow position after the resource to the baseline grid (the
|
|
5362
6041
|
// group height is arbitrary), baking in at least marginBottom — same
|
|
5363
6042
|
// convention as snapped headings — so the following text lands back on
|
|
@@ -5625,7 +6304,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5625
6304
|
/** Column balancing: the levers applied to this placement, stamped
|
|
5626
6305
|
* on the block it places (`VDTBlock.balancing`). */
|
|
5627
6306
|
let balancing;
|
|
5628
|
-
|
|
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)) {
|
|
5629
6310
|
spacingBefore = pendingSpacing;
|
|
5630
6311
|
if (vdtType === 'heading' || vdtType === 'mathDisplay') {
|
|
5631
6312
|
spacingBefore = Math.max(partIndex === 0 ? pendingBeforeSelfSnapping(blockIdx) : spacingBefore, style.marginTopPx);
|
|
@@ -5860,7 +6541,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5860
6541
|
// the tail follows the list naturally in the next column.
|
|
5861
6542
|
if (endsWithColon
|
|
5862
6543
|
&& partIndex === 0
|
|
5863
|
-
|
|
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
|
|
5864
6547
|
&& curCol.blocks.length > 0) {
|
|
5865
6548
|
const usedHeight = (curCol.bbox.height - curCol.availableHeight) + spacingBefore;
|
|
5866
6549
|
const paragraphBottom = usedHeight + totalRemainHeight;
|
|
@@ -5884,7 +6567,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
5884
6567
|
// there, by the rules that split it — a two-line item the orphan
|
|
5885
6568
|
// and widow rules keep whole needs both. `'line'` keeps the 1.4
|
|
5886
6569
|
// check, one line.
|
|
5887
|
-
if (availableAfter < minSpaceForList
|
|
6570
|
+
if (availableAfter < minSpaceForList - FIT_EPS
|
|
5888
6571
|
|| (resolved.bodyText.colonListRoom === 'item'
|
|
5889
6572
|
&& !firstListItemStarts(blockIdx, availableAfter - effectiveGap, curCol.bbox.width, blockMeasureCtx))) {
|
|
5890
6573
|
const effectiveWidowMin = resolved.bodyText.avoidWidows
|
|
@@ -6086,7 +6769,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
6086
6769
|
// A `:::space` between the heading and its text needs room too.
|
|
6087
6770
|
const minSpaceAfter = minLinesNeeded * bodyStyle.lineHeightPx
|
|
6088
6771
|
+ spaceLinesAfter(contentBlocks, blockIdx) * baselineGrid;
|
|
6089
|
-
|
|
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) {
|
|
6090
6776
|
if (curCol.blocks.length === 0)
|
|
6091
6777
|
shortColumnMoves++;
|
|
6092
6778
|
// Roll back any immediately-preceding heading blocks in this
|
|
@@ -6189,7 +6875,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
6189
6875
|
// its margin and grid snap would carry the block past the foot: it
|
|
6190
6876
|
// takes the rest of the column instead, as a design reaching past
|
|
6191
6877
|
// the foot does (the text after it opens the next column or page).
|
|
6192
|
-
if (vdtType === 'heading' && effectiveRemainHeight > totalRemainHeight && h > curCol.availableHeight) {
|
|
6878
|
+
if (vdtType === 'heading' && effectiveRemainHeight > totalRemainHeight && h > curCol.availableHeight + FIT_EPS) {
|
|
6193
6879
|
h = Math.max(effectiveRemainHeight, curCol.availableHeight);
|
|
6194
6880
|
}
|
|
6195
6881
|
placeBlockInColumn(blk, h, curCol, cursor);
|
|
@@ -6581,6 +7267,25 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
6581
7267
|
// End of the document: level the closing band and place any floats still
|
|
6582
7268
|
// pending (referenced on the last page) on pages appended after it.
|
|
6583
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);
|
|
6584
7289
|
// The notes, in the room the flow reserved for them.
|
|
6585
7290
|
reserveLeftoverNotes();
|
|
6586
7291
|
setColumnNotes();
|
|
@@ -6704,13 +7409,17 @@ function placeDocumentPass(content, config, cache, options, hints) {
|
|
|
6704
7409
|
});
|
|
6705
7410
|
if (shrunk.length > 0)
|
|
6706
7411
|
floatShrinkWarningsByDoc.set(doc, shrunk);
|
|
6707
|
-
if (textWrapNotes.length > 0)
|
|
6708
|
-
textWrapWarningsByDoc.set(doc, textWrapNotes);
|
|
6709
|
-
|
|
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 };
|
|
6710
7416
|
}
|
|
6711
7417
|
/** Passes a document printing its own contents gets at most, beyond the
|
|
6712
7418
|
* first, for the page labels it prints to settle. */
|
|
6713
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;
|
|
6714
7423
|
export function buildDocument(content, config, cache, options) {
|
|
6715
7424
|
return drainPasses(buildDocumentGen(content, config, cache, options));
|
|
6716
7425
|
}
|
|
@@ -6754,6 +7463,12 @@ export async function buildDocumentAsync(content, config, cache, options) {
|
|
|
6754
7463
|
}
|
|
6755
7464
|
/** The build as a generator: one `yield` after every placement pass. */
|
|
6756
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);
|
|
6757
7472
|
const doc = yield* buildDocumentRounds(content, config, cache, options);
|
|
6758
7473
|
// References and markup the source names wrongly: read from the source
|
|
6759
7474
|
// once, located on the pages of the finished layout. Kept apart from
|
|
@@ -6767,6 +7482,9 @@ export function* buildDocumentGen(content, config, cache, options) {
|
|
|
6767
7482
|
// whose letter-spacing such words do not take.
|
|
6768
7483
|
// Line numbers set over a float of the side column (#621).
|
|
6769
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) ?? []));
|
|
6770
7488
|
// Design texts cut to fit their width (#628).
|
|
6771
7489
|
loose.push(...designTruncationWarnings(doc));
|
|
6772
7490
|
// Comic panels whose cell cannot hold their picture's safe area (#556).
|
|
@@ -6794,8 +7512,12 @@ export function* buildDocumentGen(content, config, cache, options) {
|
|
|
6794
7512
|
const harakat = annotateArabicMarks(doc);
|
|
6795
7513
|
// The rules over `*…*` runs under `bodyText.emphasis: 'overline'`.
|
|
6796
7514
|
overlineEmphasis(doc);
|
|
6797
|
-
|
|
6798
|
-
|
|
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];
|
|
6799
7521
|
}
|
|
6800
7522
|
// Config values the build replaced (an unknown number format, a font
|
|
6801
7523
|
// stack, a side column no column width can take): walked once per build,
|
|
@@ -6815,8 +7537,33 @@ const joiningSpacingWarnings = new WeakMap();
|
|
|
6815
7537
|
const dropCapWarningsByDoc = new WeakMap();
|
|
6816
7538
|
/** The `floatShrunk` warnings of a build (#626), found by its last pass. */
|
|
6817
7539
|
const floatShrinkWarningsByDoc = new WeakMap();
|
|
6818
|
-
/** The `textWrap` warnings of a build (#627)
|
|
7540
|
+
/** The `textWrap` warnings of a build (#627) and its `columnsTooNarrow`
|
|
7541
|
+
* ones (#634), found by its last pass. */
|
|
6819
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
|
+
}
|
|
6820
7567
|
/** `doc` with the page of each of its index marks (`doc.indexMarks`). */
|
|
6821
7568
|
function withIndexMarks(doc, content) {
|
|
6822
7569
|
const { content: body, contentOffset } = extractFrontmatter(content.markdown);
|
|
@@ -6835,6 +7582,10 @@ function withIndexMarks(doc, content) {
|
|
|
6835
7582
|
/** {@link buildDocumentGen} before the document-wide warnings: the
|
|
6836
7583
|
* contents rounds around the balanced build. */
|
|
6837
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() };
|
|
6838
7589
|
// A document printing its own table of contents (`:::toc` with no
|
|
6839
7590
|
// host-supplied outline) is laid out with the page labels of the previous
|
|
6840
7591
|
// build until they no longer change: the contents' own length moves what
|
|
@@ -6851,18 +7602,18 @@ function* buildDocumentRounds(content, config, cache, options) {
|
|
|
6851
7602
|
};
|
|
6852
7603
|
if (hasTocDirective(parsed) || hasIndexDirective(parsed) || pageRefs()) {
|
|
6853
7604
|
let outline = computeOutline(parsed, resolveAllConfig(config), content.continuation?.headings, content.continuation?.statementCounters);
|
|
6854
|
-
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);
|
|
6855
7606
|
for (let round = 0; round < MAX_TOC_ROUNDS; round++) {
|
|
6856
7607
|
const after = outlineFromDoc(doc, outline);
|
|
6857
7608
|
if (sameOutline(after, outline))
|
|
6858
7609
|
break;
|
|
6859
7610
|
outline = after;
|
|
6860
|
-
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);
|
|
6861
7612
|
}
|
|
6862
7613
|
return doc;
|
|
6863
7614
|
}
|
|
6864
7615
|
}
|
|
6865
|
-
return withIndexMarks(yield* buildDocumentNumbered(content, config, cache, options), content);
|
|
7616
|
+
return withIndexMarks(yield* buildDocumentNumbered(content, config, cache, options, 0, citing), content);
|
|
6866
7617
|
}
|
|
6867
7618
|
/** The number each note's marker prints in `doc` (its first marker). */
|
|
6868
7619
|
function printedFootnoteNumbers(doc) {
|
|
@@ -6892,8 +7643,8 @@ const MAX_FOOTNOTE_ROUNDS = 3;
|
|
|
6892
7643
|
* width whatever its number, so the second build reflows nothing and the
|
|
6893
7644
|
* numbers hold; a decimal one may move a line (9 → 10), hence the bound.
|
|
6894
7645
|
*/
|
|
6895
|
-
function* buildDocumentNumbered(content, config, cache, options, tocRound = 0) {
|
|
6896
|
-
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);
|
|
6897
7648
|
const f = doc.config.footnotes;
|
|
6898
7649
|
if ((f.placement !== 'column' && f.placement !== 'spread') || (f.numbering !== 'page' && f.numbering !== 'column' && f.numbering !== 'spread'))
|
|
6899
7650
|
return doc;
|
|
@@ -6911,12 +7662,15 @@ function* buildDocumentNumbered(content, config, cache, options, tocRound = 0) {
|
|
|
6911
7662
|
next.set(id, number);
|
|
6912
7663
|
if (sameFootnoteNumbers(next, used))
|
|
6913
7664
|
break;
|
|
6914
|
-
doc = yield* buildDocumentBalanced(content, config, cache, options, tocRound, next);
|
|
7665
|
+
doc = yield* buildDocumentBalanced(content, config, cache, options, tocRound, next, citing);
|
|
6915
7666
|
}
|
|
6916
7667
|
return doc;
|
|
6917
7668
|
}
|
|
6918
7669
|
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
6919
|
-
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() }) {
|
|
6920
7674
|
// Each pass reports its own progress, numbered in build order.
|
|
6921
7675
|
let passIndex = 0;
|
|
6922
7676
|
const onProgress = options?.onProgress;
|
|
@@ -6927,12 +7681,28 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6927
7681
|
// Side captions moved under their figure (see `PassHints.captionUnder`):
|
|
6928
7682
|
// once a pass asks for one, every later pass keeps it.
|
|
6929
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;
|
|
6930
7693
|
const runPass = (hints) => {
|
|
6931
7694
|
passIndex++;
|
|
6932
7695
|
const started = onPass ? now() : 0;
|
|
6933
|
-
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
|
+
});
|
|
6934
7702
|
for (const id of result.captionUnderProposals)
|
|
6935
7703
|
captionUnder.add(id);
|
|
7704
|
+
for (const k of result.looseOffGrid)
|
|
7705
|
+
looseOffGrid.add(k);
|
|
6936
7706
|
onPass?.({ pass: passIndex, tocRound, ms: now() - started, pages: result.doc.pages.length });
|
|
6937
7707
|
return result;
|
|
6938
7708
|
};
|
|
@@ -6949,12 +7719,55 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6949
7719
|
if (captionUnder.size === before)
|
|
6950
7720
|
break;
|
|
6951
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
|
+
}
|
|
6952
7756
|
// --- Band caps (page-span blocks mid-page) -----------------------------
|
|
6953
7757
|
// A span block that arrived in an uneven band proposes a cap; the driver
|
|
6954
7758
|
// re-places the document with it (and grows / drops caps whose band
|
|
6955
7759
|
// overflowed) before balancing runs. Documents without such blocks get
|
|
6956
7760
|
// their first pass back untouched — no extra pass.
|
|
6957
|
-
|
|
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
|
+
}
|
|
6958
7771
|
let best = bands.result;
|
|
6959
7772
|
const bandCaps = bands.bandCaps;
|
|
6960
7773
|
let passCount = bands.passCount;
|
|
@@ -6980,8 +7793,20 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
6980
7793
|
// balances exactly as its chapters would one by one, instead of one
|
|
6981
7794
|
// cascade anywhere costing every page of the book a pass.
|
|
6982
7795
|
const balancing = best.doc.config.headings.balancing;
|
|
6983
|
-
|
|
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;
|
|
6984
7808
|
return best.doc;
|
|
7809
|
+
}
|
|
6985
7810
|
/** Levers accepted so far, over every segment (keyed by content index /
|
|
6986
7811
|
* `balanceKey`; each key belongs to exactly one segment). */
|
|
6987
7812
|
const applied = { lines: new Map(), loose: new Map() };
|
|
@@ -7101,6 +7926,11 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
7101
7926
|
if (!sameContent(blocks, next.doc.blocks))
|
|
7102
7927
|
return best;
|
|
7103
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);
|
|
7104
7934
|
const pageBest = pageOfContent(best.doc);
|
|
7105
7935
|
const pageNext = pageOfContent(next.doc);
|
|
7106
7936
|
const mergeMap = (a, b) => {
|
|
@@ -7130,7 +7960,10 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
7130
7960
|
spanPlacedInBand: mergeSet(best.spanPlacedInBand, next.spanPlacedInBand),
|
|
7131
7961
|
bandCapsApplied: mergeSet(best.bandCapsApplied, next.bandCapsApplied),
|
|
7132
7962
|
looseOutcome: mergeMap(best.looseOutcome, next.looseOutcome),
|
|
7963
|
+
looseOffGrid: new Set([...best.looseOffGrid, ...next.looseOffGrid]),
|
|
7133
7964
|
captionUnderProposals: new Set([...best.captionUnderProposals, ...next.captionUnderProposals]),
|
|
7965
|
+
citingPageProposals: best.citingPageProposals,
|
|
7966
|
+
citingPageRefused: best.citingPageRefused,
|
|
7134
7967
|
};
|
|
7135
7968
|
};
|
|
7136
7969
|
/**
|
|
@@ -7200,6 +8033,7 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
7200
8033
|
failedLoose,
|
|
7201
8034
|
failedLines,
|
|
7202
8035
|
closingBox: balancing.closingBox,
|
|
8036
|
+
...(onGrid && balancing.gridLines === 'off' ? { gridLines: false } : {}),
|
|
7203
8037
|
});
|
|
7204
8038
|
// The levers newly proposed, by segment; those of a segment that is
|
|
7205
8039
|
// done (plateaued, out of attempts) are withdrawn from the pass.
|
|
@@ -7265,7 +8099,10 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
7265
8099
|
// a levelled closing band spills past its cut) is a regression —
|
|
7266
8100
|
// capped columns without their box are not a layout we may keep.
|
|
7267
8101
|
const capsDelivered = [...bandCaps.keys()].every((i) => !inRange(s.range, capPage.get(i)) || next.spanPlacedInBand.has(i));
|
|
7268
|
-
|
|
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)
|
|
7269
8106
|
? gapLinesIn(nextGaps, s.range)
|
|
7270
8107
|
: Infinity;
|
|
7271
8108
|
// Loose paragraphs that gained no line at any tracking rung are
|
|
@@ -7348,7 +8185,9 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
7348
8185
|
const frozen = hintsFrom(applied.lines, applied.loose);
|
|
7349
8186
|
const trailing = yield* resolveTrailingCapsGen(best, bandCaps, (caps) => runPass({ ...frozen, bandCaps: caps, floatPages: floatPagesOf(best.doc) }));
|
|
7350
8187
|
passCount += trailing.passCount;
|
|
7351
|
-
|
|
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) {
|
|
7352
8191
|
best = trailing.result;
|
|
7353
8192
|
for (const [i, cap] of trailing.caps)
|
|
7354
8193
|
bandCaps.set(i, cap);
|
|
@@ -7364,8 +8203,41 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0, f
|
|
|
7364
8203
|
}
|
|
7365
8204
|
best.doc.iterationCount = passCount;
|
|
7366
8205
|
best.doc.converged = segments.every((s) => s.stable || s.bestScore === 0);
|
|
8206
|
+
if (onGrid)
|
|
8207
|
+
markGridRefusals(best.doc, best.forcedBreakPages, looseOffGrid, gridBalancing);
|
|
7367
8208
|
return best.doc;
|
|
7368
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'];
|
|
7369
8241
|
/** Whether two lines set one bayt of a poem (`VDTLine.verse`), which a
|
|
7370
8242
|
* column never cuts. */
|
|
7371
8243
|
function sameBayt(a, b) {
|