mirror of
https://github.com/typst/typst
synced 2025-08-13 22:57:56 +08:00
initial header and footer resolve changes
This commit is contained in:
parent
d6b0d68ffa
commit
fc56715ef6
@ -733,6 +733,13 @@ impl<'a> CellGrid<'a> {
|
|||||||
// Therefore, we use a counter, 'auto_index', to determine the position
|
// Therefore, we use a counter, 'auto_index', to determine the position
|
||||||
// of the next cell with (x: auto, y: auto). It is only stepped when
|
// of the next cell with (x: auto, y: auto). It is only stepped when
|
||||||
// a cell with (x: auto, y: auto), usually the vast majority, is found.
|
// a cell with (x: auto, y: auto), usually the vast majority, is found.
|
||||||
|
//
|
||||||
|
// Note that a separate counter ('local_auto_index') is used within
|
||||||
|
// headers and footers, as explained above its definition. Outside of
|
||||||
|
// those (when the table child being processed is a single cell),
|
||||||
|
// 'local_auto_index' will simply be an alias for 'auto_index', which
|
||||||
|
// will be updated after that cell is placed, if it is an
|
||||||
|
// automatically-positioned cell.
|
||||||
let mut auto_index: usize = 0;
|
let mut auto_index: usize = 0;
|
||||||
|
|
||||||
// We have to rebuild the grid to account for arbitrary positions.
|
// We have to rebuild the grid to account for arbitrary positions.
|
||||||
@ -755,12 +762,36 @@ impl<'a> CellGrid<'a> {
|
|||||||
for child in children {
|
for child in children {
|
||||||
let mut is_header = false;
|
let mut is_header = false;
|
||||||
let mut is_footer = false;
|
let mut is_footer = false;
|
||||||
|
|
||||||
|
// This is true for any header or footer.
|
||||||
|
let mut is_row_group = false;
|
||||||
|
|
||||||
|
// The normal auto index should only be stepped (upon placing an
|
||||||
|
// automatically-positioned cell, to indicate the position of the
|
||||||
|
// next) outside of headers or footers, in which case the auto
|
||||||
|
// index will be updated with the local auto index. Inside headers
|
||||||
|
// and footers, however, cells can only start after the first empty
|
||||||
|
// row (as determined by 'first_available_row' below), meaning that
|
||||||
|
// the next automatically-positioned cell will be in a different
|
||||||
|
// position than it would usually be if it would be in a non-empty
|
||||||
|
// row, so we must step a local index inside headers and footers
|
||||||
|
// instead, and use a separate counter outside them.
|
||||||
|
let mut local_auto_index = auto_index;
|
||||||
|
|
||||||
|
// The index of the first cell within this child in the grid. Note
|
||||||
|
// that cells outside headers and footers are children with a
|
||||||
|
// single cell inside, in which case 'child_start == child_end'.
|
||||||
let mut child_start = usize::MAX;
|
let mut child_start = usize::MAX;
|
||||||
let mut child_end = 0;
|
let mut child_end = 0;
|
||||||
let mut child_span = Span::detached();
|
let mut child_span = Span::detached();
|
||||||
let mut start_new_row = false;
|
|
||||||
let mut first_index_of_top_hlines = usize::MAX;
|
// The first row in which this table child can fit.
|
||||||
let mut first_index_of_non_top_hlines = usize::MAX;
|
//
|
||||||
|
// Within headers and footers, this will correspond to the first
|
||||||
|
// fully empty row available in the grid. This is because headers
|
||||||
|
// and footers always occupy entire rows, so they cannot occupy
|
||||||
|
// a non-empty row.
|
||||||
|
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, span, items, .. } => {
|
||||||
@ -769,20 +800,23 @@ impl<'a> CellGrid<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is_header = true;
|
is_header = true;
|
||||||
|
is_row_group = true;
|
||||||
child_span = span;
|
child_span = span;
|
||||||
repeat_header = repeat;
|
repeat_header = repeat;
|
||||||
|
|
||||||
// If any cell in the header is automatically positioned,
|
first_available_row =
|
||||||
// have it skip to the next row. This is to avoid having a
|
find_next_empty_row(&resolved_cells, local_auto_index, c);
|
||||||
// header after a partially filled row just add cells to
|
|
||||||
// that row instead of starting a new one.
|
|
||||||
// FIXME: Revise this approach when headers can start from
|
|
||||||
// arbitrary rows.
|
|
||||||
start_new_row = true;
|
|
||||||
|
|
||||||
// Any hlines at the top of the header will start at this
|
// If any cell in the header is automatically positioned,
|
||||||
// index.
|
// have it skip to the next empty row. This is to avoid
|
||||||
first_index_of_top_hlines = pending_hlines.len();
|
// having a header after a partially filled row just add
|
||||||
|
// cells to that row instead of starting a new one.
|
||||||
|
//
|
||||||
|
// Note that the first fully empty row is always after the
|
||||||
|
// latest auto-position cell, since each auto-position cell
|
||||||
|
// always occupies the first available position after the
|
||||||
|
// previous one. Therefore, this will be >= auto_index.
|
||||||
|
local_auto_index = first_available_row * c;
|
||||||
|
|
||||||
(Some(items), None)
|
(Some(items), None)
|
||||||
}
|
}
|
||||||
@ -792,18 +826,23 @@ impl<'a> CellGrid<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is_footer = true;
|
is_footer = true;
|
||||||
|
is_row_group = true;
|
||||||
child_span = span;
|
child_span = span;
|
||||||
repeat_footer = repeat;
|
repeat_footer = repeat;
|
||||||
|
|
||||||
// If any cell in the footer is automatically positioned,
|
first_available_row =
|
||||||
// have it skip to the next row. This is to avoid having a
|
find_next_empty_row(&resolved_cells, local_auto_index, c);
|
||||||
// footer after a partially filled row just add cells to
|
|
||||||
// that row instead of starting a new one.
|
|
||||||
start_new_row = true;
|
|
||||||
|
|
||||||
// Any hlines at the top of the footer will start at this
|
// If any cell in the footer is automatically positioned,
|
||||||
// index.
|
// have it skip to the next empty row. This is to avoid
|
||||||
first_index_of_top_hlines = pending_hlines.len();
|
// having a footer after a partially filled row just add
|
||||||
|
// cells to that row instead of starting a new one.
|
||||||
|
//
|
||||||
|
// Note that the first fully empty row is always after the
|
||||||
|
// latest auto-position cell, since each auto-position cell
|
||||||
|
// always occupies the first available position after the
|
||||||
|
// previous one. Therefore, this will be >= auto_index.
|
||||||
|
local_auto_index = first_available_row * c;
|
||||||
|
|
||||||
(Some(items), None)
|
(Some(items), None)
|
||||||
}
|
}
|
||||||
@ -832,7 +871,7 @@ impl<'a> CellGrid<'a> {
|
|||||||
// gutter.
|
// gutter.
|
||||||
skip_auto_index_through_fully_merged_rows(
|
skip_auto_index_through_fully_merged_rows(
|
||||||
&resolved_cells,
|
&resolved_cells,
|
||||||
&mut auto_index,
|
&mut local_auto_index,
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -856,7 +895,7 @@ impl<'a> CellGrid<'a> {
|
|||||||
// child. Effectively, this means that a hline at
|
// child. Effectively, this means that a hline at
|
||||||
// the start of a header will always appear above
|
// the start of a header will always appear above
|
||||||
// that header's first row. Similarly for footers.
|
// that header's first row. Similarly for footers.
|
||||||
auto_index
|
local_auto_index
|
||||||
.checked_sub(1)
|
.checked_sub(1)
|
||||||
.map_or(0, |last_auto_index| last_auto_index / c + 1)
|
.map_or(0, |last_auto_index| last_auto_index / c + 1)
|
||||||
});
|
});
|
||||||
@ -904,15 +943,17 @@ impl<'a> CellGrid<'a> {
|
|||||||
// to the left of the table.
|
// to the left of the table.
|
||||||
//
|
//
|
||||||
// Exceptionally, a vline is also placed to the
|
// Exceptionally, a vline is also placed to the
|
||||||
// left of the table if we should start a new row
|
// left of the table when specified at the start
|
||||||
// for the next automatically positioned cell.
|
// of a row group, such as a header or footer, that
|
||||||
|
// is, when no automatically-positioned cells have
|
||||||
|
// been specified for that group yet.
|
||||||
// For example, this means that a vline at
|
// For example, this means that a vline at
|
||||||
// the beginning of a header will be placed to its
|
// the beginning of a header will be placed to its
|
||||||
// left rather than after the previous
|
// left rather than after the previous
|
||||||
// automatically positioned cell. Same for footers.
|
// automatically positioned cell. Same for footers.
|
||||||
auto_index
|
local_auto_index
|
||||||
.checked_sub(1)
|
.checked_sub(1)
|
||||||
.filter(|_| !start_new_row)
|
.filter(|_| local_auto_index > first_available_row * c)
|
||||||
.map_or(0, |last_auto_index| last_auto_index % c + 1)
|
.map_or(0, |last_auto_index| last_auto_index % c + 1)
|
||||||
});
|
});
|
||||||
if end.is_some_and(|end| end.get() < start) {
|
if end.is_some_and(|end| end.get() < start) {
|
||||||
@ -941,9 +982,11 @@ impl<'a> CellGrid<'a> {
|
|||||||
cell_y,
|
cell_y,
|
||||||
colspan,
|
colspan,
|
||||||
rowspan,
|
rowspan,
|
||||||
|
header.as_ref(),
|
||||||
|
footer.as_ref(),
|
||||||
&resolved_cells,
|
&resolved_cells,
|
||||||
&mut auto_index,
|
&mut local_auto_index,
|
||||||
&mut start_new_row,
|
first_available_row,
|
||||||
c,
|
c,
|
||||||
)
|
)
|
||||||
.at(cell_span)?
|
.at(cell_span)?
|
||||||
@ -1061,29 +1104,14 @@ impl<'a> CellGrid<'a> {
|
|||||||
// contained within it.
|
// contained within it.
|
||||||
child_start = child_start.min(y);
|
child_start = child_start.min(y);
|
||||||
child_end = child_end.max(y + rowspan);
|
child_end = child_end.max(y + rowspan);
|
||||||
|
|
||||||
if start_new_row && child_start <= auto_index.div_ceil(c) {
|
|
||||||
// No need to start a new row as we already include
|
|
||||||
// the row of the next automatically positioned cell in
|
|
||||||
// the header or footer.
|
|
||||||
start_new_row = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !start_new_row {
|
|
||||||
// From now on, upcoming hlines won't be at the top of
|
|
||||||
// the child, as the first automatically positioned
|
|
||||||
// cell was placed.
|
|
||||||
first_index_of_non_top_hlines =
|
|
||||||
first_index_of_non_top_hlines.min(pending_hlines.len());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_header || is_footer) && child_start == usize::MAX {
|
if (is_header || is_footer) && child_start == usize::MAX {
|
||||||
// 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.
|
// at the next empty row after the latest auto index.
|
||||||
auto_index = find_next_empty_row(&resolved_cells, auto_index, c);
|
local_auto_index = first_available_row * c;
|
||||||
child_start = auto_index.div_ceil(c);
|
child_start = first_available_row;
|
||||||
child_end = child_start + 1;
|
child_end = child_start + 1;
|
||||||
|
|
||||||
if resolved_cells.len() <= c * child_start {
|
if resolved_cells.len() <= c * child_start {
|
||||||
@ -1129,22 +1157,6 @@ impl<'a> CellGrid<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if is_header || is_footer {
|
if is_header || is_footer {
|
||||||
let amount_hlines = pending_hlines.len();
|
|
||||||
for (_, top_hline, has_auto_y) in pending_hlines
|
|
||||||
.get_mut(
|
|
||||||
first_index_of_top_hlines
|
|
||||||
..first_index_of_non_top_hlines.min(amount_hlines),
|
|
||||||
)
|
|
||||||
.unwrap_or(&mut [])
|
|
||||||
{
|
|
||||||
if *has_auto_y {
|
|
||||||
// Move this hline to the top of the child, as it was
|
|
||||||
// placed before the first automatically positioned cell
|
|
||||||
// and had an automatic index.
|
|
||||||
top_hline.index = child_start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next automatically positioned cell goes under this header.
|
// Next automatically positioned cell goes under this header.
|
||||||
// FIXME: Consider only doing this if the header has any fully
|
// FIXME: Consider only doing this if the header has any fully
|
||||||
// automatically positioned cells. Otherwise,
|
// automatically positioned cells. Otherwise,
|
||||||
@ -1158,7 +1170,14 @@ impl<'a> CellGrid<'a> {
|
|||||||
// course.
|
// course.
|
||||||
// None of the above are concerns for now, as headers must
|
// None of the above are concerns for now, as headers must
|
||||||
// start at the first row.
|
// start at the first row.
|
||||||
auto_index = auto_index.max(c * child_end);
|
local_auto_index = local_auto_index.max(c * child_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_row_group {
|
||||||
|
// The child was a single cell outside headers or footers.
|
||||||
|
// Therefore, 'local_auto_index' for this table child was
|
||||||
|
// simply an alias for 'auto_index', so we update it as needed.
|
||||||
|
auto_index = local_auto_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1620,19 +1639,21 @@ impl<'a> CellGrid<'a> {
|
|||||||
/// `(auto, auto)` cell) and the amount of columns in the grid, returns the
|
/// `(auto, auto)` cell) and the amount of columns in the grid, returns the
|
||||||
/// final index of this cell in the vector of resolved cells.
|
/// final index of this cell in the vector of resolved cells.
|
||||||
///
|
///
|
||||||
/// The `start_new_row` parameter is used to ensure that, if this cell is
|
/// The `first_available_row` parameter is used by headers and footers to
|
||||||
/// fully automatically positioned, it should start a new, empty row. This is
|
/// indicate the first empty row available. Any rows before those should
|
||||||
/// useful for headers and footers, which must start at their own rows, without
|
/// not be picked by cells with `auto` row positioning, since headers and
|
||||||
/// interference from previous cells.
|
/// footers occupy entire rows, and may not conflict with cells outside them.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn resolve_cell_position(
|
fn resolve_cell_position(
|
||||||
cell_x: Smart<usize>,
|
cell_x: Smart<usize>,
|
||||||
cell_y: Smart<usize>,
|
cell_y: Smart<usize>,
|
||||||
colspan: usize,
|
colspan: usize,
|
||||||
rowspan: usize,
|
rowspan: usize,
|
||||||
|
header: Option<&Header>,
|
||||||
|
footer: Option<&(usize, Span, Footer)>,
|
||||||
resolved_cells: &[Option<Entry>],
|
resolved_cells: &[Option<Entry>],
|
||||||
auto_index: &mut usize,
|
auto_index: &mut usize,
|
||||||
start_new_row: &mut bool,
|
first_available_row: usize,
|
||||||
columns: usize,
|
columns: usize,
|
||||||
) -> HintedStrResult<usize> {
|
) -> HintedStrResult<usize> {
|
||||||
// Translates a (x, y) position to the equivalent index in the final cell vector.
|
// Translates a (x, y) position to the equivalent index in the final cell vector.
|
||||||
@ -1649,14 +1670,30 @@ fn resolve_cell_position(
|
|||||||
// Let's find the first available position starting from the
|
// Let's find the first available position starting from the
|
||||||
// automatic position counter, searching in row-major order.
|
// automatic position counter, searching in row-major order.
|
||||||
let mut resolved_index = *auto_index;
|
let mut resolved_index = *auto_index;
|
||||||
if *start_new_row {
|
if header.is_some() || footer.is_some() {
|
||||||
resolved_index =
|
// Need to skip existing headers and footers.
|
||||||
find_next_empty_row(resolved_cells, resolved_index, columns);
|
loop {
|
||||||
|
if matches!(resolved_cells.get(resolved_index), Some(Some(_))) {
|
||||||
// Next cell won't have to start a new row if we just did that,
|
resolved_index += 1;
|
||||||
// in principle.
|
} else if let Some(header) =
|
||||||
*start_new_row = false;
|
header.filter(|header| resolved_index / columns < header.end)
|
||||||
|
{
|
||||||
|
// Skip header
|
||||||
|
resolved_index = header.end * columns;
|
||||||
|
} else if let Some((footer_end, _, _)) =
|
||||||
|
footer.filter(|(end, _, footer)| {
|
||||||
|
resolved_index / columns >= footer.start
|
||||||
|
&& resolved_index / columns < *end
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// Skip footer
|
||||||
|
resolved_index = *footer_end * columns;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// No row groups to skip, so only skip non-empty cells.
|
||||||
while let Some(Some(_)) = resolved_cells.get(resolved_index) {
|
while let Some(Some(_)) = resolved_cells.get(resolved_index) {
|
||||||
// Skip any non-absent cell positions (`Some(None)`) to
|
// Skip any non-absent cell positions (`Some(None)`) to
|
||||||
// determine where this cell will be placed. An out of
|
// determine where this cell will be placed. An out of
|
||||||
@ -1696,7 +1733,11 @@ fn resolve_cell_position(
|
|||||||
} else {
|
} else {
|
||||||
// Cell has only chosen its column.
|
// Cell has only chosen its column.
|
||||||
// Let's find the first row which has that column available.
|
// Let's find the first row which has that column available.
|
||||||
let mut resolved_y = 0;
|
// If in a header or footer, start searching by the first empty
|
||||||
|
// row / the header or footer's first row (specified through
|
||||||
|
// 'first_available_row'). Otherwise, start searching at the
|
||||||
|
// first row.
|
||||||
|
let mut resolved_y = first_available_row;
|
||||||
while let Some(Some(_)) =
|
while let Some(Some(_)) =
|
||||||
resolved_cells.get(cell_index(cell_x, resolved_y)?)
|
resolved_cells.get(cell_index(cell_x, resolved_y)?)
|
||||||
{
|
{
|
||||||
@ -1710,6 +1751,12 @@ fn resolve_cell_position(
|
|||||||
}
|
}
|
||||||
// Cell has only chosen its row, not its column.
|
// Cell has only chosen its row, not its column.
|
||||||
(Smart::Auto, Smart::Custom(cell_y)) => {
|
(Smart::Auto, Smart::Custom(cell_y)) => {
|
||||||
|
if cell_y < first_available_row {
|
||||||
|
bail!(
|
||||||
|
"cell in a header or footer cannot be placed in a row with cells outside that header or footer"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Let's find the first column which has that row available.
|
// Let's find the first column which has that row available.
|
||||||
let first_row_pos = cell_index(0, cell_y)?;
|
let first_row_pos = cell_index(0, cell_y)?;
|
||||||
let last_row_pos = first_row_pos
|
let last_row_pos = first_row_pos
|
||||||
@ -1736,14 +1783,15 @@ fn resolve_cell_position(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes the index of the first cell in the next empty row in the grid,
|
/// Computes the `y` of the next available empty row, given the auto index as
|
||||||
/// starting with the given initial index.
|
/// an initial index for search, since we know that there are no empty rows
|
||||||
|
/// before automatically-positioned cells, as they are placed sequentially.
|
||||||
fn find_next_empty_row(
|
fn find_next_empty_row(
|
||||||
resolved_cells: &[Option<Entry>],
|
resolved_cells: &[Option<Entry>],
|
||||||
initial_index: usize,
|
auto_index: usize,
|
||||||
columns: usize,
|
columns: usize,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let mut resolved_index = initial_index.next_multiple_of(columns);
|
let mut resolved_index = auto_index.next_multiple_of(columns);
|
||||||
while resolved_cells
|
while resolved_cells
|
||||||
.get(resolved_index..resolved_index + columns)
|
.get(resolved_index..resolved_index + columns)
|
||||||
.is_some_and(|row| row.iter().any(Option::is_some))
|
.is_some_and(|row| row.iter().any(Option::is_some))
|
||||||
@ -1752,7 +1800,7 @@ fn find_next_empty_row(
|
|||||||
resolved_index += columns;
|
resolved_index += columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved_index
|
resolved_index / columns
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fully merged rows under the cell of latest auto index indicate rowspans
|
/// Fully merged rows under the cell of latest auto index indicate rowspans
|
||||||
|
BIN
tests/ref/issue-5359-column-override-stays-inside-footer.png
Normal file
BIN
tests/ref/issue-5359-column-override-stays-inside-footer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 674 B |
@ -385,8 +385,8 @@
|
|||||||
#table(
|
#table(
|
||||||
columns: 3,
|
columns: 3,
|
||||||
inset: 1.5pt,
|
inset: 1.5pt,
|
||||||
table.cell(y: 0)[a],
|
|
||||||
table.footer(
|
table.footer(
|
||||||
|
table.cell(y: 0)[a],
|
||||||
table.hline(stroke: red),
|
table.hline(stroke: red),
|
||||||
table.hline(y: 1, stroke: aqua),
|
table.hline(y: 1, stroke: aqua),
|
||||||
table.cell(y: 0)[b],
|
table.cell(y: 0)[b],
|
||||||
@ -404,3 +404,29 @@
|
|||||||
table.cell(rowspan: 2)[a], table.cell(rowspan: 2)[b],
|
table.cell(rowspan: 2)[a], table.cell(rowspan: 2)[b],
|
||||||
table.footer()
|
table.footer()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- grid-footer-row-pos-cell-inside-conflicts-with-row-outside ---
|
||||||
|
#set page(margin: 2pt)
|
||||||
|
#set text(6pt)
|
||||||
|
#table(
|
||||||
|
columns: 3,
|
||||||
|
inset: 1.5pt,
|
||||||
|
table.cell(y: 0)[a],
|
||||||
|
table.footer(
|
||||||
|
table.hline(stroke: red),
|
||||||
|
table.hline(y: 1, stroke: aqua),
|
||||||
|
// Error: 5-24 cell in a header or footer cannot be placed in a row with cells outside that header or footer
|
||||||
|
table.cell(y: 0)[b],
|
||||||
|
[c]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
--- issue-5359-column-override-stays-inside-footer ---
|
||||||
|
#table(
|
||||||
|
columns: 3,
|
||||||
|
[Outside],
|
||||||
|
table.footer(
|
||||||
|
[A], table.cell(x: 1)[B], [C],
|
||||||
|
table.cell(x: 1)[D],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@ -368,3 +368,15 @@
|
|||||||
[b]
|
[b]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- issue-5359-column-override-stays-inside-header ---
|
||||||
|
#table(
|
||||||
|
columns: 3,
|
||||||
|
[Outside],
|
||||||
|
// Error: 1:3-4:4 header must start at the first row
|
||||||
|
// Hint: 1:3-4:4 remove any rows before the header
|
||||||
|
table.header(
|
||||||
|
[A], table.cell(x: 1)[B], [C],
|
||||||
|
table.cell(x: 1)[D],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user