feat(engine): keep a heading with the first slice of a table or list below it#428
Merged
Conversation
A section header emitted as its own block strands at a page bottom when the first line of the block below it does not fit in the remaining space: the header flows on page N while its body starts on page N+1 — a boxed title torn from its background. keepTogether() cannot fix this; it relocates the whole block, so a page-spanning body makes it inert and the orphan returns. Add an opt-in keepWithNext() (CSS break-after:avoid): a section may not be the last placed block on its page when a sibling with content follows. When the section plus the first line of that block overflow the remaining space but fit on a fresh page, the compiler relocates the section so the heading stays with its body. - LayoutCompiler runs a run-aware lookahead in the vertical child loop: at the start of a run of consecutive keep-with-next siblings it sums the run plus the next block's leading line and hoists the break before the run's first member, so a multi-part heading (rule + banner + rule) moves as a unit. - leadingUnitHeight descends the following block's first-child chain to its first paragraph line; an indivisible or non-paragraph-splittable first unit is kept whole. Best-effort: a heading plus one line that cannot share a page flows in place; inert when nothing follows. - keepWithNext() on DocumentNode (default false), SectionNode (new component plus back-compat constructor), and SectionBuilder. Default off, so layouts that do not opt in are byte-identical.
…below it keepWithNext() relocated a stranded heading only when the block below it started with a paragraph. LayoutCompiler.leadingUnitHeight returned a true first-line lead for a paragraph but the whole outer height for every other leaf, so a heading above a page-spanning table or list computed needed = heading + wholeBodyHeight > page, the guard needed <= activeInnerHeight() failed, and the heading stayed put. Add a NodeDefinition.firstSliceHeight(prepared) seam and route leadingUnitHeight's leaf branch through it, so each leaf's split granularity lives in its own definition instead of a type switch in the compiler: - default returns the whole measured content height, keeping an atomic leaf (image, chart, shape) whole; - the paragraph returns its first visual line (the instanceof PreparedParagraphLayout branch moves out of LayoutCompiler); - the table returns its repeated header rows plus first body row, mirroring splitTable's minimal head (rowCount > headerCount); - the list returns its first item. Gated behind keepWithNext(), off by default, so layouts that do not opt in are byte-identical. CHANGELOG and the keep-together recipe now describe the first slice across paragraph, table, and list bodies.
# Conflicts: # CHANGELOG.md # core/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java # docs/recipes/keep-together.md # qa/src/test/java/com/demcha/compose/document/dsl/SectionKeepWithNextTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
keepWithNext()relocated a stranded heading only when the block below it startedwith a paragraph.
LayoutCompiler.leadingUnitHeightreturned a true first-line leadfor a paragraph but the whole outer height for every other leaf — including
SPLITTABLEtables and lists. So a keep-with-next heading directly above apage-spanning
table(...)or list computedneeded = heading + wholeBodyHeight,which exceeds the page; the guard
needed <= activeInnerHeight()failed and theheading stayed put — the exact orphan
keepWithNext()exists to prevent, just onebody kind short of working.
What changed
NodeDefinition.firstSliceHeight(PreparedNode<E>)(@Beta/@InternalSPI). The default returns the whole measured content height, so a truly atomic leaf
(image, chart, shape) is kept whole — the previous behaviour.
leadingUnitHeight'sleaf branch now delegates to it, so each leaf's split granularity lives in its own
definition instead of an
instanceofswitch in the compiler.instanceof PreparedParagraphLayoutlogic moves out ofLayoutCompilerintoTextFlowSupport.paragraphFirstSliceHeight.NodeDefinitionSupport.tableFirstSliceHeight) returns itsrepeated header rows plus first body row. This mirrors
splitTable's minimal head,which requires
rowCount > headerCount, so the smallest slice it will place is therepeated header rows plus one body row; with no repeated header it is the first row.
TextFlowSupport.listFirstSliceHeight),the same value
splitListuses for the first placeable unit.keepWithNext()(off by default), andleadingUnitHeightruns only inthat lookahead — so layouts that do not opt in are byte-identical. The default's
whole content box already includes the leaf's own padding, so the caller's added top
reservation slightly over-reserves for a padded atomic leaf; harmless, since it
only makes an already-indivisible block a touch more eager to relocate, and the value
gates a page break, never geometry.
docs/recipes/keep-together.mdnow describe the first slice acrossparagraph, table, and list bodies (previously they said non-paragraph bodies were
kept whole).
Verification
./mvnw -B -ntp clean verifyacross core + render-pdf + templates + testing + qa →BUILD SUCCESS. Core 392 green (incl.
PublicApiNoEngineLeakTest,PublicApiSinceTagCoverageTest); qa 651 green; render-pdf 120 green; javadocgate clean.
SectionKeepWithNextTestgrows 6 → 8 — two new tests mirroring the existingparagraph case:
relocatesHeaderBasedOnFirstSliceOfPageSpanningTable— header flush at a page bottomabove a page-spanning table (with a repeated header row); asserts the header
relocates onto the table's start page when opted in, and strands (header page < table
page) when not.
relocatesHeaderBasedOnFirstItemOfPageSpanningList— the list counterpart.Both fail without the production change (the whole-height default keeps
needed > activeInnerHeight, so no relocation fires).Lane: shared-engine — layout/pagination seam; no public authoring surface touched.
Merge order: stacked on #426 (base
feat/keep-with-next); merge after #426, orretarget to
developonce #426 lands.