Ensure table headers trigger rowbreak (#6687)
@ -19,7 +19,7 @@ use typst_library::text::TextElem;
|
|||||||
use typst_library::visualize::{Paint, Stroke};
|
use typst_library::visualize::{Paint, Stroke};
|
||||||
|
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::NonZeroExt;
|
use typst_utils::{NonZeroExt, SmallBitSet};
|
||||||
|
|
||||||
use crate::introspection::SplitLocator;
|
use crate::introspection::SplitLocator;
|
||||||
|
|
||||||
@ -1048,11 +1048,6 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
// automatically-positioned cell.
|
// automatically-positioned cell.
|
||||||
let mut auto_index: usize = 0;
|
let mut auto_index: usize = 0;
|
||||||
|
|
||||||
// The next header after the latest auto-positioned cell. This is used
|
|
||||||
// to avoid checking for collision with headers that were already
|
|
||||||
// skipped.
|
|
||||||
let mut next_header = 0;
|
|
||||||
|
|
||||||
// We have to rebuild the grid to account for fixed cell positions.
|
// We have to rebuild the grid to account for fixed cell positions.
|
||||||
//
|
//
|
||||||
// Create at least 'children.len()' positions, since there could be at
|
// Create at least 'children.len()' positions, since there could be at
|
||||||
@ -1067,17 +1062,26 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
let Some(child_count) = children.len().checked_next_multiple_of(columns) else {
|
let Some(child_count) = children.len().checked_next_multiple_of(columns) else {
|
||||||
bail!(self.span, "too many cells or lines were given")
|
bail!(self.span, "too many cells or lines were given")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Rows in this bitset are occupied by an existing header.
|
||||||
|
// This allows for efficiently checking whether a cell would collide
|
||||||
|
// with a header at a certain row. (For footers, it's easy as there is
|
||||||
|
// only one.)
|
||||||
|
//
|
||||||
|
// TODO(subfooters): how to add a footer here while avoiding
|
||||||
|
// unnecessary allocations?
|
||||||
|
let mut header_rows: SmallBitSet = SmallBitSet::new();
|
||||||
let mut resolved_cells: Vec<Option<Entry>> = Vec::with_capacity(child_count);
|
let mut resolved_cells: Vec<Option<Entry>> = Vec::with_capacity(child_count);
|
||||||
for child in children {
|
for child in children {
|
||||||
self.resolve_grid_child(
|
self.resolve_grid_child(
|
||||||
columns,
|
columns,
|
||||||
&mut pending_hlines,
|
&mut pending_hlines,
|
||||||
&mut pending_vlines,
|
&mut pending_vlines,
|
||||||
|
&mut header_rows,
|
||||||
&mut headers,
|
&mut headers,
|
||||||
&mut footer,
|
&mut footer,
|
||||||
&mut repeat_footer,
|
&mut repeat_footer,
|
||||||
&mut auto_index,
|
&mut auto_index,
|
||||||
&mut next_header,
|
|
||||||
&mut resolved_cells,
|
&mut resolved_cells,
|
||||||
&mut at_least_one_cell,
|
&mut at_least_one_cell,
|
||||||
child,
|
child,
|
||||||
@ -1129,11 +1133,11 @@ 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_rows: &mut SmallBitSet,
|
||||||
headers: &mut Vec<Repeatable<Header>>,
|
headers: &mut Vec<Repeatable<Header>>,
|
||||||
footer: &mut Option<(usize, Span, Footer)>,
|
footer: &mut Option<(usize, Span, Footer)>,
|
||||||
repeat_footer: &mut bool,
|
repeat_footer: &mut bool,
|
||||||
auto_index: &mut usize,
|
auto_index: &mut usize,
|
||||||
next_header: &mut usize,
|
|
||||||
resolved_cells: &mut Vec<Option<Entry<'x>>>,
|
resolved_cells: &mut Vec<Option<Entry<'x>>>,
|
||||||
at_least_one_cell: &mut bool,
|
at_least_one_cell: &mut bool,
|
||||||
child: ResolvableGridChild<T, I>,
|
child: ResolvableGridChild<T, I>,
|
||||||
@ -1149,18 +1153,35 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
// in which case this variable remains 'None'.
|
// in which case this variable remains 'None'.
|
||||||
let mut row_group_data: Option<RowGroupData> = None;
|
let mut row_group_data: Option<RowGroupData> = None;
|
||||||
|
|
||||||
// The normal auto index should only be stepped (upon placing an
|
// Usually, the global auto index is stepped only when a cell with
|
||||||
// automatically-positioned cell, to indicate the position of the
|
// fully automatic position (no fixed x/y) is placed, advancing one
|
||||||
// next) outside of headers or footers, in which case the auto
|
// position. In that usual case, 'local_auto_index == auto_index'
|
||||||
// index will be updated with the local auto index. Inside headers
|
// holds, as 'local_auto_index' is what the code below actually
|
||||||
// and footers, however, cells can only start after the first empty
|
// updates.
|
||||||
// row (as determined by 'first_available_row' below), meaning that
|
//
|
||||||
// the next automatically-positioned cell will be in a different
|
// However, headers and footers must trigger a rowbreak if the
|
||||||
// position than it would usually be if it would be in a non-empty
|
// previous row isn't empty, given they cannot occupy only part of a
|
||||||
// row, so we must step a local index inside headers and footers
|
// row. Therefore, the initial auto index used by their auto cells
|
||||||
// instead, and use a separate counter outside them.
|
// should be right below the first empty row.
|
||||||
|
//
|
||||||
|
// The problem is that we don't know whether the header will actually
|
||||||
|
// have an auto cell or not, and we don't want the external auto index
|
||||||
|
// to change (no rowbreak should be triggered) if the header has no
|
||||||
|
// auto cells (although a fully empty header does count as having
|
||||||
|
// auto cells, albeit empty).
|
||||||
|
//
|
||||||
|
// So, we use a separate auto index counter inside the header. It starts
|
||||||
|
// below the first non-empty row. If the header only has fixed-position
|
||||||
|
// cells, the external counter is unchanged. Otherwise (has auto cells
|
||||||
|
// or is empty), the external counter is synchronized and moved to
|
||||||
|
// below the header.
|
||||||
|
//
|
||||||
|
// This ensures lines and cells specified below the header in the
|
||||||
|
// source code also appear below it in the final grid/table.
|
||||||
let local_auto_index = if matches!(child, ResolvableGridChild::Item(_)) {
|
let local_auto_index = if matches!(child, ResolvableGridChild::Item(_)) {
|
||||||
auto_index
|
// Re-borrow the original auto index so we can reuse this mutable
|
||||||
|
// reference later.
|
||||||
|
&mut *auto_index
|
||||||
} else {
|
} else {
|
||||||
// Although 'usize' is Copy, we need to be explicit here that we
|
// Although 'usize' is Copy, we need to be explicit here that we
|
||||||
// aren't reborrowing the original auto index but rather making a
|
// aren't reborrowing the original auto index but rather making a
|
||||||
@ -1168,24 +1189,6 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
&mut (*auto_index).clone()
|
&mut (*auto_index).clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: usually, if 'next_header' were to be updated inside a row
|
|
||||||
// group (indicating a header was skipped by a cell), that would
|
|
||||||
// indicate a collision between the row group and that header, which
|
|
||||||
// is an error. However, the exception is for the first auto cell of
|
|
||||||
// the row group, which may skip headers while searching for a position
|
|
||||||
// where to begin the row group in the first place.
|
|
||||||
//
|
|
||||||
// Therefore, we cannot safely share the counter in the row group with
|
|
||||||
// the counter used by auto cells outside, as it might update it in a
|
|
||||||
// valid situation, whereas it must not, since its auto cells use a
|
|
||||||
// different auto index counter and will have seen different headers,
|
|
||||||
// so we copy the next header counter while inside a row group.
|
|
||||||
let local_next_header = if matches!(child, ResolvableGridChild::Item(_)) {
|
|
||||||
next_header
|
|
||||||
} else {
|
|
||||||
&mut (*next_header).clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// The first row in which this table group can fit.
|
// The first row in which this table group can fit.
|
||||||
//
|
//
|
||||||
// Within headers and footers, this will correspond to the first
|
// Within headers and footers, this will correspond to the first
|
||||||
@ -1253,7 +1256,9 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut had_auto_cells = false;
|
||||||
let items = header_footer_items.into_iter().flatten().chain(simple_item);
|
let items = header_footer_items.into_iter().flatten().chain(simple_item);
|
||||||
|
|
||||||
for item in items {
|
for item in items {
|
||||||
let cell = match item {
|
let cell = match item {
|
||||||
ResolvableGridItem::HLine { y, start, end, stroke, span, position } => {
|
ResolvableGridItem::HLine { y, start, end, stroke, span, position } => {
|
||||||
@ -1364,16 +1369,17 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
let resolved_index = {
|
let resolved_index = {
|
||||||
let cell_x = cell.x(self.styles);
|
let cell_x = cell.x(self.styles);
|
||||||
let cell_y = cell.y(self.styles);
|
let cell_y = cell.y(self.styles);
|
||||||
|
had_auto_cells |= cell_x.is_auto() && cell_y.is_auto();
|
||||||
resolve_cell_position(
|
resolve_cell_position(
|
||||||
cell_x,
|
cell_x,
|
||||||
cell_y,
|
cell_y,
|
||||||
colspan,
|
colspan,
|
||||||
rowspan,
|
rowspan,
|
||||||
|
header_rows,
|
||||||
headers,
|
headers,
|
||||||
footer.as_ref(),
|
footer.as_ref(),
|
||||||
resolved_cells,
|
resolved_cells,
|
||||||
local_auto_index,
|
local_auto_index,
|
||||||
local_next_header,
|
|
||||||
first_available_row,
|
first_available_row,
|
||||||
columns,
|
columns,
|
||||||
row_group_data.is_some(),
|
row_group_data.is_some(),
|
||||||
@ -1511,8 +1517,10 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
|
|
||||||
None => {
|
None => {
|
||||||
// Empty header/footer: consider the header/footer to be
|
// Empty header/footer: consider the header/footer to be
|
||||||
// at the next empty row after the latest auto index.
|
// automatically positioned at the next empty row after the
|
||||||
|
// latest auto index.
|
||||||
*local_auto_index = first_available_row * columns;
|
*local_auto_index = first_available_row * columns;
|
||||||
|
had_auto_cells = true;
|
||||||
let group_start = first_available_row;
|
let group_start = first_available_row;
|
||||||
let group_end = group_start + 1;
|
let group_end = group_start + 1;
|
||||||
|
|
||||||
@ -1556,6 +1564,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if had_auto_cells {
|
||||||
|
// Header/footer was automatically positioned (either by having
|
||||||
|
// auto cells or by virtue of being empty), so trigger a
|
||||||
|
// rowbreak. Move auto index counter right below it.
|
||||||
|
*auto_index = group_range.end * columns;
|
||||||
|
}
|
||||||
|
|
||||||
match row_group.kind {
|
match row_group.kind {
|
||||||
RowGroupKind::Header => {
|
RowGroupKind::Header => {
|
||||||
let data = Header {
|
let data = Header {
|
||||||
@ -1563,7 +1578,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
// is gutter. But only once all cells have been analyzed
|
// is gutter. But only once all cells have been analyzed
|
||||||
// and the header has fully expanded in the fixup loop
|
// and the header has fully expanded in the fixup loop
|
||||||
// below.
|
// below.
|
||||||
range: group_range,
|
range: group_range.clone(),
|
||||||
|
|
||||||
level: row_group.repeatable_level.get(),
|
level: row_group.repeatable_level.get(),
|
||||||
|
|
||||||
@ -1572,24 +1587,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
short_lived: false,
|
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.range.start;
|
|
||||||
for conflicting_header in
|
|
||||||
headers.iter_mut().rev().take_while(move |h| {
|
|
||||||
let conflicts = h.range.end == consecutive_header_start
|
|
||||||
&& h.level >= data.level;
|
|
||||||
|
|
||||||
consecutive_header_start = h.range.start;
|
|
||||||
conflicts
|
|
||||||
})
|
|
||||||
{
|
|
||||||
conflicting_header.short_lived = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.push(Repeatable { inner: data, repeated: row_group.repeat });
|
headers.push(Repeatable { inner: data, repeated: row_group.repeat });
|
||||||
|
|
||||||
|
for row in group_range {
|
||||||
|
header_rows.insert(row);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowGroupKind::Footer => {
|
RowGroupKind::Footer => {
|
||||||
@ -1786,9 +1788,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
row_amount: usize,
|
row_amount: usize,
|
||||||
at_least_one_cell: bool,
|
at_least_one_cell: bool,
|
||||||
) -> SourceResult<Option<Repeatable<Footer>>> {
|
) -> SourceResult<Option<Repeatable<Footer>>> {
|
||||||
// Mark consecutive headers right before the end of the table, or the
|
headers.sort_unstable_by_key(|h| h.range.start);
|
||||||
// final footer, as short lived, given that there are no normal rows
|
|
||||||
// after them, so repeating them is pointless.
|
// Mark consecutive headers in those positions as short-lived:
|
||||||
|
// (a) before a header of lower level;
|
||||||
|
// (b) right before the end of the table or the final footer;
|
||||||
|
// That's because they would stop repeating immediately, so don't even
|
||||||
|
// attempt to.
|
||||||
//
|
//
|
||||||
// It is important to do this BEFORE we update header and footer ranges
|
// It is important to do this BEFORE we update header and footer ranges
|
||||||
// due to gutter below as 'row_amount' doesn't consider gutter.
|
// due to gutter below as 'row_amount' doesn't consider gutter.
|
||||||
@ -1796,16 +1802,20 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
|||||||
// TODO(subfooters): take the last footer if it is at the end and
|
// TODO(subfooters): take the last footer if it is at the end and
|
||||||
// backtrack through consecutive footers until the first one in the
|
// backtrack through consecutive footers until the first one in the
|
||||||
// sequence is found. If there is no footer at the end, there are no
|
// sequence is found. If there is no footer at the end, there are no
|
||||||
// haeders to turn short-lived.
|
// headers to turn short-lived.
|
||||||
let mut consecutive_header_start =
|
let mut consecutive_header_start =
|
||||||
footer.as_ref().map(|(_, _, f)| f.start).unwrap_or(row_amount);
|
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 mut last_consec_level = 0;
|
||||||
let at_the_end = h.range.end == consecutive_header_start;
|
for header in headers.iter_mut().rev() {
|
||||||
|
if header.range.end == consecutive_header_start
|
||||||
|
&& header.level >= last_consec_level
|
||||||
|
{
|
||||||
|
header.short_lived = true;
|
||||||
|
} else {
|
||||||
|
last_consec_level = header.level;
|
||||||
|
}
|
||||||
|
|
||||||
consecutive_header_start = h.range.start;
|
consecutive_header_start = header.range.start;
|
||||||
at_the_end
|
|
||||||
}) {
|
|
||||||
header_at_the_end.short_lived = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repeat the gutter below a header (hence why we don't
|
// Repeat the gutter below a header (hence why we don't
|
||||||
@ -2066,32 +2076,37 @@ 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_rows: &SmallBitSet,
|
||||||
headers: &[Repeatable<Header>],
|
headers: &[Repeatable<Header>],
|
||||||
footer: Option<&(usize, Span, Footer)>,
|
footer: Option<&(usize, Span, Footer)>,
|
||||||
cell_y: usize,
|
cell_y: usize,
|
||||||
rowspan: usize,
|
rowspan: usize,
|
||||||
) -> HintedStrResult<()> {
|
) -> HintedStrResult<()> {
|
||||||
// NOTE: y + rowspan >, not >=, header.start, to check if the rowspan
|
if !headers.is_empty()
|
||||||
// enters the header. For example, consider a rowspan of 1: if
|
&& let Some(row) =
|
||||||
// `y + 1 = header.start` holds, that means `y < header.start`, and it
|
(cell_y..cell_y + rowspan).find(|&row| header_rows.contains(row))
|
||||||
// only occupies one row (`y`), so the cell is actually not in
|
|
||||||
// conflict.
|
|
||||||
if headers
|
|
||||||
.iter()
|
|
||||||
.any(|header| cell_y < header.range.end && cell_y + rowspan > header.range.start)
|
|
||||||
{
|
{
|
||||||
bail!(
|
bail!(
|
||||||
"cell would conflict with header spanning the same position";
|
"cell would conflict with header also spanning row {row}";
|
||||||
hint: "try moving the cell or the header"
|
hint: "try moving the cell or the header"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: y + rowspan >, not >=, footer.start, to check if the rowspan
|
||||||
|
// enters the footer. For example, consider a rowspan of 1: if
|
||||||
|
// `y + 1 = footer.start` holds, that means `y < footer.start`, and it
|
||||||
|
// only occupies one row (`y`), so the cell is actually not in
|
||||||
|
// conflict.
|
||||||
if let Some((_, _, footer)) = footer
|
if let Some((_, _, footer)) = footer
|
||||||
&& cell_y < footer.end
|
&& cell_y < footer.end
|
||||||
&& cell_y + rowspan > footer.start
|
&& cell_y + rowspan > footer.start
|
||||||
{
|
{
|
||||||
|
let row = (cell_y..cell_y + rowspan)
|
||||||
|
.find(|row| (footer.start..footer.end).contains(row))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
bail!(
|
bail!(
|
||||||
"cell would conflict with footer spanning the same position";
|
"cell would conflict with footer also spanning row {row}";
|
||||||
hint: "try reducing the cell's rowspan or moving the footer"
|
hint: "try reducing the cell's rowspan or moving the footer"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2114,11 +2129,11 @@ fn resolve_cell_position(
|
|||||||
cell_y: Smart<usize>,
|
cell_y: Smart<usize>,
|
||||||
colspan: usize,
|
colspan: usize,
|
||||||
rowspan: usize,
|
rowspan: usize,
|
||||||
|
header_rows: &SmallBitSet,
|
||||||
headers: &[Repeatable<Header>],
|
headers: &[Repeatable<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,
|
||||||
next_header: &mut usize,
|
|
||||||
first_available_row: usize,
|
first_available_row: usize,
|
||||||
columns: usize,
|
columns: usize,
|
||||||
in_row_group: bool,
|
in_row_group: bool,
|
||||||
@ -2140,12 +2155,11 @@ 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(
|
let resolved_index = find_next_available_position(
|
||||||
headers,
|
header_rows,
|
||||||
footer,
|
footer,
|
||||||
resolved_cells,
|
resolved_cells,
|
||||||
columns,
|
columns,
|
||||||
*auto_index,
|
*auto_index,
|
||||||
next_header,
|
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@ -2182,7 +2196,13 @@ 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(headers, footer, cell_y, rowspan)?;
|
check_for_conflicting_cell_row(
|
||||||
|
header_rows,
|
||||||
|
headers,
|
||||||
|
footer,
|
||||||
|
cell_y,
|
||||||
|
rowspan,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
cell_index(cell_x, cell_y)
|
cell_index(cell_x, cell_y)
|
||||||
@ -2200,26 +2220,11 @@ 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(
|
find_next_available_position(
|
||||||
headers,
|
header_rows,
|
||||||
footer,
|
footer,
|
||||||
resolved_cells,
|
resolved_cells,
|
||||||
columns,
|
columns,
|
||||||
initial_index,
|
initial_index,
|
||||||
// Make our own copy of the 'next_header' counter, since it
|
|
||||||
// should only be updated by auto cells. However, we cannot
|
|
||||||
// start with the same value as we are searching from the
|
|
||||||
// start, and not from 'auto_index', so auto cells might
|
|
||||||
// have skipped some headers already which this cell will
|
|
||||||
// also need to skip.
|
|
||||||
//
|
|
||||||
// We could, in theory, keep a separate 'next_header'
|
|
||||||
// counter for cells with fixed columns. But then we would
|
|
||||||
// need one for every column, and much like how there isn't
|
|
||||||
// an index counter for each column either, the potential
|
|
||||||
// speed gain seems less relevant for a less used feature.
|
|
||||||
// Still, it is something to consider for the future if
|
|
||||||
// this turns out to be a bottleneck in important cases.
|
|
||||||
&mut 0,
|
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -2230,7 +2235,13 @@ 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(headers, footer, cell_y, rowspan)?;
|
check_for_conflicting_cell_row(
|
||||||
|
header_rows,
|
||||||
|
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.
|
||||||
@ -2267,12 +2278,11 @@ fn resolve_cell_position(
|
|||||||
/// the column. That is used to find a position for a fixed column cell.
|
/// the column. That is used to find a position for a fixed column cell.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn find_next_available_position(
|
fn find_next_available_position(
|
||||||
headers: &[Repeatable<Header>],
|
header_rows: &SmallBitSet,
|
||||||
footer: Option<&(usize, Span, Footer)>,
|
footer: Option<&(usize, Span, Footer)>,
|
||||||
resolved_cells: &[Option<Entry<'_>>],
|
resolved_cells: &[Option<Entry<'_>>],
|
||||||
columns: usize,
|
columns: usize,
|
||||||
initial_index: usize,
|
initial_index: usize,
|
||||||
next_header: &mut usize,
|
|
||||||
skip_rows: bool,
|
skip_rows: bool,
|
||||||
) -> HintedStrResult<usize> {
|
) -> HintedStrResult<usize> {
|
||||||
let mut resolved_index = initial_index;
|
let mut resolved_index = initial_index;
|
||||||
@ -2296,33 +2306,30 @@ fn find_next_available_position(
|
|||||||
// 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) = headers
|
} else if header_rows.contains(resolved_index / columns) {
|
||||||
.get(*next_header)
|
// Skip header rows (can't place a cell inside it from outside it).
|
||||||
.filter(|header| resolved_index >= header.range.start * columns)
|
|
||||||
{
|
|
||||||
// Skip header (can't place a cell inside it from outside it).
|
|
||||||
// No changes needed if we already passed this header (which
|
|
||||||
// also triggers this branch) - in that case, we only update the
|
|
||||||
// counter.
|
|
||||||
if resolved_index < header.range.end * columns {
|
|
||||||
resolved_index = header.range.end * columns;
|
|
||||||
|
|
||||||
if skip_rows {
|
if skip_rows {
|
||||||
// Ensure the cell's chosen column is kept after the
|
// Ensure the cell's chosen column is kept after the header.
|
||||||
// header.
|
resolved_index += columns;
|
||||||
resolved_index += initial_index % columns;
|
} else {
|
||||||
|
// Skip to the start of the next row.
|
||||||
|
//
|
||||||
|
// Add 1 to resolved index to force moving to the next row if
|
||||||
|
// this is at the start of one. At the end of one, '+ 1'
|
||||||
|
// already pushes to the next one and 'next_multiple_of' does
|
||||||
|
// not modify it, so nothing bad happens then either.
|
||||||
|
resolved_index = (resolved_index + 1).next_multiple_of(columns);
|
||||||
}
|
}
|
||||||
}
|
} else if let Some((footer_end, _, footer)) = footer
|
||||||
|
&& resolved_index >= footer.start * columns
|
||||||
// From now on, only check the headers afterwards.
|
&& resolved_index < *footer_end * columns
|
||||||
*next_header += 1;
|
{
|
||||||
} else if let Some((footer_end, _, _)) = footer.filter(|(end, _, footer)| {
|
|
||||||
resolved_index >= footer.start * columns && resolved_index < *end * columns
|
|
||||||
}) {
|
|
||||||
// Skip footer, for the same reason.
|
// Skip footer, for the same reason.
|
||||||
resolved_index = *footer_end * columns;
|
resolved_index = *footer_end * columns;
|
||||||
|
|
||||||
if skip_rows {
|
if skip_rows {
|
||||||
|
// Ensure the cell's chosen column is kept after the
|
||||||
|
// footer.
|
||||||
resolved_index += initial_index % columns;
|
resolved_index += initial_index % columns;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
BIN
tests/ref/grid-footer-rowbreak-line.png
Normal file
After Width: | Height: | Size: 327 B |
BIN
tests/ref/grid-header-multiple-unordered.png
Normal file
After Width: | Height: | Size: 469 B |
BIN
tests/ref/grid-header-rowbreak-auto-and-fixed-pos.png
Normal file
After Width: | Height: | Size: 279 B |
BIN
tests/ref/grid-header-rowbreak-auto-pos.png
Normal file
After Width: | Height: | Size: 452 B |
BIN
tests/ref/grid-header-rowbreak-fixed-pos.png
Normal file
After Width: | Height: | Size: 616 B |
BIN
tests/ref/grid-header-rowbreak-mixed-pos.png
Normal file
After Width: | Height: | Size: 659 B |
BIN
tests/ref/grid-header-skip-unordered.png
Normal file
After Width: | Height: | Size: 294 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
BIN
tests/ref/issue-6666-auto-hlines-around-footer.png
Normal file
After Width: | Height: | Size: 683 B |
BIN
tests/ref/issue-6666-auto-hlines-around-header.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
@ -105,7 +105,7 @@
|
|||||||
grid.cell(x: 1, y: 3, rowspan: 4)[b],
|
grid.cell(x: 1, y: 3, rowspan: 4)[b],
|
||||||
grid.cell(y: 2, rowspan: 2)[a],
|
grid.cell(y: 2, rowspan: 2)[a],
|
||||||
grid.footer(),
|
grid.footer(),
|
||||||
// Error: 3-27 cell would conflict with footer spanning the same position
|
// Error: 3-27 cell would conflict with footer also spanning row 7
|
||||||
// Hint: 3-27 try reducing the cell's rowspan or moving the footer
|
// Hint: 3-27 try reducing the cell's rowspan or moving the footer
|
||||||
grid.cell(x: 1, y: 7)[d],
|
grid.cell(x: 1, y: 7)[d],
|
||||||
)
|
)
|
||||||
@ -120,7 +120,7 @@
|
|||||||
grid.cell(x: 1, y: 3, rowspan: 4)[b],
|
grid.cell(x: 1, y: 3, rowspan: 4)[b],
|
||||||
grid.cell(y: 2, rowspan: 2)[a],
|
grid.cell(y: 2, rowspan: 2)[a],
|
||||||
grid.footer(),
|
grid.footer(),
|
||||||
// Error: 3-33 cell would conflict with footer spanning the same position
|
// Error: 3-33 cell would conflict with footer also spanning row 7
|
||||||
// Hint: 3-33 try reducing the cell's rowspan or moving the footer
|
// Hint: 3-33 try reducing the cell's rowspan or moving the footer
|
||||||
grid.cell(y: 6, rowspan: 2)[d],
|
grid.cell(y: 6, rowspan: 2)[d],
|
||||||
)
|
)
|
||||||
@ -160,11 +160,20 @@
|
|||||||
columns: 2,
|
columns: 2,
|
||||||
grid.header(),
|
grid.header(),
|
||||||
grid.footer(grid.cell(y: 2)[a]),
|
grid.footer(grid.cell(y: 2)[a]),
|
||||||
// Error: 3-39 cell would conflict with footer spanning the same position
|
// Error: 3-39 cell would conflict with footer also spanning row 2
|
||||||
// Hint: 3-39 try reducing the cell's rowspan or moving the footer
|
// Hint: 3-39 try reducing the cell's rowspan or moving the footer
|
||||||
grid.cell(x: 1, y: 1, rowspan: 2)[a],
|
grid.cell(x: 1, y: 1, rowspan: 2)[a],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- grid-footer-rowbreak-line ---
|
||||||
|
#grid(
|
||||||
|
columns: 1,
|
||||||
|
[a],
|
||||||
|
grid.hline(stroke: red),
|
||||||
|
grid.footer([b]),
|
||||||
|
grid.hline(stroke: 3pt),
|
||||||
|
)
|
||||||
|
|
||||||
--- grid-footer-multiple ---
|
--- grid-footer-multiple ---
|
||||||
// Error: 4:3-4:19 cannot have more than one footer
|
// Error: 4:3-4:19 cannot have more than one footer
|
||||||
#grid(
|
#grid(
|
||||||
@ -570,3 +579,11 @@
|
|||||||
table.cell(x: 1)[D],
|
table.cell(x: 1)[D],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- issue-6666-auto-hlines-around-footer ---
|
||||||
|
#table(
|
||||||
|
columns: 2,
|
||||||
|
table.hline(stroke: 2pt + blue),
|
||||||
|
table.footer([*foo*], [*bar*]),
|
||||||
|
table.hline(stroke: 8pt),
|
||||||
|
)
|
||||||
|
@ -137,6 +137,19 @@
|
|||||||
[a],
|
[a],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- grid-header-multiple-unordered ---
|
||||||
|
#set page(height: 4em)
|
||||||
|
#grid(
|
||||||
|
grid.header(grid.cell(x: 0, y: 4)[y]),
|
||||||
|
grid.header([x]),
|
||||||
|
[a],
|
||||||
|
[b],
|
||||||
|
[c],
|
||||||
|
[d],
|
||||||
|
[e],
|
||||||
|
[f],
|
||||||
|
)
|
||||||
|
|
||||||
--- grid-header-skip ---
|
--- grid-header-skip ---
|
||||||
#grid(
|
#grid(
|
||||||
columns: 2,
|
columns: 2,
|
||||||
@ -148,6 +161,63 @@
|
|||||||
[f], grid.cell(x: 1)[g]
|
[f], grid.cell(x: 1)[g]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- grid-header-skip-unordered ---
|
||||||
|
#grid(
|
||||||
|
columns: 2,
|
||||||
|
[a],
|
||||||
|
grid.header(grid.cell(x: 0, y: 2)[y]),
|
||||||
|
[b],
|
||||||
|
grid.header([x]),
|
||||||
|
[c]
|
||||||
|
)
|
||||||
|
|
||||||
|
--- grid-header-rowbreak-auto-pos ---
|
||||||
|
#grid(
|
||||||
|
columns: 2,
|
||||||
|
[x],
|
||||||
|
grid.hline(stroke: red),
|
||||||
|
grid.header([a]),
|
||||||
|
grid.hline(stroke: 3pt),
|
||||||
|
[y],
|
||||||
|
grid.header(),
|
||||||
|
[z],
|
||||||
|
)
|
||||||
|
|
||||||
|
--- grid-header-rowbreak-fixed-pos ---
|
||||||
|
#grid(
|
||||||
|
columns: 2,
|
||||||
|
[z],
|
||||||
|
grid.hline(stroke: red),
|
||||||
|
grid.header(grid.cell(x: 0)[b]),
|
||||||
|
grid.hline(stroke: 3pt),
|
||||||
|
[w],
|
||||||
|
[j],
|
||||||
|
grid.header(grid.cell(x: 0, y: 9)[c]),
|
||||||
|
[k]
|
||||||
|
)
|
||||||
|
|
||||||
|
--- grid-header-rowbreak-mixed-pos ---
|
||||||
|
#grid(
|
||||||
|
columns: 2,
|
||||||
|
[a],
|
||||||
|
grid.header([x], grid.cell(x: 0)[b]),
|
||||||
|
[c],
|
||||||
|
grid.hline(stroke: red),
|
||||||
|
grid.header([y], grid.cell(x: 0, y: 8)[d]),
|
||||||
|
grid.hline(stroke: 3pt),
|
||||||
|
[e]
|
||||||
|
)
|
||||||
|
|
||||||
|
--- grid-header-rowbreak-auto-and-fixed-pos ---
|
||||||
|
#grid(
|
||||||
|
columns: 2,
|
||||||
|
[a],
|
||||||
|
grid.header([x]),
|
||||||
|
[b],
|
||||||
|
grid.header(grid.cell(x: 0, y: 3)[y]),
|
||||||
|
[c]
|
||||||
|
)
|
||||||
|
|
||||||
--- grid-header-too-large-non-repeating-orphan ---
|
--- grid-header-too-large-non-repeating-orphan ---
|
||||||
#set page(height: 8em)
|
#set page(height: 8em)
|
||||||
#grid(
|
#grid(
|
||||||
@ -398,7 +468,7 @@
|
|||||||
[a], [b],
|
[a], [b],
|
||||||
[c],
|
[c],
|
||||||
),
|
),
|
||||||
// Error: 3-48 cell would conflict with header spanning the same position
|
// Error: 3-48 cell would conflict with header also spanning row 1
|
||||||
// Hint: 3-48 try moving the cell or the header
|
// Hint: 3-48 try moving the cell or the header
|
||||||
table.cell(x: 1, y: 1, rowspan: 2, lorem(80))
|
table.cell(x: 1, y: 1, rowspan: 2, lorem(80))
|
||||||
)
|
)
|
||||||
@ -411,7 +481,7 @@
|
|||||||
[a], [b],
|
[a], [b],
|
||||||
[c],
|
[c],
|
||||||
),
|
),
|
||||||
// Error: 3-42 cell would conflict with header spanning the same position
|
// Error: 3-42 cell would conflict with header also spanning row 1
|
||||||
// Hint: 3-42 try moving the cell or the header
|
// Hint: 3-42 try moving the cell or the header
|
||||||
table.cell(y: 1, rowspan: 2, lorem(80))
|
table.cell(y: 1, rowspan: 2, lorem(80))
|
||||||
)
|
)
|
||||||
@ -616,6 +686,45 @@
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- grid-header-collision-multiple-ordered ---
|
||||||
|
#grid(
|
||||||
|
columns: 2,
|
||||||
|
grid.cell(x: 0, y: 0)[a],
|
||||||
|
grid.cell(x: 1, y: 0)[a],
|
||||||
|
grid.cell(x: 0, y: 3)[a],
|
||||||
|
grid.header(grid.cell(x: 0, y: 2)[y]),
|
||||||
|
// Error: 15-39 cell would cause header to expand to non-empty row 3
|
||||||
|
// Hint: 15-39 try moving its cells to available rows
|
||||||
|
grid.header(grid.cell(x: 0, y: 3)[y]),
|
||||||
|
grid.header(grid.cell(x: 0, y: 4)[y]),
|
||||||
|
)
|
||||||
|
|
||||||
|
--- grid-header-collision-multiple-unordered ---
|
||||||
|
#grid(
|
||||||
|
columns: 2,
|
||||||
|
grid.cell(x: 0, y: 0)[a],
|
||||||
|
grid.cell(x: 1, y: 0)[a],
|
||||||
|
grid.header(grid.cell(x: 0, y: 2)[y]),
|
||||||
|
grid.header(grid.cell(x: 0, y: 3)[y]),
|
||||||
|
grid.header(grid.cell(x: 0, y: 4)[y]),
|
||||||
|
// Error: 3-27 cell would conflict with header also spanning row 3
|
||||||
|
// Hint: 3-27 try moving the cell or the header
|
||||||
|
grid.cell(x: 0, y: 3)[a]
|
||||||
|
)
|
||||||
|
|
||||||
|
--- grid-header-collision-multiple-rowspan ---
|
||||||
|
#grid(
|
||||||
|
columns: 2,
|
||||||
|
grid.cell(x: 0, y: 0)[a],
|
||||||
|
grid.cell(x: 1, y: 0)[a],
|
||||||
|
grid.header(grid.cell(x: 0, y: 2)[y]),
|
||||||
|
grid.header(grid.cell(x: 0, y: 3)[y]),
|
||||||
|
grid.header(grid.cell(x: 0, y: 4)[y]),
|
||||||
|
// Error: 3-39 cell would conflict with header also spanning row 2
|
||||||
|
// Hint: 3-39 try moving the cell or the header
|
||||||
|
grid.cell(x: 0, y: 1, rowspan: 2)[a]
|
||||||
|
)
|
||||||
|
|
||||||
--- issue-5359-column-override-stays-inside-header ---
|
--- issue-5359-column-override-stays-inside-header ---
|
||||||
#table(
|
#table(
|
||||||
columns: 3,
|
columns: 3,
|
||||||
@ -625,3 +734,15 @@
|
|||||||
table.cell(x: 1)[D],
|
table.cell(x: 1)[D],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- issue-6666-auto-hlines-around-header ---
|
||||||
|
#table(
|
||||||
|
columns: 2,
|
||||||
|
table.hline(stroke: 2pt + blue),
|
||||||
|
table.header([*foo*], [*bar*]),
|
||||||
|
table.hline(stroke: 1.5pt + red),
|
||||||
|
table.cell(colspan: 2)[_asdf_],
|
||||||
|
table.hline(stroke: 1.5pt + red),
|
||||||
|
[a], [b],
|
||||||
|
[c], [d],
|
||||||
|
)
|
||||||
|