diff --git a/crates/typst/src/math/matrix.rs b/crates/typst/src/math/matrix.rs index ca62846a2..138a494bc 100644 --- a/crates/typst/src/math/matrix.rs +++ b/crates/typst/src/math/matrix.rs @@ -1,9 +1,10 @@ use smallvec::{smallvec, SmallVec}; +use unicode_math_class::MathClass; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::foundations::{ - cast, dict, elem, Array, Cast, Content, Dict, Fold, Packed, Resolve, Smart, - StyleChain, Value, + array, cast, dict, elem, Array, Content, Dict, Fold, NoneValue, Packed, Resolve, + Smart, StyleChain, Value, }; use crate::layout::{ Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Length, Point, Ratio, Rel, Size, @@ -13,6 +14,7 @@ use crate::math::{ FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator, MathContext, Scaled, DELIM_SHORT_FALL, }; +use crate::symbols::Symbol; use crate::syntax::{Span, Spanned}; use crate::text::TextElem; use crate::utils::Numeric; @@ -40,8 +42,8 @@ pub struct VecElem { /// #set math.vec(delim: "[") /// $ vec(1, 2) $ /// ``` - #[default(Some(Delimiter::Paren))] - pub delim: Option, + #[default(DelimiterPair::PAREN)] + pub delim: DelimiterPair, /// The gap between elements. /// @@ -71,14 +73,7 @@ impl LayoutMath for Packed { LeftRightAlternator::Right, )?; - layout_delimiters( - ctx, - styles, - frame, - delim.map(Delimiter::open), - delim.map(Delimiter::close), - self.span(), - ) + layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), self.span()) } } @@ -109,8 +104,8 @@ pub struct MatElem { /// #set math.mat(delim: "[") /// $ mat(1, 2; 3, 4) $ /// ``` - #[default(Some(Delimiter::Paren))] - pub delim: Option, + #[default(DelimiterPair::PAREN)] + pub delim: DelimiterPair, /// Draws augmentation lines in a matrix. /// @@ -257,14 +252,7 @@ impl LayoutMath for Packed { self.span(), )?; - layout_delimiters( - ctx, - styles, - frame, - delim.map(Delimiter::open), - delim.map(Delimiter::close), - self.span(), - ) + layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), self.span()) } } @@ -289,8 +277,8 @@ pub struct CasesElem { /// #set math.cases(delim: "[") /// $ x = cases(1, 2) $ /// ``` - #[default(Delimiter::Brace)] - pub delim: Delimiter, + #[default(DelimiterPair::BRACE)] + pub delim: DelimiterPair, /// Whether the direction of cases should be reversed. /// @@ -330,56 +318,108 @@ impl LayoutMath for Packed { )?; let (open, close) = if self.reverse(styles) { - (None, Some(delim.close())) + (None, delim.close()) } else { - (Some(delim.open()), None) + (delim.open(), None) }; layout_delimiters(ctx, styles, frame, open, close, self.span()) } } -/// A vector / matrix delimiter. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Delimiter { - /// Delimit with parentheses. - #[string("(")] - Paren, - /// Delimit with brackets. - #[string("[")] - Bracket, - /// Delimit with curly braces. - #[string("{")] - Brace, - /// Delimit with vertical bars. - #[string("|")] - Bar, - /// Delimit with double vertical bars. - #[string("||")] - DoubleBar, +/// A delimiter is a single character that is used to delimit a matrix, vector +/// or cases. The character has to be a Unicode codepoint tagged as a math +/// "opening", "closing" or "fence". +/// +/// Typically, the delimiter is stretched to fit the height of whatever it +/// delimits. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +struct Delimiter(Option); + +cast! { + Delimiter, + self => self.0.into_value(), + _: NoneValue => Self::none(), + v: Symbol => Self::char(v.get())?, + v: char => Self::char(v)?, } impl Delimiter { - /// The delimiter's opening character. - fn open(self) -> char { - match self { - Self::Paren => '(', - Self::Bracket => '[', - Self::Brace => '{', - Self::Bar => '|', - Self::DoubleBar => '‖', + fn none() -> Self { + Self(None) + } + + fn char(c: char) -> StrResult { + if !matches!( + unicode_math_class::class(c), + Some(MathClass::Opening | MathClass::Closing | MathClass::Fence), + ) { + bail!("invalid delimiter: \"{}\"", c) } + Ok(Self(Some(c))) + } + + fn get(self) -> Option { + self.0 + } + + fn find_matching(self) -> Self { + match self.0 { + None => Self::none(), + Some('[') => Self(Some(']')), + Some(']') => Self(Some('[')), + Some('{') => Self(Some('}')), + Some('}') => Self(Some('{')), + Some(c) => match unicode_math_class::class(c) { + Some(MathClass::Opening) => Self(char::from_u32(c as u32 + 1)), + Some(MathClass::Closing) => Self(char::from_u32(c as u32 - 1)), + _ => Self(Some(c)), + }, + } + } +} + +/// A pair of delimiters (one closing, one opening) used for matrices, vectors +/// and cases. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct DelimiterPair { + open: Delimiter, + close: Delimiter, +} + +cast! { + DelimiterPair, + + self => array![self.open, self.close].into_value(), + + v: Array => match v.as_slice() { + [open, close] => Self { + open: open.clone().cast()?, + close: close.clone().cast()?, + }, + _ => bail!("expected 2 delimiters, found {}", v.len()) + }, + v: Delimiter => Self { open: v, close: v.find_matching() } +} + +impl DelimiterPair { + const PAREN: Self = Self { + open: Delimiter(Some('(')), + close: Delimiter(Some(')')), + }; + const BRACE: Self = Self { + open: Delimiter(Some('{')), + close: Delimiter(Some('}')), + }; + + /// The delimiter's opening character. + fn open(self) -> Option { + self.open.get() } /// The delimiter's closing character. - fn close(self) -> char { - match self { - Self::Paren => ')', - Self::Bracket => ']', - Self::Brace => '}', - Self::Bar => '|', - Self::DoubleBar => '‖', - } + fn close(self) -> Option { + self.close.get() } } diff --git a/tests/ref/math-cases-delim.png b/tests/ref/math-cases-delim.png new file mode 100644 index 000000000..e54e277c0 Binary files /dev/null and b/tests/ref/math-cases-delim.png differ diff --git a/tests/ref/math-mat-delims-inverted.png b/tests/ref/math-mat-delims-inverted.png new file mode 100644 index 000000000..06f1cdb34 Binary files /dev/null and b/tests/ref/math-mat-delims-inverted.png differ diff --git a/tests/ref/math-mat-delims-pair.png b/tests/ref/math-mat-delims-pair.png new file mode 100644 index 000000000..954e6d824 Binary files /dev/null and b/tests/ref/math-mat-delims-pair.png differ diff --git a/tests/ref/math-mat-delims.png b/tests/ref/math-mat-delims.png new file mode 100644 index 000000000..6ba589c84 Binary files /dev/null and b/tests/ref/math-mat-delims.png differ diff --git a/tests/suite/math/cases.typ b/tests/suite/math/cases.typ index e6c4956dc..2cf48e6f7 100644 --- a/tests/suite/math/cases.typ +++ b/tests/suite/math/cases.typ @@ -11,3 +11,7 @@ $ f(x, y) := cases( --- math-cases-gap --- #set math.cases(gap: 1em) $ x = cases(1, 2) $ + +--- math-cases-delim --- +#set math.cases(delim: sym.angle.l) +$ cases(a, b, c) $ diff --git a/tests/suite/math/mat.typ b/tests/suite/math/mat.typ index e6148a348..85f918eea 100644 --- a/tests/suite/math/mat.typ +++ b/tests/suite/math/mat.typ @@ -161,3 +161,54 @@ $ mat(#1, #(foo: "bar")) $ ) $mat(augment: #1, M, v) arrow.r.squiggly mat(augment: #1, R, b)$ + +--- math-mat-delims --- +$ mat(delim: #none, 1, 2; 3, 4) $ + +$ mat(delim: "(", 1, 2; 3, 4) $ +$ mat(delim: \(, 1, 2; 3, 4) $ +$ mat(delim: paren.l, 1, 2; 3, 4) $ + +$ mat(delim: "[", 1, 2; 3, 4) $ +$ mat(delim: \[, 1, 2; 3, 4) $ +$ mat(delim: bracket.l, 1, 2; 3, 4) $ + +$ mat(delim: "⟦", 1, 2; 3, 4) $ +$ mat(delim: bracket.double.l, 1, 2; 3, 4) $ + +$ mat(delim: "{", 1, 2; 3, 4) $ +$ mat(delim: \{, 1, 2; 3, 4) $ +$ mat(delim: brace.l, 1, 2; 3, 4) $ + +$ mat(delim: "|", 1, 2; 3, 4) $ +$ mat(delim: \|, 1, 2; 3, 4) $ +$ mat(delim: bar.v, 1, 2; 3, 4) $ + +$ mat(delim: "‖", 1, 2; 3, 4) $ +$ mat(delim: bar.v.double, 1, 2; 3, 4) $ + +$ mat(delim: "⟨", 1, 2; 3, 4) $ +$ mat(delim: angle.l, 1, 2; 3, 4) $ + +--- math-mat-delims-inverted --- +$ mat(delim: ")", 1, 2; 3, 4) $ +$ mat(delim: \), 1, 2; 3, 4) $ +$ mat(delim: paren.r, 1, 2; 3, 4) $ + +$ mat(delim: "]", 1, 2; 3, 4) $ +$ mat(delim: \], 1, 2; 3, 4) $ +$ mat(delim: bracket.r, 1, 2; 3, 4) $ + +$ mat(delim: "⟧", 1, 2; 3, 4) $ +$ mat(delim: bracket.double.r, 1, 2; 3, 4) $ + +$ mat(delim: "}", 1, 2; 3, 4) $ +$ mat(delim: \}, 1, 2; 3, 4) $ +$ mat(delim: brace.r, 1, 2; 3, 4) $ + +$ mat(delim: "⟩", 1, 2; 3, 4) $ +$ mat(delim: angle.r, 1, 2; 3, 4) $ + +--- math-mat-delims-pair --- +$ mat(delim: #(none, "["), 1, 2; 3, 4) $ +$ mat(delim: #(sym.angle.r, sym.bracket.double.r), 1, 2; 3, 4) $ diff --git a/tests/suite/math/vec.typ b/tests/suite/math/vec.typ index 312c0ee45..d7bc0b6c0 100644 --- a/tests/suite/math/vec.typ +++ b/tests/suite/math/vec.typ @@ -22,6 +22,26 @@ $ v = vec(1, 2+3, 4) $ #set math.vec(delim: "[") $ vec(1, 2) $ ---- math-vec-delim-invalid --- -// Error: 22-25 expected "(", "[", "{", "|", "||", or none +--- math-vec-delim-empty-string --- +// Error: 22-24 expected exactly one character +#set math.vec(delim: "") + +--- math-vec-delim-not-single-char --- +// Error: 22-39 expected exactly one character +#set math.vec(delim: "not a delimiter") + +--- math-vec-delim-invalid-char --- +// Error: 22-25 invalid delimiter: "%" #set math.vec(delim: "%") + +--- math-vec-delim-invalid-symbol --- +// Error: 22-33 invalid delimiter: "%" +#set math.vec(delim: sym.percent) + +--- math-vec-delim-invalid-opening --- +// Error: 22-33 invalid delimiter: "%" +#set math.vec(delim: ("%", none)) + +--- math-vec-delim-invalid-closing --- +// Error: 22-33 invalid delimiter: "%" +#set math.vec(delim: (none, "%"))