summaryrefslogtreecommitdiff
path: root/library/src/math
diff options
context:
space:
mode:
Diffstat (limited to 'library/src/math')
-rw-r--r--library/src/math/mod.rs188
-rw-r--r--library/src/math/tex.rs164
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();
+ }
+}