This document is part of the Pushing Forward for CSS Print project, funded through the NGI0 Commons Fund, established by NLnet. It’s a joint initiative from core contributors of Paged.js (@julientaq, @JulieBlanc) and the WeasyPrint team (@grewn0uille, @liZe).
The goal of this proposal is to harmonize and define more precisely the way fragmentation is done in some of the parallel layouts, namely grids, flex boxes and tables.
The document is split into 3 parts:
We would be happy to discuss the whole fragmentation problems for these types of layouts. We can also discuss the specific solutions for each layout, and possibly split this huge issue into smaller ones.
Originally published here:
Today, CSS specifications offer various mechanisms for creating complex layouts, such as grids, flexbox, and tables. However, when it comes to paginated media, the way these layouts transition from one page to another requires thorough examination. While some proposals have been made in the current specifications, they are not well-defined algorithmically, making them difficult to implement effectively. We aim to propose improvements to these specifications.
Grid, flex, table and column layouts share a lot of problems related to paged media: the way we split lines and columns that include parallel items is not fully described by the Fragmentation specification or by the specifications of the layouts. The limitations are sometimes listed in the specification, and sometimes just omitted.
What we want to do is to:
There are other cases that are different and are not covered by this work, even if the choices done here could help solving these cases:
To know which improvements we can propose, we have to list the different sources of information about the current break possibilities.
Fragmentation includes a lot of standard information about breaks.
Chapter 2 is about terminology (fragmentation, box fragment, fragmentainer…), parallel fragmentation (including all cases described above in "Scope", covered or not by this work), and nested fragmentation. Parallel fragmentation, which includes the topics of our current work, is only described with a few paragraphs, but far from being fully specified.
Chapter 3 is about controlling breaks, defining break-*, orphans and widows properties. It contains values for pages, columns and regions, but regions are marked at-risk of being dropped. Flex and Grid are listed as having child to parent propagation exceptions, among other possible parallel layouts, but the exceptions are not detailed here.
Chapter 4 is about possible break points, forced and unforced breaks, and optimization of unforced breaks.
Chapter 5 is about special rules and details related to breaks: adjoining margins (with a new margin-break property introduced by Level 4), broken box extension to fill empty space, box-decoration-break, transforms. It also gives information about breaking into varying-size fragmentainers, which can be useful for paginated media.
The Paged Media module explains in Chapter 1 and Chapter 8 that fragmentation is handled by the Fragmentation module.
The Flexible box and Grid modules use the same information structure for fragmentation. They include a chapter dedicated to fragmentation (Flexbox Chapter 10, Grid Chapter 13). These chapters include an introduction that defines the general principles, and a sample of the fragmentation algorithm.
The introduction defines some rules that must be applied when breaking flex layouts, but "The exact layout of a fragmented flex/grid container is not defined in this level of Flexbox/Grid Layout."
The algorithm section begins with: "This informative section presents a possible fragmentation algorithm for flex/grid containers. Implementors are encouraged to improve on this algorithm and provide feedback to the CSS Working Group."
The rules for Flexbox cover:
break-before and break-after on flex items propagate to the flex container in both row and column flex containers;The Flexbox algorithm section describes 4 algorithms that must be followed to break flex containers and flex items in 4 different cases: single-line, single-column, multi-line, multi-column. It contains sparse comments to explain the high-level goal of each algorithm.
The rules for Grid cover:
break-* properties apply to grid containers;break-before and break-after on grid items propagate to the rows, and to grid containers for first/last rows;The Grid algorithm section gives a "rough draft of one possible fragmentation algorithm" that "needs to be severely cross-checked with the Flex algorithm for consistency". The algorithm is much shorter than the Flex one, as there’s no single/multi and line/column distinction.
The Table module, advertised as "not ready for implementation", also contains a Fragmentation chapter. This chapter contains a part about breaking rules, and a part about repeating headers/footers.
The rules for Table explain where breaks are allowed/forbidden, depending on the spanning cells, row positions, repeating headers/footers and break-* rules.
The part about repeating headers/footers explains when headers and footers have to be repeated at the top and bottom of each table fragment.
The Columns module covers fragmentation in different parts:
column and avoid-column values of the break-* properties.The Regions module explains that it "follows the fragmentation rules defined in the CSS Fragmentation Module". As for columns, "break opportunities in the named flow fragment contained by the CSS Region are determined using the standard fragmentation rules."
Specific region and avoid-region values of break-* properties are quickly described, with links to Fragmentation (where they are marked "at-risk") for more information.
Fragmentation is a great module defining global rules, breaking points and concepts. It covers most of the problems we had in WeasyPrint to break single-flow layouts. Parallel flows are discussed a few paragraphs below.
Paged media, Columns and Regions are more or less specific variants of the generic breaking rules, applied to pages, columns and regions, with their page, column and region values. They don’t introduce extra break points or special cases, they just define what are their specific fragmentainers without changing the generic fragmentation rules.
In WeasyPrint, in the case of single-flow layouts inside columns, there’s been no real breaking problem. Regions are not supported.
Parallel flows introduced by floating or absolutely positioned elements are regularly problematic in WeasyPrint, and some questions are not solved in Fragmentation in my opinion. They caused real-life issues when breaking pages and columns, at multiple levels: implementation, algorithm, high-level breaking strategy, specification. Even if these problems are really interesting and important, I think that we shouldn’t try to solve them here.
Table and Grid layouts face the same high-level breaking problems. Their layout, before possible fragmentation, creates a grid of rows and columns, with cells that can span. This ideal layout uses different algorithms, that’s why Table and Grid are not the same, but at the end they define widths for columns and heights for rows, gaps between columns and rows, borders.
The high-level problem for both is to define how we want to break rows (in a vertical main block direction). We have constrains given by break-* rules, fixed min-/max-/heights and min-/max-/widths, break points between rows, etc. The goal is to define which rules are not respected in over-constrained cases, and to give an algorithm that breaks the cells in a way that respect constraints (in a user-friendly way, if possible). This algorithm can then be adapted to Table and to Grid.
Grid fragmentation has already been implemented in WeasyPrint, using a custom algorithm. The pseudo-algorithm in the specification is not perfect, and WeasyPrint’s implementation didn’t follow all its steps. The implementation is based on global rules of Fragmentation and specificities of Table, with some extra rules that had to be defined to solve real-world, complex cases. The overall idea behind these rules is quite easy to summarize, and most of them add can be adapted for Table. We didn’t write a theoretical algorithm yet.
The Flex layout is different. The 4 cases lead to different challenges, that’s why the specification offers 4 different algorithms. The lessons we’ll learn from Table and Grid may help to find solutions that are consistent with Grid and Table, respect constraints, and propose user-friendly default breaking results. We have fragmentation in Flex for really simple cases. The current algorithms of the specification are solid, even if they don’t cover all cases and can be improved.
Fragmentation is referenced by all modules. Paged Media, Columns and Regions basically say that their problems are covered by Fragmentation, and we fully agree with that.
We should focus on Table, Flex and Grid, and only change these 3 modules, replacing their current Fragmentation chapters. There’s a lot of draft-quality but really useful content in these chapters, with open issues that we should solve.
Even if the overall ideas are more or less shared between layouts, the break algorithms are actually really different, and there’s no need to have a common source of information somewhere in the Fragmentation specification to explain these ideas. Of course, informal data could be useful to share ideas, but blog posts are probably better than a specification for this.
As explained above, we should first work on Table and Grid, on a common high-level algorithm that splits rows and columns, with drawn examples that cover complex cases. When this high-level algorithm is ready, we can write 2 detailed algorithms for Table and Grid, and propose to include them (and a short introduction) to replace their specific chapters about fragmentation.
We can then do the same for Flex.
Tables and grids share a lot of high-level properties: they are composed of cells, displayed in a grid, possibly spanning multiple rows and columns. The order, the location and the size of each cell is determined during the layout, before the fragmentation step that can be done independently. To get a correct fragmentation, for both grids and tables, we can define the same global goals and use the same rules. Some of them are already defined in the Grid specification and can be adapted to work in a general situation.
To keep these rules as simple as possible, we use the words "grid", "row", "column" and "cell" for both grid and table layouts. The rules are defined here in a default context where the grid is split between pages, the block direction used for rows is top to bottom, and the inline direction used for columns is left to right. The terms used must be adapted accordingly for other contexts.
break-before, break-inside and break-after properties on cells are propagated to their row. The break-before property on the first row and the break-after property on the last row are propagated to the row container. It means that breaks before, inside or after a cell must be considered as breaks before, inside or after its row. If a cell spans through multiple rows, break-before applies to the first row, break-after applies to the last row, and break-inside applies to all rows.The question of repeated table headers and footers is orthogonal to these rules, and should be addressed separately.
The flex layout shares properties with grids, but they have major differences that make them require very different rules:
Depending on the main direction and the allowed splits, we actually have 4 cases whose high-level goals are different. That’s why we require 4 independent sets of rules: single row, multiple rows, single column, multiple columns.
The rules are defined here in a default context where the flex container is split between pages, the block direction is top to bottom, and the inline direction is left to right. The terms used must be adapted accordingly for other contexts.
The order of the flex items is the one used after the order property is resolved.
The goal is to break all the items at the same time, as if it was a grid row.
break-before and break-after values on flex items are propagated to the flex container.The goal is to break all the items in a row at the same time, as if it was a grid row. Breaks are also possible between lines.
break-before and break-after values on flex items are propagated to its flex line. The break-before value of the first line and the break-after value of the last line are propagated to the flex container.The goal is to break around and between flex items as if they were blocks.
break-before values on the first item and the break-after values on the last item are propagated to the flex container.The goal is to render items in a column until they reach the bottom of the page, and then create a new column if vertical space is available. Forced page breaks force the remaining content to be rendered on the next page. Unforced breaks can’t split flex items between columns or between pages, unless a flex item doesn’t fit on a blank page.
break-before values on the first item and the break-after values on the last item are propagated to the flex container.The rules are defined here in a default context where the table is split between pages, the block direction is top to bottom, and the inline direction is left to right. The terms used must be adapted accordingly for other contexts.
Pre-fragmentation steps:
break-before rule of the table (or the first header row, or one of its cells, or the first body row, or one of its cells) forces a break, go to the next page.Fragmentation steps:
If the height of the header plus the height of the footer rows is greater than the remaining height in the page:
If the page is not empty: if the height of the unfragmented table is higher than the remaining height in the page, and if the break-inside rule on the table avoids a break, go to the next page and restart the pre-fragmentation steps.
If the height of the header plus the height of the footer, as determined by the unfragmented table, is lower than the remaining height in the page, remove the heights of the header and the footer from the remaining height in the page.
For each row of the table, skipping all the content rendered on previous pages:
If the page is not empty (excluding the table header): if the break-before rule on the row (or one of the cells whose top position is in the row) forces a break, go to the next page and restart the fragmentation steps.
Set the height of the row to its height defined in the unfragmented table, minus the fragment height of the row for its content already rendered.
If the height of the row is lower than the ROWMIN of the remaining content, then it is set to this ROWMIN. For this step, the fragment height of the content already rendered is removed from any height set on a cell.
If the height of the row is higher than the remaining height:
break-inside rule on the row (or one of the cells displayed in the row) avoids a break, and if the page is not empty (excluding the table header), go to the next page and restart the fragmentation steps.Render the row.
If the break-after rule on the row (or one of the cells whose top position is in the row) forces a break, go to the next page and restart the fragmentation steps.
Render the footer.
If the break-after rule on the table (or the last footer row, or one of its cells, or the last body row, or one of its cells) forces a break, go to the next page.
(This is a simplified version of the pseudo-algorithm for tables.)
The rules are defined here in a default context where the grid is split between pages, the block direction is top to bottom, and the inline direction is left to right. The terms used must be adapted accordingly for other contexts.
Pre-fragmentation steps:
break-before rule of the grid (or one of the items whose top position is in the first row) forces a break, go to the next page.Fragmentation steps:
If the page is not empty: if the height of the unfragmented grid is higher than the remaining height in the page, and if the break-inside rule on the grid avoids a break, go to the next page and restart the pre-fragmentation steps.
For each row of the grid, skipping all the content rendered on previous pages:
If the page is not empty: if the break-before rule on one of the items whose top position is in the row forces a break, go to the next page and restart the fragmentation steps.
Set the height of the row to its height defined in the unfragmented grid, minus the fragment height of the row for its content already rendered.
If the height of the row is lower than the min track sizing function value of the remaining content, then it is set to this min track sizing function value. For this step, the fragment height of the content already rendered is removed from any height set on an item.
If the height of the row is higher than the remaining height:
break-inside rule on one of the items displayed in the row avoids a break, and if the page is not empty, go to the next page and restart the fragmentation steps.Render the row.
If the break-after rule on one of the items of the row forces a break, go to the next page and restart the fragmentation steps.
If the break-after rule on the grid forces a break, go to the next page.
The rules are defined here in a default context where the flex container is split between pages, the block direction is top to bottom, and the inline direction is left to right. The terms used must be adapted accordingly for other contexts.
Pre-fragmentation steps:
break-before rule of the flex container (or one of the items in the first row) forces a break, go to the next page.Fragmentation steps:
If the page is not empty: if the height of the unfragmented flexbox is higher than the remaining height in the page, and if the break-inside rule on the flex container avoids a break, go to the next page and restart the pre-fragmentation steps.
For each row of the flex container, skipping all the content rendered on previous pages:
If the page is not empty: if the break-before rule on one of the items of the row forces a break, go to the next page and restart the fragmentation steps.
Set the height of the row to its height defined in the unfragmented flexbox, minus the fragment height of the row for its content already rendered.
If the height of the row is lower than the cross size of the remaining content, then it is set to the cross size. For this step, the fragment height of the content already rendered is removed from any height set on an item.
If the height of the row is higher than the remaining height:
break-inside rule on one of the items displayed in the row avoids a break, and if the page is not empty, go to the next page and restart the fragmentation steps.Render the row.
If the break-after rule on one of the items of the row forces a break, go to the next page and restart the fragmentation steps.
If the break-after rule on the flex container forces a break, go to the next page.
Pre-fragmentation steps:
break-before rule of the flex container (or its first item) forces a break, go to the next page.Fragmentation steps:
If the page is not empty: if the height of the unfragmented flexbox is higher than the remaining height in the page, and if the break-inside rule on the flex container avoids a break, go to the next page and restart the pre-fragmentation steps.
For each item of the flex container, skipping all the content rendered on previous pages:
If the page is not empty: if the break-before rule on the item forces a break, go to the next page and restart the fragmentation steps.
Set the height of the item to its height defined in the unfragmented flexbox, minus the fragment height of the item for its content already rendered.
If the height of the item is lower than the height of the remaining content, then it is set to the height of the remaining content. For this step, the fragment height of the content already rendered is removed from any height set on an item.
If the height of the item is higher than the remaining height:
break-inside rule on the item avoids a break, and if the page is not empty, go to the next page and restart the fragmentation steps.Render the item.
If the break-after rule on the item forces a break, go to the next page and restart the fragmentation steps.
If the break-after rule on the flex container forces a break, go to the next page.
Pre-fragmentation step:
break-before rule of the flex container (or its first item) forces a break, go to the next page.Fragmentation steps:
max-height to the minimum between (a) its original max-height value and (b) the remaining height in the page.break-before or break-after property of an item forces a break.break-after rule on the flex container forces a break, go to the next page.