Compare commits

..

No commits in common. "71ae276071f7b47aa65419e2d7dac5e8e08a369a" and "af0c27cb98716ae6614120996be1c5df85da7103" have entirely different histories.

4 changed files with 172 additions and 148 deletions

View File

@ -236,12 +236,54 @@ impl<'a> GridLayouter<'a> {
let mut y = 0; let mut y = 0;
let mut consecutive_header_count = 0; let mut consecutive_header_count = 0;
while y < self.grid.rows.len() { while y < self.grid.rows.len() {
if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count) if let Some(first_header) =
self.upcoming_headers.get(consecutive_header_count)
{ {
if next_header.range().contains(&y) { if first_header.unwrap().range().contains(&y) {
self.place_new_headers(&mut consecutive_header_count, engine)?; consecutive_header_count += 1;
y = next_header.end;
// TODO: surely there is a better way to do this
match self.upcoming_headers.get(consecutive_header_count) {
// No more headers, so place the latest headers.
None => {
self.place_new_headers(
consecutive_header_count,
None,
engine,
)?;
consecutive_header_count = 0;
}
// Next header is not consecutive, so place the latest headers.
Some(next_header)
if next_header.unwrap().start > first_header.unwrap().end =>
{
self.place_new_headers(
consecutive_header_count,
None,
engine,
)?;
consecutive_header_count = 0;
}
// Next header is consecutive and conflicts with one or
// more of the latest consecutive headers, so we must
// place them before proceeding.
Some(next_header)
if next_header.unwrap().level
<= first_header.unwrap().level =>
{
self.place_new_headers(
consecutive_header_count,
Some(next_header),
engine,
)?;
consecutive_header_count = 0;
}
// Next header is a non-conflicting consecutive header.
// Keep collecting more headers.
_ => {}
}
y = first_header.unwrap().end;
// Skip header rows during normal layout. // Skip header rows during normal layout.
continue; continue;
} }
@ -1462,11 +1504,21 @@ impl<'a> GridLayouter<'a> {
self.current.repeated_header_rows.min(self.lrows.len()); self.current.repeated_header_rows.min(self.lrows.len());
} }
let footer_would_be_widow = let footer_would_be_widow = if let Some(last_header_row) = self
if !self.lrows.is_empty() && self.current.repeated_header_rows > 0 { .current
// If headers are repeating, then we already know they are not .repeated_header_rows
// short-lived as that is checked, so they have orphan prevention. .checked_sub(1)
if self.lrows.len() == self.current.repeated_header_rows .and_then(|last_header_index| self.lrows.get(last_header_index))
{
let last_header_end = last_header_row.index();
if self.grid.rows.len() > last_header_end
&& self
.grid
.footer
.as_ref()
.and_then(Repeatable::as_repeated)
.is_none_or(|footer| footer.start != last_header_end)
&& self.lrows.len() == self.current.repeated_header_rows
&& may_progress_with_offset( && may_progress_with_offset(
self.regions, self.regions,
// Since we're trying to find a region where to place all // Since we're trying to find a region where to place all
@ -1476,9 +1528,8 @@ impl<'a> GridLayouter<'a> {
self.current.header_height + self.current.footer_height, self.current.header_height + self.current.footer_height,
) )
{ {
// Header and footer would be alone in this region, but // Header and footer would be alone in this region, but there are more
// there are more rows beyond the headers and the footer. // rows beyond the header and the footer. Push an empty region.
// Push an empty region.
self.lrows.clear(); self.lrows.clear();
self.current.last_repeated_header_end = 0; self.current.last_repeated_header_end = 0;
self.current.repeated_header_rows = 0; self.current.repeated_header_rows = 0;
@ -1487,19 +1538,17 @@ impl<'a> GridLayouter<'a> {
false false
} }
} else if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { } else if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
// If no rows other than the footer have been laid out so far, // If no rows other than the footer have been laid out so far, and
// and there are rows beside the footer, then don't lay it out // there are rows beside the footer, then don't lay it out at all.
// at all. (Similar check from above, but for the case without // (Similar check from above, but for the case without headers.)
// headers.) // TODO: widow prevention for non-repeated footers with a similar
// TODO: widow prevention for non-repeated footers with a // mechanism / when implementing multiple footers.
// similar mechanism / when implementing multiple footers.
self.lrows.is_empty() self.lrows.is_empty()
&& may_progress_with_offset( && may_progress_with_offset(
self.regions, self.regions,
// This header height isn't doing much as we just // This header height isn't doing much as we just confirmed
// confirmed that there are no headers in this region, // that there are no headers in this region, but let's keep
// but let's keep it here for correctness. It will add // it here for correctness. It will add zero anyway.
// zero anyway.
self.current.header_height + self.current.footer_height, self.current.header_height + self.current.footer_height,
) )
&& footer.start != 0 && footer.start != 0
@ -1510,11 +1559,8 @@ impl<'a> GridLayouter<'a> {
let mut laid_out_footer_start = None; let mut laid_out_footer_start = None;
if !footer_would_be_widow { if !footer_would_be_widow {
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
// Don't layout the footer if it would be alone with the header // Don't layout the footer if it would be alone with the header in
// in the page (hence the widow check), and don't layout it // the page (hence the widow check), and don't layout it twice.
// twice.
// TODO: this check can be replaced by a vector of repeating
// footers in the future.
if self.lrows.iter().all(|row| row.index() < footer.start) { if self.lrows.iter().all(|row| row.index() < footer.start) {
laid_out_footer_start = Some(footer.start); laid_out_footer_start = Some(footer.start);
self.layout_footer(footer, engine, self.finished.len())?; self.layout_footer(footer, engine, self.finished.len())?;

View File

@ -9,43 +9,71 @@ use super::rowspans::UnbreakableRowGroup;
impl<'a> GridLayouter<'a> { impl<'a> GridLayouter<'a> {
pub fn place_new_headers( pub fn place_new_headers(
&mut self, &mut self,
consecutive_header_count: &mut usize, consecutive_header_count: usize,
conflicting_header: Option<&Repeatable<Header>>,
engine: &mut Engine, engine: &mut Engine,
) -> SourceResult<()> { ) -> SourceResult<()> {
*consecutive_header_count += 1;
let (consecutive_headers, new_upcoming_headers) = let (consecutive_headers, new_upcoming_headers) =
self.upcoming_headers.split_at(*consecutive_header_count); self.upcoming_headers.split_at(consecutive_header_count);
self.upcoming_headers = new_upcoming_headers;
if new_upcoming_headers.first().is_some_and(|next_header| { let (non_conflicting_headers, conflicting_headers) = match conflicting_header {
consecutive_headers.last().is_none_or(|latest_header| { // Headers succeeded by end of grid or footer are short lived and
!latest_header.short_lived && next_header.start == latest_header.end // can be placed in separate regions (no orphan prevention).
}) && !next_header.short_lived // TODO: do this during grid resolving?
}) { // might be needed for multiple footers. Or maybe not if we check
// More headers coming, so wait until we reach them. // "upcoming_footers" (O(1) here), however that looks like.
// TODO: refactor _ if consecutive_headers
return Ok(()); .last()
.is_some_and(|x| x.unwrap().end == self.grid.rows.len())
|| self
.grid
.footer
.as_ref()
.zip(consecutive_headers.last())
.is_some_and(|(f, h)| f.unwrap().start == h.unwrap().end) =>
{
(Default::default(), consecutive_headers)
} }
self.upcoming_headers = new_upcoming_headers; Some(conflicting_header) => {
*consecutive_header_count = 0; // All immediately conflicting headers will
// be laid out without orphan prevention.
consecutive_headers.split_at(consecutive_headers.partition_point(|h| {
conflicting_header.unwrap().level > h.unwrap().level
}))
}
_ => (consecutive_headers, Default::default()),
};
// Layout short-lived headers immediately. self.layout_new_pending_headers(non_conflicting_headers, engine)?;
if consecutive_headers.last().is_some_and(|h| h.short_lived) {
// No chance of orphans as we're immediately placing conflicting
// headers afterwards, which basically are not headers, for all intents
// and purposes. It is therefore guaranteed that all new headers have
// been placed at least once.
self.flush_pending_headers();
// Layout each conflicting header independently, without orphan // Layout each conflicting header independently, without orphan
// prevention (as they don't go into 'pending_headers'). // prevention (as they don't go into 'pending_headers').
// These headers are short-lived as they are immediately followed by a // These headers are short-lived as they are immediately followed by a
// header of the same or lower level, such that they never actually get // header of the same or lower level, such that they never actually get
// to repeat. // to repeat.
self.layout_new_headers(consecutive_headers, true, engine) for conflicting_header in conflicting_headers.chunks_exact(1) {
} else { self.layout_new_headers(
self.layout_new_pending_headers(consecutive_headers, engine) // Using 'chunks_exact", we pass a slice of length one instead
// of a reference for type consistency.
// In addition, this is the only place where we layout
// short-lived headers.
conflicting_header,
true,
engine,
)?
} }
// No chance of orphans as we're immediately placing conflicting
// headers afterwards, which basically are not headers, for all intents
// and purposes. It is therefore guaranteed that all new headers have
// been placed at least once.
if !conflicting_headers.is_empty() {
self.flush_pending_headers();
}
Ok(())
} }
/// Lays out a row while indicating that it should store its persistent /// Lays out a row while indicating that it should store its persistent
@ -103,7 +131,7 @@ impl<'a> GridLayouter<'a> {
// Assuming non-conflicting headers sorted by increasing y, this must // Assuming non-conflicting headers sorted by increasing y, this must
// be the header with the lowest level (sorted by increasing levels). // be the header with the lowest level (sorted by increasing levels).
let first_level = first_header.level; let first_level = first_header.unwrap().level;
// Stop repeating conflicting headers. // Stop repeating conflicting headers.
// If we go to a new region before the pending headers fit alongside // If we go to a new region before the pending headers fit alongside

View File

@ -1,5 +1,5 @@
use std::num::{NonZeroU32, NonZeroUsize}; use std::num::{NonZeroU32, NonZeroUsize};
use std::ops::{Deref, Range}; use std::ops::Range;
use std::sync::Arc; use std::sync::Arc;
use ecow::eco_format; use ecow::eco_format;
@ -438,12 +438,6 @@ pub struct Header {
/// lower level header stops repeating, all higher level headers do as /// lower level header stops repeating, all higher level headers do as
/// well. /// well.
pub level: u32, pub level: u32,
/// Whether this header cannot be repeated nor should have orphan
/// prevention because it would be about to cease repetition, either
/// because it is followed by headers of conflicting levels, or because
/// it is at the end of the table (possibly followed by some footers at the
/// end).
pub short_lived: bool,
} }
impl Header { impl Header {
@ -475,26 +469,15 @@ impl Footer {
} }
} }
/// A possibly repeatable grid child. /// A possibly repeatable grid object.
///
/// It still exists even when not repeatable, but must not have additional /// It still exists even when not repeatable, but must not have additional
/// considerations by grid layout, other than for consistency (such as making /// considerations by grid layout, other than for consistency (such as making
/// a certain group of rows unbreakable). /// a certain group of rows unbreakable).
pub enum Repeatable<T> { pub enum Repeatable<T> {
/// The user asked this grid child to repeat.
Repeated(T), Repeated(T),
/// The user asked this grid child to not repeat.
NotRepeated(T), NotRepeated(T),
} }
impl<T> Deref for Repeatable<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.unwrap()
}
}
impl<T> Repeatable<T> { impl<T> Repeatable<T> {
/// Gets the value inside this repeatable, regardless of whether /// Gets the value inside this repeatable, regardless of whether
/// it repeats. /// it repeats.
@ -507,7 +490,7 @@ impl<T> Repeatable<T> {
} }
/// Gets the value inside this repeatable, regardless of whether /// Gets the value inside this repeatable, regardless of whether
/// it repeats (mutably). /// it repeats.
#[inline] #[inline]
pub fn unwrap_mut(&mut self) -> &mut T { pub fn unwrap_mut(&mut self) -> &mut T {
match self { match self {
@ -1557,29 +1540,8 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
end: group_range.end, end: group_range.end,
level: row_group.repeatable_level.get(), level: row_group.repeatable_level.get(),
// This can only change at a later iteration, if we
// find a conflicting header or footer right away.
short_lived: false,
}; };
// Mark consecutive headers right before this one as short
// lived if they would have a higher or equal level, as
// then they would immediately stop repeating during
// layout.
let mut consecutive_header_start = data.start;
for conflicting_header in
headers.iter_mut().rev().take_while(move |h| {
let conflicts = h.end == consecutive_header_start
&& h.level >= data.level;
consecutive_header_start = h.start;
conflicts
})
{
conflicting_header.unwrap_mut().short_lived = true;
}
headers.push(if row_group.repeat { headers.push(if row_group.repeat {
Repeatable::Repeated(data) Repeatable::Repeated(data)
} else { } else {
@ -1826,7 +1788,8 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// TODO: will need a global slice of headers and footers for // TODO: will need a global slice of headers and footers for
// when we have multiple footers // when we have multiple footers
let last_header_end = headers.last().map(|header| header.end); let last_header_end =
headers.last().map(Repeatable::unwrap).map(|header| header.end);
if has_gutter { if has_gutter {
// Convert the footer's start index to post-gutter coordinates. // Convert the footer's start index to post-gutter coordinates.
@ -1863,20 +1826,6 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
} }
}); });
// Mark consecutive headers right before the end of the table, or the
// final footer, as short lived, given that there are no normal rows
// after them, so repeating them is pointless.
let mut consecutive_header_start =
footer.as_ref().map(|f| f.start).unwrap_or(row_amount);
for header_at_the_end in headers.iter_mut().rev().take_while(move |h| {
let at_the_end = h.end == consecutive_header_start;
consecutive_header_start = h.start;
at_the_end
}) {
header_at_the_end.unwrap_mut().short_lived = true;
}
Ok(footer) Ok(footer)
} }
@ -2059,10 +2008,9 @@ fn check_for_conflicting_cell_row(
// `y + 1 = header.start` holds, that means `y < header.start`, and it // `y + 1 = header.start` holds, that means `y < header.start`, and it
// only occupies one row (`y`), so the cell is actually not in // only occupies one row (`y`), so the cell is actually not in
// conflict. // conflict.
if headers if headers.iter().any(|header| {
.iter() cell_y < header.unwrap().end && cell_y + rowspan > header.unwrap().start
.any(|header| cell_y < header.end && cell_y + rowspan > header.start) }) {
{
bail!( bail!(
"cell would conflict with header spanning the same position"; "cell would conflict with header spanning the same position";
hint: "try moving the cell or the header" hint: "try moving the cell or the header"
@ -2256,9 +2204,11 @@ fn find_next_available_position<const SKIP_ROWS: bool>(
} }
// TODO: consider keeping vector of upcoming headers to make this check // TODO: consider keeping vector of upcoming headers to make this check
// non-quadratic (O(cells) instead of O(headers * cells)). // non-quadratic (O(cells) instead of O(headers * cells)).
} else if let Some(header) = headers.iter().find(|header| { } else if let Some(header) =
headers.iter().map(Repeatable::unwrap).find(|header| {
(header.start * columns..header.end * columns).contains(&resolved_index) (header.start * columns..header.end * columns).contains(&resolved_index)
}) { })
{
// Skip header (can't place a cell inside it from outside it). // Skip header (can't place a cell inside it from outside it).
resolved_index = header.end * columns; resolved_index = header.end * columns;

View File

@ -293,13 +293,13 @@ fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
}; };
let footer = grid.footer.map(|ft| { let footer = grid.footer.map(|ft| {
let rows = rows.drain(ft.start..); let rows = rows.drain(ft.unwrap().start..);
elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))) elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
}); });
// TODO: Headers and footers in arbitrary positions // TODO: Headers and footers in arbitrary positions
// Right now, only those at either end are accepted // Right now, only those at either end are accepted
let header = grid.headers.first().filter(|h| h.start == 0).map(|hd| { let header = grid.headers.first().filter(|h| h.unwrap().start == 0).map(|hd| {
let rows = rows.drain(..hd.end); let rows = rows.drain(..hd.unwrap().end);
elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row)))) elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))
}); });