use std::num::NonZeroUsize; use typst_utils::NonZeroExt; use unicode_math_class::MathClass; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, }; use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; use crate::layout::{ AlignElem, Alignment, BlockElem, InlineElem, OuterHAlignment, SpecificAlignment, VAlignment, }; use crate::math::{MathSize, MathVariant}; use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; /// A mathematical equation. /// /// Can be displayed inline with text or as a separate block. An equation /// becomes block-level through the presence of at least one space after the /// opening dollar sign and one space before the closing dollar sign. /// /// # Example /// ```example /// #set text(font: "New Computer Modern") /// /// Let $a$, $b$, and $c$ be the side /// lengths of right-angled triangle. /// Then, we know that: /// $ a^2 + b^2 = c^2 $ /// /// Prove by induction: /// $ sum_(k=1)^n k = (n(n+1)) / 2 $ /// ``` /// /// By default, block-level equations will not break across pages. This can be /// changed through `{show math.equation: set block(breakable: true)}`. /// /// # Syntax /// This function also has dedicated syntax: Write mathematical markup within /// dollar signs to create an equation. Starting and ending the equation with at /// least one space lifts it into a separate block that is centered /// horizontally. For more details about math syntax, see the /// [main math page]($category/math). #[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)] pub struct EquationElem { /// Whether the equation is displayed as a separate block. #[default(false)] pub block: bool, /// How to [number]($numbering) block-level equations. /// /// ```example /// #set math.equation(numbering: "(1)") /// /// We define: /// $ phi.alt := (1 + sqrt(5)) / 2 $ /// /// With @ratio, we get: /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ /// ``` #[borrowed] pub numbering: Option, /// The alignment of the equation numbering. /// /// By default, the alignment is `{end + horizon}`. For the horizontal /// component, you can use `{right}`, `{left}`, or `{start}` and `{end}` /// of the text direction; for the vertical component, you can use /// `{top}`, `{horizon}`, or `{bottom}`. /// /// ```example /// #set math.equation(numbering: "(1)", number-align: bottom) /// /// We can calculate: /// $ E &= sqrt(m_0^2 + p^2) \ /// &approx 125 "GeV" $ /// ``` #[default(SpecificAlignment::Both(OuterHAlignment::End, VAlignment::Horizon))] pub number_align: SpecificAlignment, /// A supplement for the equation. /// /// For references to equations, this is added before the referenced number. /// /// If a function is specified, it is passed the referenced equation and /// should return content. /// /// ```example /// #set math.equation(numbering: "(1)", supplement: [Eq.]) /// /// We define: /// $ phi.alt := (1 + sqrt(5)) / 2 $ /// /// With @ratio, we get: /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ /// ``` pub supplement: Smart>, /// The contents of the equation. #[required] pub body: Content, /// The size of the glyphs. #[internal] #[default(MathSize::Text)] #[ghost] pub size: MathSize, /// The style variant to select. #[internal] #[ghost] pub variant: MathVariant, /// Affects the height of exponents. #[internal] #[default(false)] #[ghost] pub cramped: bool, /// Whether to use bold glyphs. #[internal] #[default(false)] #[ghost] pub bold: bool, /// Whether to use italic glyphs. #[internal] #[ghost] pub italic: Smart, /// A forced class to use for all fragment. #[internal] #[ghost] pub class: Option, /// Values of `scriptPercentScaleDown` and `scriptScriptPercentScaleDown` /// respectively in the current font's MathConstants table. #[internal] #[default((70, 50))] #[ghost] pub script_scale: (i16, i16), } impl Synthesize for Packed { fn synthesize( &mut self, engine: &mut Engine, styles: StyleChain, ) -> SourceResult<()> { let supplement = match self.as_ref().supplement(styles) { Smart::Auto => TextElem::packed(Self::local_name_in(styles)), Smart::Custom(None) => Content::empty(), Smart::Custom(Some(supplement)) => { supplement.resolve(engine, styles, [self.clone().pack()])? } }; self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); Ok(()) } } impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { if self.block(styles) { Ok(BlockElem::multi_layouter( self.clone(), engine.routines.layout_equation_block, ) .pack() .spanned(self.span())) } else { Ok(InlineElem::layouter(self.clone(), engine.routines.layout_equation_inline) .pack() .spanned(self.span())) } } } impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); if self.block(styles) { out.set(AlignElem::set_alignment(Alignment::CENTER)); out.set(BlockElem::set_breakable(false)); out.set(ParLine::set_numbering(None)); out.set(EquationElem::set_size(MathSize::Display)); } else { out.set(EquationElem::set_size(MathSize::Text)); } out.set(TextElem::set_weight(FontWeight::from_number(450))); out.set(TextElem::set_font(FontList(vec![FontFamily::new( "New Computer Modern Math", )]))); out } } impl Count for Packed { fn update(&self) -> Option { (self.block(StyleChain::default()) && self.numbering().is_some()) .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) } } impl LocalName for Packed { const KEY: &'static str = "equation"; } impl Refable for Packed { fn supplement(&self) -> Content { // After synthesis, this should always be custom content. match (**self).supplement(StyleChain::default()) { Smart::Custom(Some(Supplement::Content(content))) => content, _ => Content::empty(), } } fn counter(&self) -> Counter { Counter::of(EquationElem::elem()) } fn numbering(&self) -> Option<&Numbering> { (**self).numbering(StyleChain::default()).as_ref() } } impl Outlinable for Packed { fn outlined(&self) -> bool { self.block(StyleChain::default()) && self.numbering().is_some() } fn prefix(&self, numbers: Content) -> Content { let supplement = self.supplement(); if !supplement.is_empty() { supplement + TextElem::packed('\u{a0}') + numbers } else { numbers } } fn body(&self) -> Content { Content::empty() } }