diff options
Diffstat (limited to 'library/src/math')
| -rw-r--r-- | library/src/math/mod.rs | 188 | ||||
| -rw-r--r-- | library/src/math/tex.rs | 164 |
2 files changed, 352 insertions, 0 deletions
diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs new file mode 100644 index 00000000..e46ba040 --- /dev/null +++ b/library/src/math/mod.rs @@ -0,0 +1,188 @@ +//! 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<Content>, + /// 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<BlockSpacing> = Some(Ratio::one().into()); + /// The spacing below display math. + #[property(resolve, shorthand(around))] + pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into()); +} + +impl Show for MathNode { + fn unguard_parts(&self, _: Selector) -> Content { + self.clone().pack() + } + + fn field(&self, _: &str) -> Option<Value> { + None + } + + fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> { + Ok(if self.display { + self.clone().pack().aligned(Axes::with_x(Some(Align::Center.into()))) + } else { + self.clone().pack() + }) + } + + fn finalize( + &self, + _: Tracked<dyn World>, + styles: StyleChain, + realized: Content, + ) -> SourceResult<Content> { + 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<dyn World>, + _: &Regions, + styles: StyleChain, + ) -> SourceResult<Vec<Frame>> { + 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<Content>, + /// The superscript. + pub sup: Option<Content>, +} + +#[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 + } +} diff --git a/library/src/math/tex.rs b/library/src/math/tex.rs new file mode 100644 index 00000000..7b40aa2b --- /dev/null +++ b/library/src/math/tex.rs @@ -0,0 +1,164 @@ +use rex::error::{Error, LayoutError}; +use rex::font::FontContext; +use rex::layout::{LayoutSettings, Style}; +use rex::parser::color::RGBA; +use rex::render::{Backend, Cursor, Renderer}; +use typst::font::Font; + +use super::*; +use crate::prelude::*; +use crate::text::{variant, LinebreakNode, SpaceNode, TextNode}; + +/// Turn a math node into TeX math code. +#[capability] +pub trait Texify: 'static + Sync + Send { + /// Perform the conversion. + fn texify(&self) -> EcoString; +} + +impl Texify for Content { + fn texify(&self) -> EcoString { + if self.is::<SpaceNode>() { + return EcoString::new(); + } + + if self.is::<LinebreakNode>() { + return r"\\".into(); + } + + if let Some(node) = self.to::<dyn Texify>() { + return node.texify(); + } + + panic!("{self:?} is not math"); + } +} + +/// Layout a TeX formula into a frame. +pub fn layout_tex( + tex: &str, + display: bool, + world: Tracked<dyn World>, + styles: StyleChain, +) -> SourceResult<Frame> { + // Load the font. + let font = world + .book() + .select(styles.get(MathNode::FAMILY).as_str(), variant(styles)) + .and_then(|id| world.font(id)) + .expect("failed to find math font"); + + // Prepare the font context. + let ctx = font + .math() + .map(|math| FontContext::new(font.ttf(), math)) + .expect("font is not suitable for math"); + + // Layout the formula. + let em = styles.get(TextNode::SIZE); + let style = if display { Style::Display } else { Style::Text }; + let settings = LayoutSettings::new(&ctx, em.to_pt(), style); + let renderer = Renderer::new(); + let layout = renderer + .layout(&tex, settings) + .map_err(|err| match err { + Error::Parse(err) => err.to_string(), + Error::Layout(LayoutError::Font(err)) => err.to_string(), + }) + .expect("failed to layout with rex"); + + // Determine the metrics. + let (x0, y0, x1, y1) = renderer.size(&layout); + let width = Abs::pt(x1 - x0); + let mut top = Abs::pt(y1); + let mut bottom = Abs::pt(-y0); + if style != Style::Display { + let metrics = font.metrics(); + top = styles.get(TextNode::TOP_EDGE).resolve(styles, metrics); + bottom = -styles.get(TextNode::BOTTOM_EDGE).resolve(styles, metrics); + }; + + // Prepare a frame rendering backend. + let size = Size::new(width, top + bottom); + let mut backend = FrameBackend { + frame: { + let mut frame = Frame::new(size); + frame.set_baseline(top); + frame.apply_role(Role::Formula); + frame + }, + baseline: top, + font: font.clone(), + fill: styles.get(TextNode::FILL), + lang: styles.get(TextNode::LANG), + colors: vec![], + }; + + // Render into the frame. + renderer.render(&layout, &mut backend); + Ok(backend.frame) +} + +/// A ReX rendering backend that renders into a frame. +struct FrameBackend { + frame: Frame, + baseline: Abs, + font: Font, + fill: Paint, + lang: Lang, + colors: Vec<RGBA>, +} + +impl FrameBackend { + /// The currently active fill paint. + fn fill(&self) -> Paint { + self.colors + .last() + .map(|&RGBA(r, g, b, a)| RgbaColor::new(r, g, b, a).into()) + .unwrap_or(self.fill) + } + + /// Convert a cursor to a point. + fn transform(&self, cursor: Cursor) -> Point { + Point::new(Abs::pt(cursor.x), self.baseline + Abs::pt(cursor.y)) + } +} + +impl Backend for FrameBackend { + fn symbol(&mut self, pos: Cursor, gid: u16, scale: f64) { + self.frame.push( + self.transform(pos), + Element::Text(Text { + font: self.font.clone(), + size: Abs::pt(scale), + fill: self.fill(), + lang: self.lang, + glyphs: vec![Glyph { + id: gid, + x_advance: Em::new(0.0), + x_offset: Em::new(0.0), + c: ' ', + }], + }), + ); + } + + fn rule(&mut self, pos: Cursor, width: f64, height: f64) { + self.frame.push( + self.transform(pos), + Element::Shape(Shape { + geometry: Geometry::Rect(Size::new(Abs::pt(width), Abs::pt(height))), + fill: Some(self.fill()), + stroke: None, + }), + ); + } + + fn begin_color(&mut self, color: RGBA) { + self.colors.push(color); + } + + fn end_color(&mut self) { + self.colors.pop(); + } +} |
