//! Mathematical formulas. mod tex; use std::fmt::Write; use self::tex::{layout_tex, Texify}; use crate::layout::BlockSpacing; use crate::prelude::*; use crate::text::FontFamily; /// A piece of a mathematical formula. #[derive(Debug, Clone, Hash)] pub struct MathNode { /// The pieces of the formula. pub children: Vec, /// Whether the formula is display-level. pub display: bool, } #[node(Show, LayoutInline, Texify)] impl MathNode { /// The math font family. #[property(referenced)] pub const FAMILY: FontFamily = FontFamily::new("NewComputerModernMath"); /// The spacing above display math. #[property(resolve, shorthand(around))] pub const ABOVE: Option = Some(Ratio::one().into()); /// The spacing below display math. #[property(resolve, shorthand(around))] pub const BELOW: Option = Some(Ratio::one().into()); } impl Show for MathNode { fn unguard_parts(&self, _: Selector) -> Content { self.clone().pack() } fn field(&self, _: &str) -> Option { None } fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { Ok(if self.display { self.clone().pack().aligned(Axes::with_x(Some(Align::Center.into()))) } else { self.clone().pack() }) } fn finalize( &self, _: Tracked, styles: StyleChain, realized: Content, ) -> SourceResult { Ok(if self.display { realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)) } else { realized }) } } impl LayoutInline for MathNode { fn layout_inline( &self, world: Tracked, _: &Regions, styles: StyleChain, ) -> SourceResult> { Ok(vec![layout_tex( &self.texify(), self.display, world, styles, )?]) } } impl Texify for MathNode { fn texify(&self) -> EcoString { self.children.iter().map(Texify::texify).collect() } } /// An atom in a math formula: `x`, `+`, `12`. #[derive(Debug, Hash)] pub struct AtomNode(pub EcoString); #[node(Texify)] impl AtomNode {} impl Texify for AtomNode { fn texify(&self) -> EcoString { self.0.chars().map(escape_char).collect() } } /// A fraction in a mathematical formula. #[derive(Debug, Hash)] pub struct FracNode { /// The numerator. pub num: Content, /// The denominator. pub denom: Content, } #[node(Texify)] impl FracNode {} impl Texify for FracNode { fn texify(&self) -> EcoString { format_eco!( "\\frac{{{}}}{{{}}}", unparen(self.num.texify()), unparen(self.denom.texify()) ) } } /// A sub- and/or superscript in a mathematical formula. #[derive(Debug, Hash)] pub struct ScriptNode { /// The base. pub base: Content, /// The subscript. pub sub: Option, /// The superscript. pub sup: Option, } #[node(Texify)] impl ScriptNode {} impl Texify for ScriptNode { fn texify(&self) -> EcoString { let mut tex = self.base.texify(); if let Some(sub) = &self.sub { write!(tex, "_{{{}}}", unparen(sub.texify())).unwrap(); } if let Some(sup) = &self.sup { write!(tex, "^{{{}}}", unparen(sup.texify())).unwrap(); } tex } } /// A math alignment indicator: `&`, `&&`. #[derive(Debug, Hash)] pub struct AlignNode(pub usize); #[node(Texify)] impl AlignNode {} impl Texify for AlignNode { fn texify(&self) -> EcoString { EcoString::new() } } /// Escape a char for TeX usage. #[rustfmt::skip] fn escape_char(c: char) -> EcoString { match c { '{' | '}' | '%' | '&' | '$' | '#' => format_eco!(" \\{c} "), 'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' | '*' | '+' | '-' | '[' | '(' | ']' | ')' | '?' | '!' | '=' | '<' | '>' | ':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => c.into(), c => unicode_math::SYMBOLS .iter() .find(|sym| sym.codepoint == c) .map(|sym| format_eco!("\\{} ", sym.name)) .unwrap_or_default(), } } /// Trim grouping parenthesis≤. fn unparen(s: EcoString) -> EcoString { if s.starts_with('(') && s.ends_with(')') { s[1 .. s.len() - 1].into() } else { s } }