From be0f8fe6d70bc5919e4351b73a2835e89001b000 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Mon, 10 Jul 2023 11:00:12 +0200 Subject: [PATCH] Customizable math classes (#1681) --- crates/typst-library/src/math/class.rs | 36 ++++++++++++++++++++++ crates/typst-library/src/math/ctx.rs | 4 ++- crates/typst-library/src/math/fragment.rs | 35 +++++++++++++++++---- crates/typst-library/src/math/mod.rs | 4 +++ crates/typst-library/src/math/style.rs | 7 +++++ crates/typst/src/eval/cast.rs | 32 +++++++++++++++++++ tests/ref/math/class.png | Bin 0 -> 5308 bytes tests/typ/math/class.typ | 33 ++++++++++++++++++++ 8 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 crates/typst-library/src/math/class.rs create mode 100644 tests/ref/math/class.png create mode 100644 tests/typ/math/class.typ diff --git a/crates/typst-library/src/math/class.rs b/crates/typst-library/src/math/class.rs new file mode 100644 index 000000000..0d6a370b7 --- /dev/null +++ b/crates/typst-library/src/math/class.rs @@ -0,0 +1,36 @@ +use super::*; + +/// Forced use of a certain math class. +/// +/// This is useful to treat certain symbols as if they were of a different +/// class, e.g. to make text behave like a binary operator. +/// +/// # Example +/// ```example +/// $x class("relation", "<=") 5$ +/// ``` +/// +/// Display: Class +/// Category: math +#[element(LayoutMath)] +pub struct ClassElem { + /// The class to apply to the content. + #[required] + pub class: MathClass, + + /// The content to which the class is applied. + #[required] + pub body: Content, +} + +impl LayoutMath for ClassElem { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + ctx.style(ctx.style.with_class(self.class())); + let mut fragment = ctx.layout_fragment(&self.body())?; + ctx.unstyle(); + + fragment.set_class(self.class()); + ctx.push(fragment); + Ok(()) + } +} diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs index a1684ffa7..999a5ccb9 100644 --- a/crates/typst-library/src/math/ctx.rs +++ b/crates/typst-library/src/math/ctx.rs @@ -101,6 +101,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { style: MathStyle { variant: MathVariant::Serif, size: if block { MathSize::Display } else { MathSize::Text }, + class: Smart::Auto, cramped: false, bold: variant.weight >= FontWeight::BOLD, italic: match variant.style { @@ -167,7 +168,8 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { // A single letter that is available in the math font. match self.style.size { MathSize::Display => { - if glyph.class == Some(MathClass::Large) { + let class = self.style.class.as_custom().or(glyph.class); + if class == Some(MathClass::Large) { let height = scaled!(self, display_operator_min_height); glyph.stretch_vertical(self, height, Abs::zero()).into() } else { diff --git a/crates/typst-library/src/math/fragment.rs b/crates/typst-library/src/math/fragment.rs index a1702aefc..1a90eaadd 100644 --- a/crates/typst-library/src/math/fragment.rs +++ b/crates/typst-library/src/math/fragment.rs @@ -61,12 +61,12 @@ impl MathFragment { } pub fn class(&self) -> Option { - match self { + self.style().and_then(|style| style.class.as_custom()).or(match self { Self::Glyph(glyph) => glyph.class, Self::Variant(variant) => variant.class, Self::Frame(fragment) => Some(fragment.class), _ => None, - } + }) } pub fn style(&self) -> Option { @@ -88,10 +88,27 @@ impl MathFragment { } pub fn set_class(&mut self, class: MathClass) { + macro_rules! set_style_class { + ($fragment:ident) => { + if $fragment.style.class.is_custom() { + $fragment.style.class = Smart::Custom(class); + } + }; + } + match self { - Self::Glyph(glyph) => glyph.class = Some(class), - Self::Variant(variant) => variant.class = Some(class), - Self::Frame(fragment) => fragment.class = class, + Self::Glyph(glyph) => { + glyph.class = Some(class); + set_style_class!(glyph); + } + Self::Variant(variant) => { + variant.class = Some(class); + set_style_class!(variant); + } + Self::Frame(fragment) => { + fragment.class = class; + set_style_class!(fragment); + } _ => {} } } @@ -107,7 +124,13 @@ impl MathFragment { pub fn is_spaced(&self) -> bool { match self { - MathFragment::Frame(frame) => frame.spaced, + MathFragment::Frame(frame) => { + match self.style().and_then(|style| style.class.as_custom()) { + Some(MathClass::Fence) => true, + Some(_) => false, + None => frame.spaced, + } + } _ => self.class() == Some(MathClass::Fence), } } diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index f4199fd8b..b5410a03a 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -6,6 +6,7 @@ mod accent; mod align; mod attach; mod cancel; +mod class; mod delimited; mod frac; mod fragment; @@ -22,6 +23,7 @@ pub use self::accent::*; pub use self::align::*; pub use self::attach::*; pub use self::cancel::*; +pub use self::class::*; pub use self::delimited::*; pub use self::frac::*; pub use self::matrix::*; @@ -106,6 +108,8 @@ pub fn module() -> Module { math.define("script", script_func()); math.define("sscript", sscript_func()); + math.define("class", ClassElem::func()); + // Text operators. math.define("op", OpElem::func()); op::define(&mut math); diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs index 235770dbd..35a4cfdbd 100644 --- a/crates/typst-library/src/math/style.rs +++ b/crates/typst-library/src/math/style.rs @@ -320,6 +320,8 @@ pub struct MathStyle { pub variant: MathVariant, /// The size of the glyphs. pub size: MathSize, + /// The class of the element. + pub class: Smart, /// Affects the height of exponents. pub cramped: bool, /// Whether to use bold glyphs. @@ -339,6 +341,11 @@ impl MathStyle { Self { size, ..self } } + // This style, with the given `class`. + pub fn with_class(self, class: MathClass) -> Self { + Self { class: Smart::Custom(class), ..self } + } + /// This style, with `cramped` set to the given value. pub fn with_cramped(self, cramped: bool) -> Self { Self { cramped, ..self } diff --git a/crates/typst/src/eval/cast.rs b/crates/typst/src/eval/cast.rs index 917972ed9..6a49d1282 100644 --- a/crates/typst/src/eval/cast.rs +++ b/crates/typst/src/eval/cast.rs @@ -1,4 +1,5 @@ pub use typst_macros::{cast, Cast}; +use unicode_math_class::MathClass; use std::fmt::Write; use std::ops::Add; @@ -314,3 +315,34 @@ impl FromValue for Never { Err(Self::error(&value)) } } + +cast! { + MathClass, + self => IntoValue::into_value(match self { + MathClass::Normal => "normal", + MathClass::Alphabetic => "alphabetic", + MathClass::Binary => "binary", + MathClass::Closing => "closing", + MathClass::Diacritic => "diacritic", + MathClass::Fence => "fence", + MathClass::GlyphPart => "glyph-part", + MathClass::Large => "large", + MathClass::Opening => "opening", + MathClass::Punctuation => "punctuation", + MathClass::Relation => "relation", + MathClass::Space => "space", + MathClass::Unary => "unary", + MathClass::Vary => "vary", + MathClass::Special => "special", + }), + "normal" => MathClass::Normal, + "binary" => MathClass::Binary, + "closing" => MathClass::Closing, + "fence" => MathClass::Fence, + "large" => MathClass::Large, + "opening" => MathClass::Opening, + "punctuation" => MathClass::Punctuation, + "relation" => MathClass::Relation, + "unary" => MathClass::Unary, + "vary" => MathClass::Vary, +} diff --git a/tests/ref/math/class.png b/tests/ref/math/class.png new file mode 100644 index 0000000000000000000000000000000000000000..dffb6f7a0ecc59ce084797653c8a86a0018b4925 GIT binary patch literal 5308 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 literal 0 HcmV?d00001 diff --git a/tests/typ/math/class.typ b/tests/typ/math/class.typ new file mode 100644 index 000000000..188e7d90a --- /dev/null +++ b/tests/typ/math/class.typ @@ -0,0 +1,33 @@ +// Test math classes. + +--- +// Test characters. +$ a class("normal", +) b \ + a class("binary", .) b \ + lr(class("opening", \/) a/b class("closing", \\)) \ + { x class("fence", \;) x > 0} \ + a class("large", \/) b \ + a class("punctuation", :) b \ + a class("relation", ~) b \ + a + class("unary", times) b \ + class("vary", :) a class("vary", :) b $ + +--- +// Test custom content. +#let dotsq = square( + size: 0.7em, + stroke: 0.5pt, + align(center+horizon, circle(radius: 0.15em, fill: black)) +) + +$ a dotsq b \ + a class("normal", dotsq) b \ + a class("vary", dotsq) b \ + a + class("vary", dotsq) b \ + a class("punctuation", dotsq) b $ + +--- +// Test nested. +#let normal = math.class.with("normal") +#let pluseq = $class("binary", normal(+) normal(=))$ +$ a pluseq 5 $