From a6cf584ee9c19cb27bf79410c0d1fec9e5012a27 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Wed, 22 May 2024 21:36:53 +0200 Subject: [PATCH] Allow somewhat arbitrary characters as `mat`, `vec` and `cases` `delim` (#4211) --- crates/typst/src/math/matrix.rs | 160 +++++++++++++++---------- tests/ref/math-cases-delim.png | Bin 0 -> 308 bytes tests/ref/math-mat-delims-inverted.png | Bin 0 -> 1908 bytes tests/ref/math-mat-delims-pair.png | Bin 0 -> 521 bytes tests/ref/math-mat-delims.png | Bin 0 -> 2966 bytes tests/suite/math/cases.typ | 4 + tests/suite/math/mat.typ | 51 ++++++++ tests/suite/math/vec.typ | 24 +++- 8 files changed, 177 insertions(+), 62 deletions(-) create mode 100644 tests/ref/math-cases-delim.png create mode 100644 tests/ref/math-mat-delims-inverted.png create mode 100644 tests/ref/math-mat-delims-pair.png create mode 100644 tests/ref/math-mat-delims.png 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 0000000000000000000000000000000000000000..e54e277c073d737faed067059a42a2c42eaa4026 GIT binary patch literal 308 zcmV-40n7f0P)t~2!@v_QXfb699Z>&1J~Ll*!=;3M}8+5UMGzjJPL;6TcbK4 z7S=pQJ=n$t3f!o}t~0JWv3Xi4k1KyJb|~$n^=GE}KvXa2LxMII89@MQcLGNAdH8RU zDOVGiHx8W7;FGiqOs?(azSYc>Oo7|5jEMWsr2ia-JZK@yOiDo~&riO^W6+A3R79tFc9AfznH79tUdtN}?Pq6iO= z00wZ7C04cy1OjMCVyzgKvMK~5ETIS*_8oa>>l-`o{dLaF_s*SjKKDCwQyd*^ST5efP{y20f4lH-RbYokp^c6hV7kk8q$Ov8lF9)2iDbNXW-dq zP9461XDfDl>IJU zJlXeYMDY$4xw*n7uWP^w3=p9l-R{(skxo!Y^y;^bgQ zz_f{0R!{=D_4g5dn%N&M8Hm5Lk}efdGk%?V498tm1!eSW&m1 zA&8QCaaAFMQq{|JgqFt*Gj|P@CoC|cnk*H2bo5%nOw7Ex)K#s^&U~5YES8FP89vq~7Hw{%aGzZ(Rrt@r}UyVlX z(N-L*p--JfH@M(?yn+V_;&AfJCdkIFz&+@}#DrealLClvICibF(PQ@6Gud|`kkNVT z%@M7x{^y`dbLu_jjZ$0JSAegXG=?XYRq-#c>|d6nRU`f9&I@uD1MZ8>7S0O>U~YJq zHzgZy$gPHM1~g+A(4X_;r_<}&mA3us=~XU{yiI=qjD!&9Gx9wUcS<(-gzWsuDba!% zlQ;5kSgLw49bq2Ik6qQD_x{=k1asrqK;CP{ET!(Nzsb*Qu1V+qnTrcF9%zf3m~Lmy z)~=Ef*6@A7Zw|ihSBng|w!i!b&_~N;Y&!@)E2V)g31Ci^m0%$)w-N>)<=-hEGfW!~(}`AYNtB6pQ_23L zG|4tvG1U-8IgqY*t2?Pi(4CUJUn!!9qOy07)d7br-xX5o^>TquQmNp1pa6kp<$9w@ znMj?C`tx#kyM!HLb8k2o_sGomg(Kht@# zMgm3=SNKWv1@fZlQ!lmZ6^I7qsHi09{}EMjvaGWH89J^5b&-x1%w^5Xwb_OC$CU#Xi&>!t#FA~UrI&1I!-+AZI3qqn)n8B0Qfw~V@*FAnMx9d_JW(Nj1iBgi zBom}Dlg>(`gPLQ0psw@raioyj-gJqYGp*Bd4z?(Lu~%ez9ktrQo-0OeaWzCXZG;G= zcwCxa-Jz{w@EnX-G)6NL_QBRD$GV0}psXp-&U4n^!9S~l7JMrGVK@84NPwT}lc{3_ Q`|zFGSvj1p{J|&bKRKhBX#fBK literal 0 HcmV?d00001 diff --git a/tests/ref/math-mat-delims-pair.png b/tests/ref/math-mat-delims-pair.png new file mode 100644 index 0000000000000000000000000000000000000000..954e6d824322b755e0b614e3f13b6862e8a20a35 GIT binary patch literal 521 zcmV+k0`~ohP)sa~l4wyxPmC7Piz3Jtij)ZjQuctNC_-vui#CNt zxv9)vN|IoTMb79K%v=ext!KdQ-Q4e-&5sK+bI)W!rIp2D4s)2pYQhZ~GYKpnXjzIa zOwIu4AUd)1R>0EA4(fO5U?2gX+0kuL0|tU6K~>+4FNeF34(A! z1g9h5WBG=;9T z!qA50?YQ%uHLSD4BG8VxG{Z+`dGl3!mD-`l&^!faUGXuUtlLC<5K>$t00000 LNkvXXu0mjfU=Zxr literal 0 HcmV?d00001 diff --git a/tests/ref/math-mat-delims.png b/tests/ref/math-mat-delims.png new file mode 100644 index 0000000000000000000000000000000000000000..6ba589c84fbfc09e26f240bdb531f8799ce795b5 GIT binary patch literal 2966 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 LEKXMbhQ0eQ3dE85 literal 0 HcmV?d00001 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, "%"))