summaryrefslogtreecommitdiff
path: root/src/library/math
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-10-04 13:42:49 +0200
committerLaurenz <laurmaedje@gmail.com>2022-10-04 13:45:16 +0200
commit5a8534a395b500a25cbc46ee15ec031c8231de59 (patch)
treea525c447c3243fe315c7ed91923e158df131809b /src/library/math
parent7ef6cb31df0fe1ebec99b1077053a586a349f530 (diff)
Parse basic math syntax
Diffstat (limited to 'src/library/math')
-rw-r--r--src/library/math/frac.rs17
-rw-r--r--src/library/math/mod.rs292
-rw-r--r--src/library/math/rex.rs157
-rw-r--r--src/library/math/script.rs31
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
+ }
+}