diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-10-04 13:42:49 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-10-04 13:45:16 +0200 |
| commit | 5a8534a395b500a25cbc46ee15ec031c8231de59 (patch) | |
| tree | a525c447c3243fe315c7ed91923e158df131809b /src/library/math | |
| parent | 7ef6cb31df0fe1ebec99b1077053a586a349f530 (diff) | |
Parse basic math syntax
Diffstat (limited to 'src/library/math')
| -rw-r--r-- | src/library/math/frac.rs | 17 | ||||
| -rw-r--r-- | src/library/math/mod.rs | 292 | ||||
| -rw-r--r-- | src/library/math/rex.rs | 157 | ||||
| -rw-r--r-- | src/library/math/script.rs | 31 |
4 files changed, 304 insertions, 193 deletions
diff --git a/src/library/math/frac.rs b/src/library/math/frac.rs new file mode 100644 index 00000000..791fd19a --- /dev/null +++ b/src/library/math/frac.rs @@ -0,0 +1,17 @@ +use super::*; +use crate::library::prelude::*; + +/// A fraction in a mathematical formula. +#[derive(Debug, Hash)] +pub struct FracNode { + /// The numerator. + pub num: MathNode, + /// The denominator. + pub denom: MathNode, +} + +impl Texify for FracNode { + fn texify(&self) -> EcoString { + format_eco!("\\frac{{{}}}{{{}}}", self.num.texify(), self.denom.texify()) + } +} diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs index 1f5ea8f3..7b5fdf52 100644 --- a/src/library/math/mod.rs +++ b/src/library/math/mod.rs @@ -1,19 +1,39 @@ //! Mathematical formulas. -mod rex; +mod frac; +mod script; +pub use frac::*; +pub use script::*; + +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 crate::font::Font; use crate::library::layout::BlockSpacing; use crate::library::prelude::*; -use crate::library::text::FontFamily; -use crate::syntax::Spanned; - -/// A mathematical formula. -#[derive(Debug, Hash)] -pub struct MathNode { - /// The formula. - pub formula: Spanned<EcoString>, - /// Whether the formula is display-level. - pub display: bool, +use crate::library::text::{variant, FontFamily, Lang, TextNode}; + +/// A piece of a mathematical formula. +#[derive(Debug, Clone, Hash)] +pub enum MathNode { + /// Whitespace. + Space, + /// A forced line break. + Linebreak, + /// An atom in a math formula: `x`, `+`, `12`. + Atom(EcoString), + /// A base with optional sub and superscripts: `a_1^2`. + Script(Arc<ScriptNode>), + /// A fraction: `x/2`. + Frac(Arc<FracNode>), + /// A numbered math alignment indicator: `&`, `&&`. + Align(usize), + /// A row of mathematical material. + Row(Arc<Vec<MathNode>>, Span), } #[node(showable)] @@ -28,41 +48,54 @@ impl MathNode { #[property(resolve, shorthand(around))] pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into()); - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Content::show(Self { - formula: args.expect("formula")?, - display: args.named("display")?.unwrap_or(false), - })) + fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> { + todo!() + } +} + +impl MathNode { + /// Whether this is a display-style node. + pub fn display(&self) -> bool { + match self { + Self::Row(row, _) => { + matches!(row.as_slice(), [MathNode::Space, .., MathNode::Space]) + } + _ => false, + } + } + + /// Strip parentheses from the node. + pub fn unparen(self) -> Self { + if let Self::Row(row, span) = &self { + if let [MathNode::Atom(l), .., MathNode::Atom(r)] = row.as_slice() { + if l == "(" && r == ")" { + let inner = row[1 .. row.len() - 1].to_vec(); + return Self::Row(Arc::new(inner), *span); + } + } + } + + self } } impl Show for MathNode { fn unguard(&self, _: Selector) -> ShowNode { - Self { formula: self.formula.clone(), ..*self }.pack() + ShowNode::new(self.clone()) } fn encode(&self, _: StyleChain) -> Dict { - dict! { - "formula" => Value::Str(self.formula.v.clone().into()), - "display" => Value::Bool(self.display) - } + todo!() } - fn realize( - &self, - _: Tracked<dyn World>, - styles: StyleChain, - ) -> SourceResult<Content> { - let node = self::rex::RexNode { - tex: self.formula.clone(), - display: self.display, - family: styles.get(Self::FAMILY).clone(), - }; - - Ok(if self.display { - Content::block(node.pack().aligned(Spec::with_x(Some(Align::Center.into())))) + fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> { + Ok(if self.display() { + Content::block( + LayoutNode::new(self.clone()) + .aligned(Spec::with_x(Some(Align::Center.into()))), + ) } else { - Content::inline(node) + Content::inline(self.clone()) }) } @@ -75,10 +108,197 @@ impl Show for MathNode { let mut map = StyleMap::new(); map.set_family(styles.get(Self::FAMILY).clone(), styles); - if self.display { + if self.display() { realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)); } Ok(realized.styled_with_map(map)) } } + +impl Layout for MathNode { + fn layout( + &self, + world: Tracked<dyn World>, + _: &Regions, + styles: StyleChain, + ) -> SourceResult<Vec<Frame>> { + let style = if self.display() { Style::Display } else { Style::Text }; + let span = match self { + &Self::Row(_, span) => span, + _ => Span::detached(), + }; + Ok(vec![layout_tex(world, self, span, style, styles)?]) + } +} + +/// Layout a TeX formula into a frame. +fn layout_tex( + world: Tracked<dyn World>, + node: &dyn Texify, + span: Span, + style: Style, + styles: StyleChain, +) -> SourceResult<Frame> { + let tex = node.texify(); + + // Load the font. + let font = world + .book() + .select(styles.get(MathNode::FAMILY).as_str(), variant(styles)) + .and_then(|id| world.font(id)) + .ok_or("failed to find math font") + .at(span)?; + + // Prepare the font context. + let ctx = font + .math() + .map(|math| FontContext::new(font.ttf(), math)) + .ok_or("font is not suitable for math") + .at(span)?; + + // Layout the formula. + let em = styles.get(TextNode::SIZE); + 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(), + }) + .at(span)?; + + // Determine the metrics. + let (x0, y0, x1, y1) = renderer.size(&layout); + let width = Length::pt(x1 - x0); + let mut top = Length::pt(y1); + let mut bottom = Length::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: Length, + 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(Length::pt(cursor.x), self.baseline + Length::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: Length::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( + Length::pt(width), + Length::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(); + } +} + +/// Turn a math node into TeX math code. +trait Texify { + /// Perform the conversion. + fn texify(&self) -> EcoString; +} + +impl Texify for MathNode { + fn texify(&self) -> EcoString { + match self { + Self::Space => "".into(), + Self::Linebreak => r"\\".into(), + Self::Atom(atom) => atom.chars().map(escape_char).collect(), + Self::Script(script) => script.texify(), + Self::Frac(frac) => frac.texify(), + Self::Align(_) => "".into(), + Self::Row(row, _) => row.iter().map(Texify::texify).collect(), + } + } +} + +#[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(), + } +} diff --git a/src/library/math/rex.rs b/src/library/math/rex.rs deleted file mode 100644 index 96e8e438..00000000 --- a/src/library/math/rex.rs +++ /dev/null @@ -1,157 +0,0 @@ -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 crate::font::Font; -use crate::library::prelude::*; -use crate::library::text::{variant, FontFamily, Lang, TextNode}; - -/// A layout node that renders with ReX. -#[derive(Debug, Hash)] -pub struct RexNode { - /// The TeX formula. - pub tex: Spanned<EcoString>, - /// Whether the formula is display-level. - pub display: bool, - /// The font family. - pub family: FontFamily, -} - -impl Layout for RexNode { - fn layout( - &self, - world: Tracked<dyn World>, - _: &Regions, - styles: StyleChain, - ) -> SourceResult<Vec<Frame>> { - // Load the font. - let span = self.tex.span; - let font = world - .book() - .select(self.family.as_str(), variant(styles)) - .and_then(|id| world.font(id)) - .ok_or("failed to find math font") - .at(span)?; - - // Prepare the font context. - let ctx = font - .math() - .map(|math| FontContext::new(font.ttf(), math)) - .ok_or("font is not suitable for math") - .at(span)?; - - // Layout the formula. - let em = styles.get(TextNode::SIZE); - let style = if self.display { Style::Display } else { Style::Text }; - let settings = LayoutSettings::new(&ctx, em.to_pt(), style); - let renderer = Renderer::new(); - let layout = renderer - .layout(&self.tex.v, settings) - .map_err(|err| match err { - Error::Parse(err) => err.to_string(), - Error::Layout(LayoutError::Font(err)) => err.to_string(), - }) - .at(span)?; - - // Determine the metrics. - let (x0, y0, x1, y1) = renderer.size(&layout); - let width = Length::pt(x1 - x0); - let mut top = Length::pt(y1); - let mut bottom = Length::pt(-y0); - if !self.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(vec![backend.frame]) - } -} - -/// A ReX rendering backend that renders into a frame. -struct FrameBackend { - frame: Frame, - baseline: Length, - 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(Length::pt(cursor.x), self.baseline + Length::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: Length::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( - Length::pt(width), - Length::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(); - } -} diff --git a/src/library/math/script.rs b/src/library/math/script.rs new file mode 100644 index 00000000..09f52164 --- /dev/null +++ b/src/library/math/script.rs @@ -0,0 +1,31 @@ +use std::fmt::Write; + +use super::*; +use crate::library::prelude::*; + +/// A sub- and/or superscript in a mathematical formula. +#[derive(Debug, Hash)] +pub struct ScriptNode { + /// The base. + pub base: MathNode, + /// The subscript. + pub sub: Option<MathNode>, + /// The superscript. + pub sup: Option<MathNode>, +} + +impl Texify for ScriptNode { + fn texify(&self) -> EcoString { + let mut tex = self.base.texify(); + + if let Some(sub) = &self.sub { + write!(tex, "_{{{}}}", sub.texify()).unwrap(); + } + + if let Some(sup) = &self.sup { + write!(tex, "^{{{}}}", sup.texify()).unwrap(); + } + + tex + } +} |
