Table line customization [More Flexible Tables Pt.4] (#3393)

This commit is contained in:
PgBiel 2024-02-20 12:41:43 -03:00 committed by GitHub
parent 96fba58cf2
commit 4873312233
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 2674 additions and 423 deletions

View File

@ -746,6 +746,34 @@ impl<T, const N: usize> Fold for SmallVec<[T; N]> {
}
}
/// A variant of fold for foldable optional (`Option<T>`) values where an inner
/// `None` value isn't respected (contrary to `Option`'s usual `Fold`
/// implementation, with which folding with an inner `None` always returns
/// `None`). Instead, when either of the `Option` objects is `None`, the other
/// one is necessarily returned by `fold_or`. Normal folding still occurs when
/// both values are `Some`, using `T`'s `Fold` implementation.
///
/// This is useful when `None` in a particular context means "unspecified"
/// rather than "absent", in which case a specified value (`Some`) is chosen
/// over an unspecified one (`None`), while two specified values are folded
/// together.
pub trait AlternativeFold {
/// Attempts to fold this inner value with an outer value. However, if
/// either value is `None`, returns the other one instead of folding.
fn fold_or(self, outer: Self) -> Self;
}
impl<T: Fold> AlternativeFold for Option<T> {
fn fold_or(self, outer: Self) -> Self {
match (self, outer) {
(Some(inner), Some(outer)) => Some(inner.fold(outer)),
// If one of values is `None`, return the other one instead of
// folding.
(inner, outer) => inner.or(outer),
}
}
}
/// A type that accumulates depth when folded.
#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
pub struct Depth(pub usize);

View File

@ -13,13 +13,14 @@ use crate::diag::StrResult;
use crate::eval::ops;
use crate::foundations::{
fields, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Dict,
Duration, FromValue, Func, IntoValue, Label, Module, NativeElement, NativeType,
NoneValue, Plugin, Reflect, Repr, Scope, Str, Styles, Type, Version,
Duration, Fold, FromValue, Func, IntoValue, Label, Module, NativeElement, NativeType,
NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, Styles, Type, Version,
};
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
use crate::symbols::Symbol;
use crate::syntax::{ast, Span};
use crate::text::{RawElem, TextElem};
use crate::util::ArcExt;
use crate::visualize::{Color, Gradient, Pattern};
/// A computational value.
@ -668,6 +669,53 @@ primitive! { Type: "type", Type }
primitive! { Module: "module", Module }
primitive! { Plugin: "plugin", Plugin }
impl<T: Reflect> Reflect for Arc<T> {
fn input() -> CastInfo {
T::input()
}
fn output() -> CastInfo {
T::output()
}
fn castable(value: &Value) -> bool {
T::castable(value)
}
fn error(found: &Value) -> EcoString {
T::error(found)
}
}
impl<T: Clone + IntoValue> IntoValue for Arc<T> {
fn into_value(self) -> Value {
Arc::take(self).into_value()
}
}
impl<T: FromValue> FromValue for Arc<T> {
fn from_value(value: Value) -> StrResult<Self> {
match value {
v if T::castable(&v) => Ok(Arc::new(T::from_value(v)?)),
_ => Err(Self::error(&value)),
}
}
}
impl<T: Clone + Resolve> Resolve for Arc<T> {
type Output = Arc<T::Output>;
fn resolve(self, styles: super::StyleChain) -> Self::Output {
Arc::new(Arc::take(self).resolve(styles))
}
}
impl<T: Clone + Fold> Fold for Arc<T> {
fn fold(self, outer: Self) -> Self {
Arc::new(Arc::take(self).fold(Arc::take(outer)))
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -4,7 +4,7 @@ use std::ops::{Add, Div, Mul, Neg, Rem};
use ecow::EcoString;
use crate::foundations::{cast, repr, Repr, Value};
use crate::foundations::{cast, repr, Fold, Repr, Value};
use crate::util::{Numeric, Scalar};
/// An absolute length.
@ -227,6 +227,12 @@ impl<'a> Sum<&'a Self> for Abs {
}
}
impl Fold for Abs {
fn fold(self, _: Self) -> Self {
self
}
}
cast! {
Abs,
self => Value::Length(self.into()),

View File

@ -321,6 +321,40 @@ cast! {
}
}
/// A horizontal alignment which only allows `left`/`right` and `start`/`end`,
/// thus excluding `center`.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub enum OuterHAlignment {
#[default]
Start,
Left,
Right,
End,
}
impl From<OuterHAlignment> for HAlignment {
fn from(value: OuterHAlignment) -> Self {
match value {
OuterHAlignment::Start => Self::Start,
OuterHAlignment::Left => Self::Left,
OuterHAlignment::Right => Self::Right,
OuterHAlignment::End => Self::End,
}
}
}
cast! {
OuterHAlignment,
self => HAlignment::from(self).into_value(),
align: Alignment => match align {
Alignment::H(HAlignment::Start) => Self::Start,
Alignment::H(HAlignment::Left) => Self::Left,
Alignment::H(HAlignment::Right) => Self::Right,
Alignment::H(HAlignment::End) => Self::End,
v => bail!("expected `start`, `left`, `right`, or `end`, found {}", v.repr()),
}
}
/// Where to align something vertically.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum VAlignment {
@ -383,6 +417,34 @@ cast! {
}
}
/// A vertical alignment which only allows `top` and `bottom`, thus excluding
/// `horizon`.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum OuterVAlignment {
#[default]
Top,
Bottom,
}
impl From<OuterVAlignment> for VAlignment {
fn from(value: OuterVAlignment) -> Self {
match value {
OuterVAlignment::Top => Self::Top,
OuterVAlignment::Bottom => Self::Bottom,
}
}
}
cast! {
OuterVAlignment,
self => VAlignment::from(self).into_value(),
align: Alignment => match align {
Alignment::V(VAlignment::Top) => Self::Top,
Alignment::V(VAlignment::Bottom) => Self::Bottom,
v => bail!("expected `top` or `bottom`, found {}", v.repr()),
}
}
/// A fixed alignment in the global coordinate space.
///
/// For horizontal alignment, start is globally left and for vertical alignment

