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 000000000..dffb6f7a0 Binary files /dev/null and b/tests/ref/math/class.png differ 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 $