initial subheader resolving and api

This commit is contained in:
PgBiel 2025-04-06 15:17:29 -03:00
parent 48d0a07ef4
commit 5e2241ab65
6 changed files with 104 additions and 65 deletions

View File

@ -634,7 +634,7 @@ mod test {
}, },
vec![], vec![],
vec![], vec![],
None, vec![],
None, None,
entries, entries,
) )
@ -1172,7 +1172,7 @@ mod test {
}, },
vec![], vec![],
vec![], vec![],
None, vec![],
None, None,
entries, entries,
) )

View File

@ -1,4 +1,6 @@
use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; use std::num::{
NonZeroI64, NonZeroIsize, NonZeroU32, NonZeroU64, NonZeroUsize, ParseIntError,
};
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -482,3 +484,16 @@ cast! {
"number too large" "number too large"
})?, })?,
} }
cast! {
NonZeroU32,
self => Value::Int(self.get() as _),
v: i64 => v
.try_into()
.and_then(|v: u32| v.try_into())
.map_err(|_| if v <= 0 {
"number must be positive"
} else {
"number too large"
})?,
}

View File

@ -1,6 +1,6 @@
pub mod resolve; pub mod resolve;
use std::num::NonZeroUsize; use std::num::{NonZeroU32, NonZeroUsize};
use std::sync::Arc; use std::sync::Arc;
use comemo::Track; use comemo::Track;
@ -468,6 +468,14 @@ pub struct GridHeader {
#[default(true)] #[default(true)]
pub repeat: bool, pub repeat: bool,
/// The level of the header. Must not be zero.
///
/// This is used during repetition multiple headers at once. When a header
/// with a lower level starts repeating, all headers with a lower level stop
/// repeating.
#[default(NonZeroU32::ONE)]
pub level: NonZeroU32,
/// The cells and lines within the header. /// The cells and lines within the header.
#[variadic] #[variadic]
pub children: Vec<GridItem>, pub children: Vec<GridItem>,

View File

@ -1,4 +1,4 @@
use std::num::NonZeroUsize; use std::num::{NonZeroU32, NonZeroUsize};
use std::ops::Range; use std::ops::Range;
use std::sync::Arc; use std::sync::Arc;
@ -48,6 +48,7 @@ pub fn grid_to_cellgrid<'a>(
let children = elem.children.iter().map(|child| match child { let children = elem.children.iter().map(|child| match child {
GridChild::Header(header) => ResolvableGridChild::Header { GridChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles), repeat: header.repeat(styles),
level: header.level(styles),
span: header.span(), span: header.span(),
items: header.children.iter().map(resolve_item), items: header.children.iter().map(resolve_item),
}, },
@ -101,6 +102,7 @@ pub fn table_to_cellgrid<'a>(
let children = elem.children.iter().map(|child| match child { let children = elem.children.iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header { TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles), repeat: header.repeat(styles),
level: header.level(styles),
span: header.span(), span: header.span(),
items: header.children.iter().map(resolve_item), items: header.children.iter().map(resolve_item),
}, },
@ -647,7 +649,7 @@ impl<'a> Entry<'a> {
/// Any grid child, which can be either a header or an item. /// Any grid child, which can be either a header or an item.
pub enum ResolvableGridChild<T: ResolvableCell, I> { pub enum ResolvableGridChild<T: ResolvableCell, I> {
Header { repeat: bool, span: Span, items: I }, Header { repeat: bool, level: NonZeroU32, span: Span, items: I },
Footer { repeat: bool, span: Span, items: I }, Footer { repeat: bool, span: Span, items: I },
Item(ResolvableGridItem<T>), Item(ResolvableGridItem<T>),
} }
@ -668,10 +670,10 @@ pub struct CellGrid<'a> {
/// Gutter rows are not included. /// Gutter rows are not included.
/// Contains up to 'rows_without_gutter.len() + 1' vectors of lines. /// Contains up to 'rows_without_gutter.len() + 1' vectors of lines.
pub hlines: Vec<Vec<Line>>, pub hlines: Vec<Vec<Line>>,
/// The repeatable footer of this grid.
pub footer: Option<Repeatable<Footer>>,
/// The repeatable headers of this grid. /// The repeatable headers of this grid.
pub headers: Vec<Repeatable<Header>>, pub headers: Vec<Repeatable<Header>>,
/// The repeatable footer of this grid.
pub footer: Option<Repeatable<Footer>>,
/// Whether this grid has gutters. /// Whether this grid has gutters.
pub has_gutter: bool, pub has_gutter: bool,
} }
@ -684,7 +686,7 @@ impl<'a> CellGrid<'a> {
cells: impl IntoIterator<Item = Cell<'a>>, cells: impl IntoIterator<Item = Cell<'a>>,
) -> Self { ) -> Self {
let entries = cells.into_iter().map(Entry::Cell).collect(); let entries = cells.into_iter().map(Entry::Cell).collect();
Self::new_internal(tracks, gutter, vec![], vec![], None, None, entries) Self::new_internal(tracks, gutter, vec![], vec![], vec![], None, entries)
} }
/// Generates the cell grid, given the tracks and resolved entries. /// Generates the cell grid, given the tracks and resolved entries.
@ -693,7 +695,7 @@ impl<'a> CellGrid<'a> {
gutter: Axes<&[Sizing]>, gutter: Axes<&[Sizing]>,
vlines: Vec<Vec<Line>>, vlines: Vec<Vec<Line>>,
hlines: Vec<Vec<Line>>, hlines: Vec<Vec<Line>>,
header: Option<Repeatable<Header>>, headers: Vec<Repeatable<Header>>,
footer: Option<Repeatable<Footer>>, footer: Option<Repeatable<Footer>>,
entries: Vec<Entry<'a>>, entries: Vec<Entry<'a>>,
) -> Self { ) -> Self {
@ -747,8 +749,8 @@ impl<'a> CellGrid<'a> {
entries, entries,
vlines, vlines,
hlines, hlines,
headers,
footer, footer,
headers: header.into_iter().collect(),
has_gutter, has_gutter,
} }
} }
@ -972,6 +974,9 @@ struct RowGroupData {
span: Span, span: Span,
kind: RowGroupKind, kind: RowGroupKind,
/// Level of this header or footer.
repeatable_level: NonZeroU32,
/// Start of the range of indices of hlines at the top of the row group. /// Start of the range of indices of hlines at the top of the row group.
/// This is always the first index after the last hline before we started /// This is always the first index after the last hline before we started
/// building the row group - any upcoming hlines would appear at least at /// building the row group - any upcoming hlines would appear at least at
@ -1019,7 +1024,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
let mut pending_vlines: Vec<(Span, Line)> = vec![]; let mut pending_vlines: Vec<(Span, Line)> = vec![];
let has_gutter = self.gutter.any(|tracks| !tracks.is_empty()); let has_gutter = self.gutter.any(|tracks| !tracks.is_empty());
let mut header: Option<Header> = None; let mut headers: Vec<Header> = vec![];
let mut repeat_header = false; let mut repeat_header = false;
// Stores where the footer is supposed to end, its span, and the // Stores where the footer is supposed to end, its span, and the
@ -1063,7 +1068,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
columns, columns,
&mut pending_hlines, &mut pending_hlines,
&mut pending_vlines, &mut pending_vlines,
&mut header, &mut headers,
&mut repeat_header, &mut repeat_header,
&mut footer, &mut footer,
&mut repeat_footer, &mut repeat_footer,
@ -1084,9 +1089,9 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
row_amount, row_amount,
)?; )?;
let (header, footer) = self.finalize_headers_and_footers( let (headers, footer) = self.finalize_headers_and_footers(
has_gutter, has_gutter,
header, headers,
repeat_header, repeat_header,
footer, footer,
repeat_footer, repeat_footer,
@ -1098,7 +1103,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
self.gutter, self.gutter,
vlines, vlines,
hlines, hlines,
header, headers,
footer, footer,
resolved_cells, resolved_cells,
)) ))
@ -1118,7 +1123,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
columns: usize, columns: usize,
pending_hlines: &mut Vec<(Span, Line, bool)>, pending_hlines: &mut Vec<(Span, Line, bool)>,
pending_vlines: &mut Vec<(Span, Line)>, pending_vlines: &mut Vec<(Span, Line)>,
header: &mut Option<Header>, headers: &mut Vec<Header>,
repeat_header: &mut bool, repeat_header: &mut bool,
footer: &mut Option<(usize, Span, Footer)>, footer: &mut Option<(usize, Span, Footer)>,
repeat_footer: &mut bool, repeat_footer: &mut bool,
@ -1158,15 +1163,12 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
let mut first_available_row = 0; let mut first_available_row = 0;
let (header_footer_items, simple_item) = match child { let (header_footer_items, simple_item) = match child {
ResolvableGridChild::Header { repeat, span, items, .. } => { ResolvableGridChild::Header { repeat, level, span, items, .. } => {
if header.is_some() {
bail!(span, "cannot have more than one header");
}
row_group_data = Some(RowGroupData { row_group_data = Some(RowGroupData {
range: None, range: None,
span, span,
kind: RowGroupKind::Header, kind: RowGroupKind::Header,
repeatable_level: level,
top_hlines_start: pending_hlines.len(), top_hlines_start: pending_hlines.len(),
top_hlines_end: None, top_hlines_end: None,
}); });
@ -1198,6 +1200,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
range: None, range: None,
span, span,
kind: RowGroupKind::Footer, kind: RowGroupKind::Footer,
repeatable_level: NonZeroU32::ONE,
top_hlines_start: pending_hlines.len(), top_hlines_start: pending_hlines.len(),
top_hlines_end: None, top_hlines_end: None,
}); });
@ -1330,7 +1333,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
cell_y, cell_y,
colspan, colspan,
rowspan, rowspan,
header.as_ref(), headers,
footer.as_ref(), footer.as_ref(),
resolved_cells, resolved_cells,
&mut local_auto_index, &mut local_auto_index,
@ -1518,15 +1521,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
match row_group.kind { match row_group.kind {
RowGroupKind::Header => { RowGroupKind::Header => {
if group_range.start != 0 { headers.push(Header {
bail!(
row_group.span,
"header must start at the first row";
hint: "remove any rows before the header"
);
}
*header = Some(Header {
start: group_range.start, start: group_range.start,
// Later on, we have to correct this number in case there // Later on, we have to correct this number in case there
@ -1535,7 +1530,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// below. // below.
end: group_range.end, end: group_range.end,
level: 1, level: row_group.repeatable_level.get(),
}); });
} }
@ -1730,13 +1725,14 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
fn finalize_headers_and_footers( fn finalize_headers_and_footers(
&self, &self,
has_gutter: bool, has_gutter: bool,
header: Option<Header>, headers: Vec<Header>,
repeat_header: bool, repeat_header: bool,
footer: Option<(usize, Span, Footer)>, footer: Option<(usize, Span, Footer)>,
repeat_footer: bool, repeat_footer: bool,
row_amount: usize, row_amount: usize,
) -> SourceResult<(Option<Repeatable<Header>>, Option<Repeatable<Footer>>)> { ) -> SourceResult<(Vec<Repeatable<Header>>, Option<Repeatable<Footer>>)> {
let header = header let headers: Vec<Repeatable<Header>> = headers
.into_iter()
.map(|mut header| { .map(|mut header| {
// Repeat the gutter below a header (hence why we don't // Repeat the gutter below a header (hence why we don't
// subtract 1 from the gutter case). // subtract 1 from the gutter case).
@ -1774,7 +1770,8 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
} else { } else {
Repeatable::NotRepeated(header) Repeatable::NotRepeated(header)
} }
}); })
.collect();
let footer = footer let footer = footer
.map(|(footer_end, footer_span, mut footer)| { .map(|(footer_end, footer_span, mut footer)| {
@ -1782,8 +1779,10 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
bail!(footer_span, "footer must end at the last row"); bail!(footer_span, "footer must end at the last row");
} }
let header_end = // TODO: will need a global slice of headers and footers for
header.as_ref().map(Repeatable::unwrap).map(|header| header.end); // when we have multiple footers
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.
@ -1792,7 +1791,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// Include the gutter right before the footer, unless there is // Include the gutter right before the footer, unless there is
// none, or the gutter is already included in the header (no // none, or the gutter is already included in the header (no
// rows between the header and the footer). // rows between the header and the footer).
if header_end != Some(footer.start) { if last_header_end != Some(footer.start) {
footer.start = footer.start.saturating_sub(1); footer.start = footer.start.saturating_sub(1);
} }
@ -1820,7 +1819,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
} }
}); });
Ok((header, footer)) Ok((headers, footer))
} }
/// Resolves the cell's fields based on grid-wide properties. /// Resolves the cell's fields based on grid-wide properties.
@ -1991,24 +1990,26 @@ fn expand_row_group(
/// Check if a cell's fixed row would conflict with a header or footer. /// Check if a cell's fixed row would conflict with a header or footer.
fn check_for_conflicting_cell_row( fn check_for_conflicting_cell_row(
header: Option<&Header>, headers: &[Header],
footer: Option<&(usize, Span, Footer)>, footer: Option<&(usize, Span, Footer)>,
cell_y: usize, cell_y: usize,
rowspan: usize, rowspan: usize,
) -> HintedStrResult<()> { ) -> HintedStrResult<()> {
if let Some(header) = header { // TODO: use upcoming headers slice to make this an O(1) check
// NOTE: y + rowspan >, not >=, header.start, to check if the rowspan // NOTE: y + rowspan >, not >=, header.start, to check if the rowspan
// enters the header. For example, consider a rowspan of 1: if // enters the header. For example, consider a rowspan of 1: if
// `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 cell_y < header.end && cell_y + rowspan > header.start { if headers
.iter()
.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"
); );
} }
}
if let Some((_, _, footer)) = footer { if let Some((_, _, footer)) = footer {
if cell_y < footer.end && cell_y + rowspan > footer.start { if cell_y < footer.end && cell_y + rowspan > footer.start {
@ -2037,7 +2038,7 @@ fn resolve_cell_position(
cell_y: Smart<usize>, cell_y: Smart<usize>,
colspan: usize, colspan: usize,
rowspan: usize, rowspan: usize,
header: Option<&Header>, headers: &[Header],
footer: Option<&(usize, Span, Footer)>, footer: Option<&(usize, Span, Footer)>,
resolved_cells: &[Option<Entry>], resolved_cells: &[Option<Entry>],
auto_index: &mut usize, auto_index: &mut usize,
@ -2062,7 +2063,7 @@ fn resolve_cell_position(
// but automatically-positioned cells will avoid conflicts by // but automatically-positioned cells will avoid conflicts by
// simply skipping existing cells, headers and footers. // simply skipping existing cells, headers and footers.
let resolved_index = find_next_available_position::<false>( let resolved_index = find_next_available_position::<false>(
header, headers,
footer, footer,
resolved_cells, resolved_cells,
columns, columns,
@ -2102,7 +2103,7 @@ fn resolve_cell_position(
// footer (but only if it isn't already in one, otherwise there // footer (but only if it isn't already in one, otherwise there
// will already be a separate check). // will already be a separate check).
if !in_row_group { if !in_row_group {
check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?; check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?;
} }
cell_index(cell_x, cell_y) cell_index(cell_x, cell_y)
@ -2120,7 +2121,7 @@ fn resolve_cell_position(
// ('None'), in which case we'd create a new row to place this // ('None'), in which case we'd create a new row to place this
// cell in. // cell in.
find_next_available_position::<true>( find_next_available_position::<true>(
header, headers,
footer, footer,
resolved_cells, resolved_cells,
columns, columns,
@ -2134,7 +2135,7 @@ fn resolve_cell_position(
// footer (but only if it isn't already in one, otherwise there // footer (but only if it isn't already in one, otherwise there
// will already be a separate check). // will already be a separate check).
if !in_row_group { if !in_row_group {
check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?; check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?;
} }
// Let's find the first column which has that row available. // Let's find the first column which has that row available.
@ -2168,7 +2169,7 @@ fn resolve_cell_position(
/// have cells specified by the user) as well as any headers and footers. /// have cells specified by the user) as well as any headers and footers.
#[inline] #[inline]
fn find_next_available_position<const SKIP_ROWS: bool>( fn find_next_available_position<const SKIP_ROWS: bool>(
header: Option<&Header>, headers: &[Header],
footer: Option<&(usize, Span, Footer)>, footer: Option<&(usize, Span, Footer)>,
resolved_cells: &[Option<Entry<'_>>], resolved_cells: &[Option<Entry<'_>>],
columns: usize, columns: usize,
@ -2195,9 +2196,9 @@ fn find_next_available_position<const SKIP_ROWS: bool>(
// would become impractically large before this overflows. // would become impractically large before this overflows.
resolved_index += 1; resolved_index += 1;
} }
} else if let Some(header) = } else if let Some(header) = headers.iter().find(|header| {
header.filter(|header| resolved_index < header.end * columns) (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

@ -1,4 +1,4 @@
use std::num::NonZeroUsize; use std::num::{NonZeroU32, NonZeroUsize};
use std::sync::Arc; use std::sync::Arc;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
@ -494,6 +494,14 @@ pub struct TableHeader {
#[default(true)] #[default(true)]
pub repeat: bool, pub repeat: bool,
/// The level of the header. Must not be zero.
///
/// This is used during repetition multiple headers at once. When a header
/// with a lower level starts repeating, all headers with a lower level stop
/// repeating.
#[default(NonZeroU32::ONE)]
pub level: NonZeroU32,
/// The cells and lines within the header. /// The cells and lines within the header.
#[variadic] #[variadic]
pub children: Vec<TableItem>, pub children: Vec<TableItem>,

View File

@ -26,7 +26,7 @@ pub use once_cell;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::hash::Hash; use std::hash::Hash;
use std::iter::{Chain, Flatten, Rev}; use std::iter::{Chain, Flatten, Rev};
use std::num::NonZeroUsize; use std::num::{NonZeroU32, NonZeroUsize};
use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; use std::ops::{Add, Deref, Div, Mul, Neg, Sub};
use std::sync::Arc; use std::sync::Arc;
@ -72,6 +72,13 @@ impl NonZeroExt for NonZeroUsize {
}; };
} }
impl NonZeroExt for NonZeroU32 {
const ONE: Self = match Self::new(1) {
Some(v) => v,
None => unreachable!(),
};
}
/// Extra methods for [`Arc`]. /// Extra methods for [`Arc`].
pub trait ArcExt<T> { pub trait ArcExt<T> {
/// Takes the inner value if there is exactly one strong reference and /// Takes the inner value if there is exactly one strong reference and