View File

@ -2,7 +2,8 @@ use std::fmt::{self, Debug, Formatter};
use crate::diag::StrResult;
use crate::foundations::{
CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve, StyleChain, Value,
AlternativeFold, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve,
StyleChain, Value,
};
use crate::layout::Side;
use crate::util::Get;
@ -240,13 +241,10 @@ impl<T: Resolve> Resolve for Corners<T> {
impl<T: Fold> Fold for Corners<Option<T>> {
fn fold(self, outer: Self) -> Self {
self.zip(outer).map(|(inner, outer)| match (inner, outer) {
(Some(inner), Some(outer)) => Some(inner.fold(outer)),
// Usually, folding an inner `None` with an `outer` preferres the
// explicit `None`. However, here `None` means unspecified and thus
// we want `outer`.
(inner, outer) => inner.or(outer),
})
// Usually, folding an inner `None` with an `outer` preferres the
// explicit `None`. However, here `None` means unspecified and thus
// we want `outer`, so we use `fold_or` to opt into such behavior.
self.zip(outer).map(|(inner, outer)| inner.fold_or(outer))
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,11 @@
mod layout;
mod lines;
pub use self::layout::{Cell, CellGrid, Celled, GridLayouter, ResolvableCell};
pub use self::layout::{Cell, CellGrid, Celled, GridItem, GridLayouter, ResolvableCell};
pub use self::lines::LinePosition;
use std::num::NonZeroUsize;
use std::sync::Arc;
use ecow::eco_format;
use smallvec::{smallvec, SmallVec};
@ -13,10 +16,11 @@ use crate::foundations::{
cast, elem, scope, Array, Content, Fold, Packed, Show, Smart, StyleChain, Value,
};
use crate::layout::{
AlignElem, Alignment, Axes, Fragment, LayoutMultiple, Length, Regions, Rel, Sides,
Sizing,
Abs, AlignElem, Alignment, Axes, Dir, Fragment, LayoutMultiple, Length,
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, Sizing,
};
use crate::syntax::Span;
use crate::text::TextElem;
use crate::util::NonZeroExt;
use crate::visualize::{Paint, Stroke};
@ -235,12 +239,14 @@ pub struct GridElem {
/// Grids have no strokes by default, which can be changed by setting this
/// option to the desired stroke.
///
/// _Note:_ Richer stroke customization for individual cells is not yet
/// implemented, but will be in the future. In the meantime, you can use the
/// third-party [tablex library](https://github.com/PgBiel/typst-tablex/).
/// If it is necessary to place lines which can cross spacing between cells
/// produced by the `gutter` option, or to override the stroke between
/// multiple specific cells, consider specifying one or more of
/// [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline) alongside
/// your grid cells.
#[resolve]
#[fold]
pub stroke: Option<Stroke>,
pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
/// How much to pad the cells' content.
///
@ -266,17 +272,25 @@ pub struct GridElem {
#[fold]
pub inset: Sides<Option<Rel<Length>>>,
/// The contents of the grid cells.
/// The contents of the grid cells, plus any extra grid lines specified
/// with the [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline)
/// elements.
///
/// The cells are populated in row-major order.
#[variadic]
pub children: Vec<Packed<GridCell>>,
pub children: Vec<GridChild>,
}
#[scope]
impl GridElem {
#[elem]
type GridCell;
#[elem]
type GridHLine;
#[elem]
type GridVLine;
}
impl LayoutMultiple for Packed<GridElem> {
@ -294,26 +308,60 @@ impl LayoutMultiple for Packed<GridElem> {
let column_gutter = self.column_gutter(styles);
let row_gutter = self.row_gutter(styles);
let fill = self.fill(styles);
let stroke = self.stroke(styles).map(Stroke::unwrap_or_default);
let stroke = self.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the grid when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
let items = self.children().iter().map(|child| match child {
GridChild::HLine(hline) => GridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
GridChild::VLine(vline) => GridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => {
LinePosition::Before
}
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
GridChild::Cell(cell) => GridItem::Cell(cell.clone()),
});
let grid = CellGrid::resolve(
tracks,
gutter,
self.children(),
items,
fill,
align,
inset,
&stroke,
engine,
styles,
self.span(),
)
.trace(engine.world, tracepoint, self.span())?;
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
// Measure the columns and layout the grid row-by-row.
layouter.layout(engine)
@ -332,6 +380,151 @@ cast! {
values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
}
/// Any child of a grid element.
#[derive(Debug, PartialEq, Clone, Hash)]
pub enum GridChild {
HLine(Packed<GridHLine>),
VLine(Packed<GridVLine>),
Cell(Packed<GridCell>),
}
cast! {
GridChild,
self => match self {
Self::HLine(hline) => hline.into_value(),
Self::VLine(vline) => vline.into_value(),
Self::Cell(cell) => cell.into_value(),
},
v: Content => v.into(),
}
impl From<Content> for GridChild {
fn from(value: Content) -> Self {
value
.into_packed::<GridHLine>()
.map(GridChild::HLine)
.or_else(|value| value.into_packed::<GridVLine>().map(GridChild::VLine))
.or_else(|value| value.into_packed::<GridCell>().map(GridChild::Cell))
.unwrap_or_else(|value| {
let span = value.span();
GridChild::Cell(Packed::new(GridCell::new(value)).spanned(span))
})
}
}
/// A horizontal line in the grid.
///
/// Overrides any per-cell stroke, including stroke specified through the
/// grid's `stroke` field. Can cross spacing between cells created through
/// the grid's `column-gutter` option.
#[elem(name = "hline", title = "Grid Horizontal Line")]
pub struct GridHLine {
/// The row above which the horizontal line is placed (zero-indexed).
/// If the `position` field is set to `{bottom}`, the line is placed below
/// the row with the given index instead (see that field's docs for
/// details).
///
/// Specifying `{auto}` causes the line to be placed at the row below the
/// last automatically positioned cell (that is, cell without coordinate
/// overrides) before the line among the grid's children. If there is no
/// such cell before the line, it is placed at the top of the grid (row 0).
/// Note that specifying for this option exactly the total amount of rows
/// in the grid causes this horizontal line to override the bottom border
/// of the grid, while a value of 0 overrides the top border.
pub y: Smart<usize>,
/// The column at which the horizontal line starts (zero-indexed, inclusive).
pub start: usize,
/// The column before which the horizontal line ends (zero-indexed,
/// exclusive).
/// Therefore, the horizontal line will be drawn up to and across column
/// `end - 1`.
///
/// A value equal to `{none}` or to the amount of columns causes it to
/// extend all the way towards the end of the grid.
pub end: Option<NonZeroUsize>,
/// The line's stroke.
///
/// Specifying `{none}` interrupts previous hlines placed across this
/// line's range, but does not affect per-cell stroke or vlines.
#[resolve]
#[fold]
#[default(Some(Arc::new(Stroke::default())))]
pub stroke: Option<Arc<Stroke>>,
/// The position at which the line is placed, given its row (`y`) - either
/// `{top}` to draw above it or `{bottom}` to draw below it.
///
/// This setting is only relevant when row gutter is enabled (and
/// shouldn't be used otherwise - prefer just increasing the `y` field by
/// one instead), since then the position below a row becomes different
/// from the position above the next row due to the spacing between both.
#[default(OuterVAlignment::Top)]
pub position: OuterVAlignment,
}
/// A vertical line in the grid.
///
/// Overrides any per-cell stroke, including stroke specified through the
/// grid's `stroke` field. Can cross spacing between cells created through
/// the grid's `row-gutter` option.
#[elem(name = "vline", title = "Grid Vertical Line")]
pub struct GridVLine {
/// The column before which the horizontal line is placed (zero-indexed).
/// If the `position` field is set to `{end}`, the line is placed after the
/// column with the given index instead (see that field's docs for
/// details).
///
/// Specifying `{auto}` causes the line to be placed at the column after
/// the last automatically positioned cell (that is, cell without
/// coordinate overrides) before the line among the grid's children. If
/// there is no such cell before the line, it is placed before the grid's
/// first column (column 0).
/// Note that specifying for this option exactly the total amount of
/// columns in the grid causes this vertical line to override the end
/// border of the grid (right in LTR, left in RTL), while a value of 0
/// overrides the start border (left in LTR, right in RTL).
pub x: Smart<usize>,
/// The row at which the vertical line starts (zero-indexed, inclusive).
pub start: usize,
/// The row on top of which the vertical line ends (zero-indexed,
/// exclusive).
/// Therefore, the vertical line will be drawn up to and across row
/// `end - 1`.
///
/// A value equal to `{none}` or to the amount of rows causes it to extend
/// all the way towards the bottom of the grid.
pub end: Option<NonZeroUsize>,
/// The line's stroke.
///
/// Specifying `{none}` interrupts previous vlines placed across this
/// line's range, but does not affect per-cell stroke or hlines.
#[resolve]
#[fold]
#[default(Some(Arc::new(Stroke::default())))]
pub stroke: Option<Arc<Stroke>>,
/// The position at which the line is placed, given its column (`x`) -
/// either `{start}` to draw before it or `{end}` to draw after it.
///
/// The values `{left}` and `{right}` are also accepted, but discouraged as
/// they cause your grid to be inconsistent between left-to-right and
/// right-to-left documents.
///
/// This setting is only relevant when column gutter is enabled (and
/// shouldn't be used otherwise - prefer just increasing the `x` field by
/// one instead), since then the position after a column becomes different
/// from the position before the next column due to the spacing between
/// both.
#[default(OuterHAlignment::Start)]
pub position: OuterHAlignment,
}
/// A cell in the grid. Use this to either override grid properties for a
/// particular cell, or in show rules to apply certain styles to multiple cells
/// at once.
@ -383,7 +576,7 @@ cast! {
pub struct GridCell {
/// The cell's body.
#[required]
body: Content,
pub body: Content,
/// The cell's column (zero-indexed).
/// This field may be used in show rules to style a cell depending on its
@ -408,7 +601,7 @@ pub struct GridCell {
/// [1], grid.cell(x: 3)[4], [2],
/// )
/// ```
x: Smart<usize>,
pub x: Smart<usize>,
/// The cell's row (zero-indexed).
/// This field may be used in show rules to style a cell depending on its
@ -427,20 +620,25 @@ pub struct GridCell {
/// [A], grid.cell(y: 1)[B], grid.cell(y: 1)[C], grid.cell(y: 2)[D]
/// )
/// ```
y: Smart<usize>,
pub y: Smart<usize>,
/// The amount of columns spanned by this cell.
#[default(NonZeroUsize::ONE)]
colspan: NonZeroUsize,
pub colspan: NonZeroUsize,
/// The cell's fill override.
fill: Smart<Option<Paint>>,
pub fill: Smart<Option<Paint>>,
/// The cell's alignment override.
align: Smart<Alignment>,
pub align: Smart<Alignment>,
/// The cell's inset override.
inset: Smart<Sides<Option<Rel<Length>>>>,
pub inset: Smart<Sides<Option<Rel<Length>>>>,
/// The cell's stroke override.
#[resolve]
#[fold]
pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
}
cast! {
@ -462,11 +660,27 @@ impl ResolvableCell for Packed<GridCell> {
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
styles: StyleChain,
) -> Cell {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
@ -482,7 +696,26 @@ impl ResolvableCell for Packed<GridCell> {
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
Cell { body: self.pack(), fill, colspan }
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on grid cells.
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
Cell {
body: self.pack(),
fill,
colspan,
stroke,
stroke_overridden,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {

View File

@ -3,7 +3,8 @@ use std::ops::Add;
use crate::diag::{bail, StrResult};
use crate::foundations::{
cast, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve, StyleChain, Value,
cast, AlternativeFold, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve,
StyleChain, Value,
};
use crate::layout::{Abs, Alignment, Axes, Axis, Corner, Rel, Size};
use crate::util::Get;
@ -245,13 +246,10 @@ impl<T: Resolve> Resolve for Sides<T> {
impl<T: Fold> Fold for Sides<Option<T>> {
fn fold(self, outer: Self) -> Self {
self.zip(outer).map(|(inner, outer)| match (inner, outer) {
(Some(inner), Some(outer)) => Some(inner.fold(outer)),
// Usually, folding an inner `None` with an `outer` preferres the
// explicit `None`. However, here `None` means unspecified and thus
// we want `outer`.
(inner, outer) => inner.or(outer),
})
// Usually, folding an inner `None` with an `outer` preferres the
// explicit `None`. However, here `None` means unspecified and thus
// we want `outer`, so we use `fold_or` to opt into such behavior.
self.zip(outer).map(|(inner, outer)| inner.fold_or(outer))
}
}

View File

@ -29,7 +29,8 @@ use crate::foundations::{
};
use crate::introspection::{Introspector, Locatable, Location};
use crate::layout::{
BlockElem, Em, GridCell, GridElem, HElem, PadElem, Sizing, TrackSizings, VElem,
BlockElem, Em, GridCell, GridChild, GridElem, HElem, PadElem, Sizing, TrackSizings,
VElem,
};
use crate::model::{
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
@ -237,11 +238,13 @@ impl Show for Packed<BibliographyElem> {
if references.iter().any(|(prefix, _)| prefix.is_some()) {
let mut cells = vec![];
for (prefix, reference) in references {
cells.push(
cells.push(GridChild::Cell(
Packed::new(GridCell::new(prefix.clone().unwrap_or_default()))
.spanned(span),
);
cells.push(Packed::new(GridCell::new(reference.clone())).spanned(span));
));
cells.push(GridChild::Cell(
Packed::new(GridCell::new(reference.clone())).spanned(span),
));
}
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
@ -945,8 +948,8 @@ impl ElemRenderer<'_> {
if let Some(prefix) = suf_prefix {
const COLUMN_GUTTER: Em = Em::new(0.65);
content = GridElem::new(vec![
Packed::new(GridCell::new(prefix)).spanned(self.span),
Packed::new(GridCell::new(content)).spanned(self.span),
GridChild::Cell(Packed::new(GridCell::new(prefix)).spanned(self.span)),
GridChild::Cell(Packed::new(GridCell::new(content)).spanned(self.span)),
])
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))

View File

@ -271,7 +271,6 @@ impl LayoutMultiple for Packed<EnumElem> {
number = number.saturating_add(1);
}
let stroke = None;
let grid = CellGrid::new(
Axes::with_x(&[
Sizing::Rel(indent.into()),
@ -282,7 +281,7 @@ impl LayoutMultiple for Packed<EnumElem> {
Axes::with_y(&[gutter.into()]),
cells,
);
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
layouter.layout(engine)
}

View File

@ -168,7 +168,6 @@ impl LayoutMultiple for Packed<ListElem> {
));
}
let stroke = None;
let grid = CellGrid::new(
Axes::with_x(&[
Sizing::Rel(indent.into()),
@ -179,7 +178,7 @@ impl LayoutMultiple for Packed<ListElem> {
Axes::with_y(&[gutter.into()]),
cells,
);
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
layouter.layout(engine)
}

View File

@ -1,4 +1,5 @@
use std::num::NonZeroUsize;
use std::sync::Arc;
use ecow::eco_format;
@ -8,12 +9,13 @@ use crate::foundations::{
cast, elem, scope, Content, Fold, Packed, Show, Smart, StyleChain,
};
use crate::layout::{
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Fragment, GridLayouter,
LayoutMultiple, Length, Regions, Rel, ResolvableCell, Sides, TrackSizings,
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Dir, Fragment,
GridItem, GridLayouter, LayoutMultiple, Length, LinePosition, OuterHAlignment,
OuterVAlignment, Regions, Rel, ResolvableCell, Sides, TrackSizings,
};
use crate::model::Figurable;
use crate::syntax::Span;
use crate::text::{Lang, LocalName, Region};
use crate::text::{Lang, LocalName, Region, TextElem};
use crate::util::NonZeroExt;
use crate::visualize::{Paint, Stroke};
@ -166,13 +168,17 @@ pub struct TableElem {
///
/// Strokes can be disabled by setting this to `{none}`.
///
/// _Note:_ Richer stroke customization for individual cells is not yet
/// implemented, but will be in the future. In the meantime, you can use the
/// third-party [tablex library](https://github.com/PgBiel/typst-tablex/).
/// If it is necessary to place lines which can cross spacing between cells
/// produced by the `gutter` option, or to override the stroke between
/// multiple specific cells, consider specifying one or more of
/// [`table.hline`]($table.hline) and [`table.vline`]($table.vline) alongside
/// your table cells.
///
/// See the [grid documentation]($grid) for more information on stroke.
#[resolve]
#[fold]
#[default(Some(Stroke::default()))]
pub stroke: Option<Stroke>,
#[default(Celled::Value(Sides::splat(Some(Some(Arc::new(Stroke::default()))))))]
pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
/// How much to pad the cells' content.
///
@ -197,15 +203,23 @@ pub struct TableElem {
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
pub inset: Sides<Option<Rel<Length>>>,
/// The contents of the table cells.
/// The contents of the table cells, plus any extra table lines specified
/// with the [`table.hline`]($table.hline) and
/// [`table.vline`]($table.vline) elements.
#[variadic]
pub children: Vec<Packed<TableCell>>,
pub children: Vec<TableChild>,
}
#[scope]
impl TableElem {
#[elem]
type TableCell;
#[elem]
type TableHLine;
#[elem]
type TableVLine;
}
impl LayoutMultiple for Packed<TableElem> {
@ -223,26 +237,60 @@ impl LayoutMultiple for Packed<TableElem> {
let column_gutter = self.column_gutter(styles);
let row_gutter = self.row_gutter(styles);
let fill = self.fill(styles);
let stroke = self.stroke(styles).map(Stroke::unwrap_or_default);
let stroke = self.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the table when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
let items = self.children().iter().map(|child| match child {
TableChild::HLine(hline) => GridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
TableChild::VLine(vline) => GridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => {
LinePosition::Before
}
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
TableChild::Cell(cell) => GridItem::Cell(cell.clone()),
});
let grid = CellGrid::resolve(
tracks,
gutter,
self.children(),
items,
fill,
align,
inset,
&stroke,
engine,
styles,
self.span(),
)
.trace(engine.world, tracepoint, self.span())?;
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
layouter.layout(engine)
}
}
@ -286,6 +334,122 @@ impl LocalName for Packed<TableElem> {
impl Figurable for Packed<TableElem> {}
/// Any child of a table element.
#[derive(Debug, PartialEq, Clone, Hash)]
pub enum TableChild {
HLine(Packed<TableHLine>),
VLine(Packed<TableVLine>),
Cell(Packed<TableCell>),
}
cast! {
TableChild,
self => match self {
Self::HLine(hline) => hline.into_value(),
Self::VLine(vline) => vline.into_value(),
Self::Cell(cell) => cell.into_value(),
},
v: Content => v.into(),
}
impl From<Content> for TableChild {
fn from(value: Content) -> Self {
value
.into_packed::<TableHLine>()
.map(TableChild::HLine)
.or_else(|value| value.into_packed::<TableVLine>().map(TableChild::VLine))
.or_else(|value| value.into_packed::<TableCell>().map(TableChild::Cell))
.unwrap_or_else(|value| {
let span = value.span();
TableChild::Cell(Packed::new(TableCell::new(value)).spanned(span))
})
}
}
/// A horizontal line in the table. See the docs for
/// [`grid.hline`]($grid.hline) for more information regarding how to use this
/// element's fields.
///
/// Overrides any per-cell stroke, including stroke specified through the
/// table's `stroke` field. Can cross spacing between cells created through
/// the table's `column-gutter` option.
#[elem(name = "hline", title = "Table Horizontal Line")]
pub struct TableHLine {
/// The row above which the horizontal line is placed (zero-indexed).
/// Functions identically to the `y` field in [`grid.hline`]($grid.hline).
pub y: Smart<usize>,
/// The column at which the horizontal line starts (zero-indexed, inclusive).
pub start: usize,
/// The column before which the horizontal line ends (zero-indexed,
/// exclusive).
pub end: Option<NonZeroUsize>,
/// The line's stroke.
///
/// Specifying `{none}` interrupts previous hlines placed across this
/// line's range, but does not affect per-cell stroke or vlines.
#[resolve]
#[fold]
#[default(Some(Arc::new(Stroke::default())))]
pub stroke: Option<Arc<Stroke>>,
/// The position at which the line is placed, given its row (`y`) - either
/// `{top}` to draw above it or `{bottom}` to draw below it.
///
/// This setting is only relevant when row gutter is enabled (and
/// shouldn't be used otherwise - prefer just increasing the `y` field by
/// one instead), since then the position below a row becomes different
/// from the position above the next row due to the spacing between both.
#[default(OuterVAlignment::Top)]
pub position: OuterVAlignment,
}
/// A vertical line in the table. See the docs for [`grid.vline`]($grid.vline)
/// for more information regarding how to use this element's fields.
///
/// Overrides any per-cell stroke, including stroke specified through the
/// table's `stroke` field. Can cross spacing between cells created through
/// the table's `row-gutter` option.
#[elem(name = "vline", title = "Table Vertical Line")]
pub struct TableVLine {
/// The column before which the horizontal line is placed (zero-indexed).
/// Functions identically to the `x` field in [`grid.vline`]($grid.vline).
pub x: Smart<usize>,
/// The row at which the vertical line starts (zero-indexed, inclusive).
pub start: usize,
/// The row on top of which the vertical line ends (zero-indexed,
/// exclusive).
pub end: Option<NonZeroUsize>,
/// The line's stroke.
///
/// Specifying `{none}` interrupts previous vlines placed across this
/// line's range, but does not affect per-cell stroke or hlines.
#[resolve]
#[fold]
#[default(Some(Arc::new(Stroke::default())))]
pub stroke: Option<Arc<Stroke>>,
/// The position at which the line is placed, given its column (`x`) -
/// either `{start}` to draw before it or `{end}` to draw after it.
///
/// The values `{left}` and `{right}` are also accepted, but discouraged as
/// they cause your table to be inconsistent between left-to-right and
/// right-to-left documents.
///
/// This setting is only relevant when column gutter is enabled (and
/// shouldn't be used otherwise - prefer just increasing the `x` field by
/// one instead), since then the position after a column becomes different
/// from the position before the next column due to the spacing between
/// both.
#[default(OuterHAlignment::Start)]
pub position: OuterHAlignment,
}
/// A cell in the table. Use this to either override table properties for a
/// particular cell, or in show rules to apply certain styles to multiple cells
/// at once.
@ -336,28 +500,33 @@ impl Figurable for Packed<TableElem> {}
pub struct TableCell {
/// The cell's body.
#[required]
body: Content,
pub body: Content,
/// The cell's column (zero-indexed).
/// Functions identically to the `x` field in [`grid.cell`]($grid.cell).
x: Smart<usize>,
pub x: Smart<usize>,
/// The cell's row (zero-indexed).
/// Functions identically to the `y` field in [`grid.cell`]($grid.cell).
y: Smart<usize>,
pub y: Smart<usize>,
/// The cell's fill override.
fill: Smart<Option<Paint>>,
pub fill: Smart<Option<Paint>>,
/// The amount of columns spanned by this cell.
#[default(NonZeroUsize::ONE)]
colspan: NonZeroUsize,
pub colspan: NonZeroUsize,
/// The cell's alignment override.
align: Smart<Alignment>,
pub align: Smart<Alignment>,
/// The cell's inset override.
inset: Smart<Sides<Option<Rel<Length>>>>,
pub inset: Smart<Sides<Option<Rel<Length>>>>,
/// The cell's stroke override.
#[resolve]
#[fold]
pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
}
cast! {
@ -379,11 +548,27 @@ impl ResolvableCell for Packed<TableCell> {
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
styles: StyleChain,
) -> Cell {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
@ -399,7 +584,26 @@ impl ResolvableCell for Packed<TableCell> {
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
Cell { body: self.pack(), fill, colspan }
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on table cells.
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
Cell {
body: self.pack(),
fill,
colspan,
stroke,
stroke_overridden,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -88,3 +88,52 @@
[e], [g], grid.cell(colspan: 2)[eee\ e\ e\ e],
grid.cell(colspan: 4)[eeee e e e]
)
---
// Test left and right for vlines in RTL
#set text(dir: rtl)
#grid(
columns: 3,
inset: 5pt,
grid.vline(stroke: red, position: left), grid.vline(stroke: green, position: right), [a],
grid.vline(stroke: red, position: left), grid.vline(stroke: 2pt, position: right), [b],
grid.vline(stroke: red, position: left), grid.vline(stroke: 2pt, position: right), [c],
grid.vline(stroke: aqua, position: right)
)
#grid(
columns: 3,
inset: 5pt,
gutter: 3pt,
grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [b],
grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [c],
grid.vline(stroke: 2pt, position: right)
)
#grid(
columns: 3,
inset: 5pt,
grid.vline(stroke: green, position: start), grid.vline(stroke: red, position: end), [a],
grid.vline(stroke: 2pt, position: start), grid.vline(stroke: red, position: end), [b],
grid.vline(stroke: 2pt, position: start), grid.vline(stroke: red, position: end), [c],
grid.vline(stroke: 2pt, position: start)
)
#grid(
columns: 3,
inset: 5pt,
gutter: 3pt,
grid.vline(stroke: green, position: start), grid.vline(stroke: red, position: end), [a],
grid.vline(stroke: blue, position: start), grid.vline(stroke: red, position: end), [b],
grid.vline(stroke: blue, position: start), grid.vline(stroke: red, position: end), [c],
grid.vline(stroke: 2pt, position: start)
)
---
// Error: 3:8-3:34 cannot place vertical line at the 'end' position of the end border (x = 1)
// Hint: 3:8-3:34 set the line's position to 'start' or place it at a smaller 'x' index
#set text(dir: rtl)
#grid(
[a], grid.vline(position: left)
)

View File

@ -0,0 +1,379 @@
#let double-line = pattern(size: (1.5pt, 1.5pt), {
place(line(stroke: .6pt, start: (0%, 50%), end: (100%, 50%)))
})
#table(
stroke: (_, y) => if y != 1 { (bottom: black) },
columns: 3,
table.cell(colspan: 3, align: center)[*Epic Table*],
align(center)[*Name*], align(center)[*Age*], align(center)[*Data*],
table.hline(stroke: (paint: double-line, thickness: 2pt)),
[John], [30], [None],
[Martha], [20], [A],
[Joseph], [35], [D]
)
---
// Test folding
#set grid(stroke: red)
#set grid(stroke: 5pt)
#grid(
inset: 10pt,
columns: 2,
stroke: stroke(dash: "loosely-dotted"),
grid.vline(start: 2, end: 3, stroke: (paint: green, dash: none)),
[a], [b],
grid.hline(end: 1, stroke: blue),
[c], [d],
[e], grid.cell(stroke: aqua)[f]
)
---
// Test set rules on cells and folding
#set table.cell(stroke: 4pt)
#set table.cell(stroke: blue)
#set table.hline(stroke: red)
#set table.hline(stroke: 0.75pt)
#set table.vline(stroke: 0.75pt)
#set table.vline(stroke: aqua)
#table(
columns: 3,
gutter: 3pt,
inset: 5pt,
[a], [b], table.vline(position: end), [c],
[d], [e], [f],
table.hline(position: bottom),
[g], [h], [i],
)
---
// Test stroke field on cell show rules
#set grid.cell(stroke: (x: 4pt))
#set grid.cell(stroke: (x: blue))
#show grid.cell: it => {
test(it.stroke, (left: stroke(paint: blue, thickness: 4pt, dash: "loosely-dotted"), right: blue + 4pt, top: stroke(thickness: 1pt), bottom: none))
it
}
#grid(
stroke: (left: (dash: "loosely-dotted")),
inset: 5pt,
grid.hline(stroke: red),
grid.cell(stroke: (top: 1pt))[a], grid.vline(stroke: yellow),
)
---
#table(
columns: 3,
[a], table.cell(colspan: 2)[b c],
table.cell(stroke: blue)[d], [e], [f],
[g], [h], table.cell(stroke: (left: yellow, top: green, right: aqua, bottom: red))[i],
[j], [k], [l],
table.cell(stroke: 3pt)[m], [n], table.cell(stroke: (dash: "loosely-dotted"))[o],
)
---
// Test per-column stroke array
#let t = table(
columns: 3,
stroke: (red, blue, green),
[a], [b], [c],
[d], [e], [f],
[h], [i], [j],
)
#t
#set text(dir: rtl)
#t
---
#grid(
columns: 3,
inset: 3pt,
stroke: (x, _) => (right: (5pt, (dash: "dotted")).at(calc.rem(x, 2)), bottom: (dash: "densely-dotted")),
grid.vline(x: 0, stroke: red),
grid.vline(x: 1, stroke: red),
grid.vline(x: 2, stroke: red),
grid.vline(x: 3, stroke: red),
grid.hline(y: 0, end: 1, stroke: blue),
grid.hline(y: 1, end: 1, stroke: blue),
grid.cell[a],
[b], [c]
)
---
#set page(height: 5em)
#table(
columns: 3,
inset: 3pt,
table.hline(y: 0, end: none, stroke: 3pt + blue),
table.vline(x: 0, end: none, stroke: 3pt + green),
table.hline(y: 5, end: none, stroke: 3pt + red),
table.vline(x: 3, end: none, stroke: 3pt + yellow),
[a], [b], [c],
[a], [b], [c],
[a], [b], [c],
[a], [b], [c],
[a], [b], [c],
)
---
// Automatically positioned lines
// Plus stroke thickness ordering
#table(
columns: 3,
table.hline(stroke: red + 5pt),
table.vline(stroke: blue + 5pt),
table.vline(stroke: 2pt),
[a],
table.vline(x: 1, stroke: aqua + 5pt),
[b],
table.vline(stroke: aqua + 5pt),
[c],
table.vline(stroke: yellow + 5.2pt),
table.hline(stroke: green + 5pt),
[a], [b], [c],
[a], table.hline(stroke: green + 2pt), table.vline(stroke: 2pt), [b], [c],
)
---
// Line specification order priority
// The last line should be blue, not red.
// The middle line should have disappeared.
#grid(
columns: 2,
inset: 2pt,
grid.hline(y: 2, stroke: red + 5pt),
grid.vline(),
[a], [b],
grid.hline(stroke: red),
grid.hline(stroke: none),
[c], grid.cell(stroke: (top: aqua))[d],
grid.hline(stroke: blue),
)
---
// Position: bottom and position: end with gutter should have a visible effect
// of moving the lines after the next track.
#table(
columns: 3,
gutter: 3pt,
stroke: blue,
table.hline(end: 2, stroke: red),
table.hline(end: 2, stroke: aqua, position: bottom),
table.vline(end: 2, stroke: green), [a], table.vline(end: 2, stroke: green), table.vline(end: 2, position: end, stroke: orange), [b], table.vline(end: 2, stroke: aqua, position: end), table.vline(end: 2, stroke: green), [c], table.vline(end: 2, stroke: green),
[d], [e], [f],
table.hline(end: 2, stroke: red),
[g], [h], [ie],
table.hline(end: 2, stroke: green),
)
---
// Using position: bottom and position: end without gutter should be the same
// as placing a line after the next track.
#table(
columns: 3,
stroke: blue,
table.hline(end: 2, stroke: red),
table.hline(end: 2, stroke: aqua, position: bottom),
table.vline(end: 2, stroke: green), [a], table.vline(end: 2, stroke: green), [b], table.vline(end: 2, stroke: aqua, position: end), table.vline(end: 2, stroke: green), [c], table.vline(end: 2, stroke: green),
table.hline(end: 2, stroke: 5pt),
[d], [e], [f],
table.hline(end: 2, stroke: red),
[g], [h], [i],
table.hline(end: 2, stroke: red),
)
---
// Test left and right for grid vlines.
#grid(
columns: 3,
inset: 5pt,
grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
grid.vline(stroke: 2pt, position: left), grid.vline(stroke: red, position: right), [b],
grid.vline(stroke: 2pt, position: left), grid.vline(stroke: red, position: right), [c],
grid.vline(stroke: 2pt, position: left)
)
#grid(
columns: 3,
inset: 5pt,
gutter: 3pt,
grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [b],
grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [c],
grid.vline(stroke: 2pt, position: left)
)
---
// Test left and right for table vlines.
#table(
columns: 3,
inset: 5pt,
table.vline(stroke: green, position: left), table.vline(stroke: red, position: right), [a],
table.vline(stroke: 2pt, position: left), table.vline(stroke: red, position: right), [b],
table.vline(stroke: 2pt, position: left), table.vline(stroke: red, position: right), [c],
table.vline(stroke: 2pt, position: left)
)
#table(
columns: 3,
inset: 5pt,
gutter: 3pt,
table.vline(stroke: green, position: left), table.vline(stroke: red, position: right), [a],
table.vline(stroke: blue, position: left), table.vline(stroke: red, position: right), [b],
table.vline(stroke: blue, position: left), table.vline(stroke: red, position: right), [c],
table.vline(stroke: 2pt, position: left)
)
---
// Hlines and vlines should always appear on top of cell strokes.
#table(
columns: 3,
stroke: aqua,
table.vline(stroke: red, position: end), [a], table.vline(stroke: red), [b], [c],
table.cell(stroke: blue)[d], [e], [f],
table.hline(stroke: red),
[g], table.cell(stroke: blue)[h], [i],
)
#table(
columns: 3,
gutter: 3pt,
stroke: aqua,
table.vline(stroke: red, position: end), [a], table.vline(stroke: red), [b], [c],
table.cell(stroke: blue)[d], [e], [f],
table.hline(stroke: red),
[g], table.cell(stroke: blue)[h], [i],
)
---
// Ensure cell stroke overrides always appear on top.
#table(
columns: 2,
stroke: black,
table.cell(stroke: red)[a], [b],
[c], [d],
)
#table(
columns: 2,
table.cell(stroke: red)[a], [b],
[c], [d],
)
---
// Error: 7:3-7:32 cannot place horizontal line at the 'bottom' position of the bottom border (y = 2)
// Hint: 7:3-7:32 set the line's position to 'top' or place it at a smaller 'y' index
#table(
columns: 2,
[a], [b],
[c], [d],
table.hline(stroke: aqua),
table.hline(position: top),
table.hline(position: bottom)
)
---
// Error: 8:3-8:32 cannot place horizontal line at the 'bottom' position of the bottom border (y = 2)
// Hint: 8:3-8:32 set the line's position to 'top' or place it at a smaller 'y' index
#table(
columns: 2,
gutter: 3pt,
[a], [b],
[c], [d], table.vline(stroke: red),
table.hline(stroke: aqua),
table.hline(position: top),
table.hline(position: bottom)
)
---
// Error: 6:3-6:28 cannot place vertical line at the 'end' position of the end border (x = 2)
// Hint: 6:3-6:28 set the line's position to 'start' or place it at a smaller 'x' index
#grid(
columns: 2,
[a], [b],
grid.vline(stroke: aqua),
grid.vline(position: start),
grid.vline(position: end)
)
---
// Error: 7:3-7:28 cannot place vertical line at the 'end' position of the end border (x = 2)
// Hint: 7:3-7:28 set the line's position to 'start' or place it at a smaller 'x' index
#grid(
columns: 2,
gutter: 3pt,
[a], [b],
grid.vline(stroke: aqua),
grid.vline(position: start),
grid.vline(position: end)
)
---
// Error: 4:3-4:19 cannot place horizontal line at invalid row 3
#grid(
[a],
[b],
grid.hline(y: 3)
)
---
// Error: 5:3-5:19 cannot place horizontal line at invalid row 3
#grid(
gutter: 3pt,
[a],
[b],
grid.hline(y: 3)
)
---
// Error: 4:3-4:20 cannot place vertical line at invalid column 3
#table(
columns: 2,
[a], [b],
table.vline(x: 3)
)
---
// Error: 5:3-5:20 cannot place vertical line at invalid column 3
#table(
columns: 2,
gutter: 3pt,
[a], [b],
table.vline(x: 3)
)
---
// Error: 3:3-3:31 line cannot end before it starts
#grid(
columns: 3,
grid.hline(start: 2, end: 1),
[a], [b], [c],
)
---
// Error: 3:3-3:32 line cannot end before it starts
#table(
columns: 3,
table.vline(start: 2, end: 1),
[a], [b], [c],
[d], [e], [f],
[g], [h], [i],
)
---
// Error: 24-31 expected `top` or `bottom`, found horizon
#table.hline(position: horizon)
---
// Error: 24-30 expected `start`, `left`, `right`, or `end`, found center
#table.vline(position: center)
---
// Error: 24-29 expected `top` or `bottom`, found right
#table.hline(position: right)
---
// Error: 24-27 expected `start`, `left`, `right`, or `end`, found top
#table.vline(position: top)