summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-01-22 13:26:42 +0100
committerLaurenz <laurmaedje@gmail.com>2023-01-22 13:26:42 +0100
commit83b68581461df8968e408bec1b979ed2e3a8f0c5 (patch)
tree9c36f1ccb25187ff20eac970847b554ddd01df8b
parenta2d77e36bab83056e3c487adfbeff161485ca47d (diff)
Math framework
-rw-r--r--library/src/lib.rs21
-rw-r--r--library/src/math/ctx.rs169
-rw-r--r--library/src/math/fragment.rs239
-rw-r--r--library/src/math/group.rs71
-rw-r--r--library/src/math/mod.rs264
-rw-r--r--library/src/math/row.rs118
-rw-r--r--src/doc.rs18
-rw-r--r--src/model/styles.rs5
8 files changed, 628 insertions, 277 deletions
diff --git a/library/src/lib.rs b/library/src/lib.rs
index 24f3560f..f714c72b 100644
--- a/library/src/lib.rs
+++ b/library/src/lib.rs
@@ -49,24 +49,7 @@ fn scope() -> Scope {
std.def_func::<text::RawNode>("raw");
// Math.
- std.def_func::<math::MathNode>("math");
- std.def_func::<math::AccNode>("acc");
- std.def_func::<math::FracNode>("frac");
- std.def_func::<math::BinomNode>("binom");
- std.def_func::<math::ScriptNode>("script");
- std.def_func::<math::SqrtNode>("sqrt");
- std.def_func::<math::FloorNode>("floor");
- std.def_func::<math::CeilNode>("ceil");
- std.def_func::<math::VecNode>("vec");
- std.def_func::<math::CasesNode>("cases");
- std.def_func::<math::SerifNode>("serif");
- std.def_func::<math::SansNode>("sans");
- std.def_func::<math::BoldNode>("bold");
- std.def_func::<math::ItalNode>("ital");
- std.def_func::<math::CalNode>("cal");
- std.def_func::<math::FrakNode>("frak");
- std.def_func::<math::MonoNode>("mono");
- std.def_func::<math::BbNode>("bb");
+ math::define(&mut std);
// Layout.
std.def_func::<layout::PageNode>("page");
@@ -204,7 +187,7 @@ fn items() -> LangItems {
term_item: |term, description| {
layout::ListItem::Term(basics::TermItem { term, description }).pack()
},
- math: |children, block| math::MathNode { children, block }.pack(),
+ math: |body, block| math::MathNode { body, block }.pack(),
math_atom: |atom| math::AtomNode(atom).pack(),
math_script: |base, sub, sup| math::ScriptNode { base, sub, sup }.pack(),
math_frac: |num, denom| math::FracNode { num, denom }.pack(),
diff --git a/library/src/math/ctx.rs b/library/src/math/ctx.rs
new file mode 100644
index 00000000..41299f98
--- /dev/null
+++ b/library/src/math/ctx.rs
@@ -0,0 +1,169 @@
+use ttf_parser::math::MathValue;
+
+use super::*;
+
+macro_rules! scaled {
+ ($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => {
+ match $ctx.style.size {
+ MathSize::Display => scaled!($ctx, $display),
+ _ => scaled!($ctx, $text),
+ }
+ };
+ ($ctx:expr, $name:ident) => {
+ $ctx.constants.$name().scaled($ctx)
+ };
+}
+
+macro_rules! percent {
+ ($ctx:expr, $name:ident) => {
+ $ctx.constants.$name() as f64 / 100.0
+ };
+}
+
+/// The context for math layout.
+pub(super) struct MathContext<'a, 'b, 'v> {
+ pub vt: &'v mut Vt<'b>,
+ pub outer: StyleChain<'a>,
+ pub map: StyleMap,
+ pub regions: Regions<'a>,
+ pub font: &'a Font,
+ pub ttf: &'a ttf_parser::Face<'a>,
+ pub table: ttf_parser::math::Table<'a>,
+ pub constants: ttf_parser::math::Constants<'a>,
+ pub fill: Paint,
+ pub lang: Lang,
+ pub row: MathRow,
+ pub style: MathStyle,
+ base_size: Abs,
+ scaled_size: Abs,
+ style_stack: Vec<MathStyle>,
+}
+
+impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
+ pub fn new(
+ vt: &'v mut Vt<'b>,
+ styles: StyleChain<'a>,
+ regions: Regions,
+ font: &'a Font,
+ block: bool,
+ ) -> Self {
+ let table = font.ttf().tables().math.unwrap();
+ let constants = table.constants.unwrap();
+ let size = styles.get(TextNode::SIZE);
+ Self {
+ vt,
+ outer: styles,
+ map: StyleMap::new(),
+ regions: {
+ let size = Size::new(regions.first.x, regions.base.y);
+ Regions::one(size, regions.base, Axes::splat(false))
+ },
+ style: MathStyle {
+ variant: MathVariant::Serif,
+ size: if block { MathSize::Display } else { MathSize::Text },
+ cramped: false,
+ bold: false,
+ italic: true,
+ },
+ fill: styles.get(TextNode::FILL),
+ lang: styles.get(TextNode::LANG),
+ font: &font,
+ ttf: font.ttf(),
+ table,
+ constants,
+ row: MathRow::new(),
+ base_size: size,
+ scaled_size: size,
+ style_stack: vec![],
+ }
+ }
+
+ pub fn push(&mut self, fragment: impl Into<MathFragment>) {
+ self.row.push(self.scaled_size, self.style, fragment);
+ }
+
+ pub fn layout_non_math(&mut self, content: &Content) -> SourceResult<Frame> {
+ Ok(content
+ .layout(&mut self.vt, self.outer.chain(&self.map), self.regions)?
+ .into_frame())
+ }
+
+ pub fn layout_fragment(
+ &mut self,
+ node: &dyn LayoutMath,
+ ) -> SourceResult<MathFragment> {
+ let row = self.layout_row(node)?;
+ Ok(if row.0.len() == 1 {
+ row.0.into_iter().next().unwrap()
+ } else {
+ row.to_frame(self).into()
+ })
+ }
+
+ pub fn layout_row(&mut self, node: &dyn LayoutMath) -> SourceResult<MathRow> {
+ let prev = std::mem::take(&mut self.row);
+ node.layout_math(self)?;
+ Ok(std::mem::replace(&mut self.row, prev))
+ }
+
+ pub fn layout_frame(&mut self, node: &dyn LayoutMath) -> SourceResult<Frame> {
+ Ok(self.layout_fragment(node)?.to_frame(self))
+ }
+
+ pub fn size(&self) -> Abs {
+ self.scaled_size
+ }
+
+ pub fn style(&mut self, style: MathStyle) {
+ self.style_stack.push(self.style);
+ self.style = style;
+ self.rescale();
+ self.map.set(TextNode::SIZE, TextSize(self.scaled_size.into()));
+ }
+
+ pub fn unstyle(&mut self) {
+ self.style = self.style_stack.pop().unwrap();
+ self.rescale();
+ self.map.unset();
+ }
+
+ fn rescale(&mut self) {
+ self.scaled_size = match self.style.size {
+ MathSize::Display | MathSize::Text => self.base_size,
+ MathSize::Script => {
+ self.base_size * percent!(self, script_percent_scale_down)
+ }
+ MathSize::ScriptScript => {
+ self.base_size * percent!(self, script_script_percent_scale_down)
+ }
+ };
+ }
+}
+
+pub(super) trait Scaled {
+ fn scaled(self, ctx: &MathContext) -> Abs;
+}
+
+impl Scaled for i16 {
+ fn scaled(self, ctx: &MathContext) -> Abs {
+ ctx.font.to_em(self).scaled(ctx)
+ }
+}
+
+impl Scaled for u16 {
+ fn scaled(self, ctx: &MathContext) -> Abs {
+ ctx.font.to_em(self).scaled(ctx)
+ }
+}
+
+impl Scaled for Em {
+ fn scaled(self, ctx: &MathContext) -> Abs {
+ self.at(ctx.size())
+ }
+}
+
+impl Scaled for MathValue<'_> {
+ fn scaled(self, ctx: &MathContext) -> Abs {
+ self.value.scaled(ctx)
+ }
+}
diff --git a/library/src/math/fragment.rs b/library/src/math/fragment.rs
new file mode 100644
index 00000000..d6d55cc3
--- /dev/null
+++ b/library/src/math/fragment.rs
@@ -0,0 +1,239 @@
+use super::*;
+
+#[derive(Debug, Clone)]
+pub(super) enum MathFragment {
+ Glyph(GlyphFragment),
+ Variant(VariantFragment),
+ Frame(FrameFragment),
+ Spacing(Abs),
+ Align,
+ Linebreak,
+}
+
+impl MathFragment {
+ pub fn size(&self) -> Size {
+ Size::new(self.width(), self.height())
+ }
+
+ pub fn width(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.width,
+ Self::Variant(variant) => variant.frame.width(),
+ Self::Frame(fragment) => fragment.frame.width(),
+ Self::Spacing(amount) => *amount,
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn height(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.height(),
+ Self::Variant(variant) => variant.frame.height(),
+ Self::Frame(fragment) => fragment.frame.height(),
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn ascent(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.ascent,
+ Self::Variant(variant) => variant.frame.ascent(),
+ Self::Frame(fragment) => fragment.frame.baseline(),
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn descent(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.descent,
+ Self::Variant(variant) => variant.frame.descent(),
+ Self::Frame(fragment) => fragment.frame.descent(),
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn class(&self) -> Option<MathClass> {
+ match self {
+ Self::Glyph(glyph) => glyph.class(),
+ Self::Variant(variant) => variant.class(),
+ Self::Frame(fragment) => Some(fragment.class),
+ _ => None,
+ }
+ }
+
+ pub fn italics_correction(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.italics_correction,
+ Self::Variant(variant) => variant.italics_correction,
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn to_frame(self, ctx: &MathContext) -> Frame {
+ match self {
+ Self::Glyph(glyph) => glyph.to_frame(ctx),
+ Self::Variant(variant) => variant.frame,
+ Self::Frame(fragment) => fragment.frame,
+ _ => Frame::new(self.size()),
+ }
+ }
+}
+
+impl From<GlyphFragment> for MathFragment {
+ fn from(glyph: GlyphFragment) -> Self {
+ Self::Glyph(glyph)
+ }
+}
+
+impl From<VariantFragment> for MathFragment {
+ fn from(variant: VariantFragment) -> Self {
+ Self::Variant(variant)
+ }
+}
+
+impl From<FrameFragment> for MathFragment {
+ fn from(fragment: FrameFragment) -> Self {
+ Self::Frame(fragment)
+ }
+}
+
+impl From<Frame> for MathFragment {
+ fn from(frame: Frame) -> Self {
+ Self::Frame(FrameFragment { frame, class: MathClass::Normal, limits: false })
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub(super) struct GlyphFragment {
+ pub id: GlyphId,
+ pub c: char,
+ pub font_size: Abs,
+ pub width: Abs,
+ pub ascent: Abs,
+ pub descent: Abs,
+ pub italics_correction: Abs,
+}
+
+impl GlyphFragment {
+ pub fn new(ctx: &MathContext, c: char) -> Self {
+ let c = ctx.style.styled_char(c);
+ let id = ctx.ttf.glyph_index(c).unwrap_or_default();
+ Self::with_id(ctx, c, id)
+ }
+
+ pub fn try_new(ctx: &MathContext, c: char) -> Option<Self> {
+ let c = ctx.style.styled_char(c);
+ let id = ctx.ttf.glyph_index(c)?;
+ Some(Self::with_id(ctx, c, id))
+ }
+
+ pub fn with_id(ctx: &MathContext, c: char, id: GlyphId) -> Self {
+ let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default();
+ let italics = italics_correction(ctx, id).unwrap_or_default();
+ let bbox = ctx.ttf.glyph_bounding_box(id).unwrap_or(Rect {
+ x_min: 0,
+ y_min: 0,
+ x_max: 0,
+ y_max: 0,
+ });
+ Self {
+ id,
+ c,
+ font_size: ctx.size(),
+ width: advance.scaled(ctx),
+ ascent: bbox.y_max.scaled(ctx),
+ descent: -bbox.y_min.scaled(ctx),
+ italics_correction: italics,
+ }
+ }
+
+ pub fn height(&self) -> Abs {
+ self.ascent + self.descent
+ }
+
+ pub fn class(&self) -> Option<MathClass> {
+ unicode_math_class::class(self.c)
+ }
+
+ pub fn to_variant(&self, ctx: &MathContext) -> VariantFragment {
+ VariantFragment {
+ c: self.c,
+ id: Some(self.id),
+ frame: self.to_frame(ctx),
+ italics_correction: self.italics_correction,
+ }
+ }
+
+ pub fn to_frame(&self, ctx: &MathContext) -> Frame {
+ let text = Text {
+ font: ctx.font.clone(),
+ size: self.font_size,
+ fill: ctx.fill,
+ lang: ctx.lang,
+ glyphs: vec![Glyph {
+ id: self.id.0,
+ c: self.c,
+ x_advance: Em::from_length(self.width, ctx.size()),
+ x_offset: Em::zero(),
+ }],
+ };
+ let size = Size::new(self.width, self.ascent + self.descent);
+ let mut frame = Frame::new(size);
+ frame.set_baseline(self.ascent);
+ frame.push(Point::with_y(self.ascent), Element::Text(text));
+ frame
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct VariantFragment {
+ pub c: char,
+ pub id: Option<GlyphId>,
+ pub frame: Frame,
+ pub italics_correction: Abs,
+}
+
+impl VariantFragment {
+ pub fn class(&self) -> Option<MathClass> {
+ unicode_math_class::class(self.c)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct FrameFragment {
+ pub frame: Frame,
+ pub class: MathClass,
+ pub limits: bool,
+}
+
+/// Look up the italics correction for a glyph.
+fn italics_correction(ctx: &MathContext, id: GlyphId) -> Option<Abs> {
+ Some(ctx.table.glyph_info?.italic_corrections?.get(id)?.scaled(ctx))
+}
+
+/// Look up a kerning value at a specific corner and height.
+///
+/// This can be integrated once we've found a font that actually provides this
+/// data.
+#[allow(unused)]
+fn kern_at_height(
+ ctx: &MathContext,
+ id: GlyphId,
+ corner: Corner,
+ height: Abs,
+) -> Option<Abs> {
+ let kerns = ctx.table.glyph_info?.kern_infos?.get(id)?;
+ let kern = match corner {
+ Corner::TopLeft => kerns.top_left,
+ Corner::TopRight => kerns.top_right,
+ Corner::BottomRight => kerns.bottom_right,
+ Corner::BottomLeft => kerns.bottom_left,
+ }?;
+
+ let mut i = 0;
+ while i < kern.count() && height > kern.height(i)?.scaled(ctx) {
+ i += 1;
+ }
+
+ Some(kern.kern(i)?.scaled(ctx))
+}
diff --git a/library/src/math/group.rs b/library/src/math/group.rs
deleted file mode 100644
index 4a55e0e8..00000000
--- a/library/src/math/group.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-use super::*;
-
-/// # Floor
-/// A floored expression.
-///
-/// ## Example
-/// ```
-/// $ floor(x/2) $
-/// ```
-///
-/// ## Parameters
-/// - body: Content (positional, required)
-/// The expression to floor.
-///
-/// ## Category
-/// math
-#[func]
-#[capable(Texify)]
-#[derive(Debug, Hash)]
-pub struct FloorNode(pub Content);
-
-#[node]
-impl FloorNode {
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-}
-
-impl Texify for FloorNode {
- fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
- t.push_str("\\left\\lfloor ");
- self.0.texify(t)?;
- t.push_str("\\right\\rfloor ");
- Ok(())
- }
-}
-
-/// # Ceil
-/// A ceiled expression.
-///
-/// ## Example
-/// ```
-/// $ ceil(x/2) $
-/// ```
-///
-/// ## Parameters
-/// - body: Content (positional, required)
-/// The expression to ceil.
-///
-/// ## Category
-/// math
-#[func]
-#[capable(Texify)]
-#[derive(Debug, Hash)]
-pub struct CeilNode(pub Content);
-
-#[node]
-impl CeilNode {
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-}
-
-impl Texify for CeilNode {
- fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
- t.push_str("\\left\\lceil ");
- self.0.texify(t)?;
- t.push_str("\\right\\rceil ");
- Ok(())
- }
-}
diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs
index 785e027a..14fa9f5d 100644
--- a/library/src/math/mod.rs
+++ b/library/src/math/mod.rs
@@ -1,5 +1,7 @@
//! Mathematical formulas.
+#[macro_use]
+mod ctx;
mod accent;
mod atom;
mod frac;
@@ -8,23 +10,54 @@ mod matrix;
mod root;
mod script;
mod style;
-mod tex;
pub use self::accent::*;
+pub use self::align::*;
pub use self::atom::*;
+pub use self::braced::*;
pub use self::frac::*;
-pub use self::group::*;
+pub use self::lr::*;
pub use self::matrix::*;
+pub use self::op::*;
pub use self::root::*;
pub use self::script::*;
pub use self::style::*;
-use typst::model::{Guard, SequenceNode};
-use unicode_segmentation::UnicodeSegmentation;
-
-use self::tex::layout_tex;
+use ttf_parser::GlyphId;
+use ttf_parser::Rect;
+use typst::font::Font;
+use typst::model::{Guard, Scope, SequenceNode};
+use unicode_math_class::MathClass;
+
+use self::ctx::*;
+use self::fragment::*;
+use self::row::*;
+use self::spacing::*;
+use crate::layout::HNode;
+use crate::layout::ParNode;
use crate::prelude::*;
-use crate::text::{FontFamily, LinebreakNode, SpaceNode, SymbolNode, TextNode};
+use crate::text::LinebreakNode;
+use crate::text::TextNode;
+use crate::text::TextSize;
+use crate::text::{families, variant, FallbackList, FontFamily, SpaceNode, SymbolNode};
+
+/// Hook up all math definitions.
+pub fn define(scope: &mut Scope) {
+ scope.def_func::<MathNode>("math");
+ scope.def_func::<FracNode>("frac");
+ scope.def_func::<ScriptNode>("script");
+ scope.def_func::<SqrtNode>("sqrt");
+ scope.def_func::<VecNode>("vec");
+ scope.def_func::<CasesNode>("cases");
+ scope.def_func::<BoldNode>("bold");
+ scope.def_func::<ItalicNode>("italic");
+ scope.def_func::<SerifNode>("serif");
+ scope.def_func::<SansNode>("sans");
+ scope.def_func::<CalNode>("cal");
+ scope.def_func::<FrakNode>("frak");
+ scope.def_func::<MonoNode>("mono");
+ scope.def_func::<BbNode>("bb");
+}
/// # Math
/// A mathematical formula.
@@ -67,8 +100,8 @@ use crate::text::{FontFamily, LinebreakNode, SpaceNode, SymbolNode, TextNode};
/// ```
///
/// ## Parameters
-/// - items: Content (positional, variadic)
-/// The individual parts of the formula.
+/// - body: Content (positional, required)
+/// The contents of the formula.
///
/// - block: bool (named)
/// Whether the formula is displayed as a separate block.
@@ -76,21 +109,21 @@ use crate::text::{FontFamily, LinebreakNode, SpaceNode, SymbolNode, TextNode};
/// ## Category
/// math
#[func]
-#[capable(Show, Layout, Inline, Texify)]
+#[capable(Show, Finalize, Layout, Inline, LayoutMath)]
#[derive(Debug, Clone, Hash)]
pub struct MathNode {
/// Whether the formula is displayed as a separate block.
pub block: bool,
- /// The pieces of the formula.
- pub children: Vec<Content>,
+ /// The content of the formula.
+ pub body: Content,
}
#[node]
impl MathNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ let body = args.expect("body")?;
let block = args.named("block")?.unwrap_or(false);
- let children = args.all()?;
- Ok(Self { block, children }.pack())
+ Ok(Self { block, body }.pack())
}
fn field(&self, name: &str) -> Option<Value> {
@@ -125,213 +158,74 @@ impl Layout for MathNode {
&self,
vt: &mut Vt,
styles: StyleChain,
- _: Regions,
+ regions: Regions,
) -> SourceResult<Fragment> {
- let mut t = Texifier::new(styles);
- self.texify(&mut t)?;
- Ok(layout_tex(vt, &t.finish(), self.block, styles)
- .unwrap_or(Fragment::frame(Frame::new(Size::zero()))))
- }
-}
-
-impl Inline for MathNode {}
-
-/// Turn a math node into TeX math code.
-#[capability]
-trait Texify {
- /// Perform the conversion.
- fn texify(&self, t: &mut Texifier) -> SourceResult<()>;
-
- /// Texify the node, but trim parentheses..
- fn texify_unparen(&self, t: &mut Texifier) -> SourceResult<()> {
- let s = {
- let mut sub = Texifier::new(t.styles);
- self.texify(&mut sub)?;
- sub.finish()
- };
-
- let unparened = if s.starts_with("\\left(") && s.ends_with("\\right)") {
- s[6..s.len() - 7].into()
- } else {
- s
+ // Find a math font.
+ let variant = variant(styles);
+ let world = vt.world();
+ let Some(font) = families(styles)
+ .find_map(|family| {
+ let id = world.book().select(family, variant)?;
+ let font = world.font(id)?;
+ let _ = font.ttf().tables().math?.constants?;
+ Some(font)
+ })
+ else {
+ return Ok(Fragment::frame(Frame::new(Size::zero())))
};
- t.push_str(&unparened);
- Ok(())
+ let mut ctx = MathContext::new(vt, styles, regions, &font, self.block);
+ let frame = ctx.layout_frame(self)?;
+ Ok(Fragment::frame(frame))
}
}
-/// Builds the TeX representation of the formula.
-struct Texifier<'a> {
- tex: EcoString,
- support: bool,
- space: bool,
- styles: StyleChain<'a>,
-}
-
-impl<'a> Texifier<'a> {
- /// Create a new texifier.
- fn new(styles: StyleChain<'a>) -> Self {
- Self {
- tex: EcoString::new(),
- support: false,
- space: false,
- styles,
- }
- }
-
- /// Finish texifier and return the TeX string.
- fn finish(self) -> EcoString {
- self.tex
- }
-
- /// Push a weak space.
- fn push_space(&mut self) {
- self.space = !self.tex.is_empty();
- }
-
- /// Mark this position as supportive. This allows a space before or after
- /// to exist.
- fn support(&mut self) {
- self.support = true;
- }
-
- /// Flush a space.
- fn flush(&mut self) {
- if self.space && self.support {
- self.tex.push_str("\\ ");
- }
-
- self.space = false;
- self.support = false;
- }
-
- /// Push a string.
- fn push_str(&mut self, s: &str) {
- self.flush();
- self.tex.push_str(s);
- }
+impl Inline for MathNode {}
- /// Escape and push a char for TeX usage.
- #[rustfmt::skip]
- fn push_escaped(&mut self, c: char) {
- self.flush();
- match c {
- ' ' => self.tex.push_str("\\ "),
- '%' | '&' | '$' | '#' => {
- self.tex.push('\\');
- self.tex.push(c);
- self.tex.push(' ');
- }
- '{' => self.tex.push_str("\\left\\{"),
- '}' => self.tex.push_str("\\right\\}"),
- '[' | '(' => {
- self.tex.push_str("\\left");
- self.tex.push(c);
- }
- ']' | ')' => {
- self.tex.push_str("\\right");
- self.tex.push(c);
- }
- 'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' |
- '*' | '+' | '-' | '?' | '!' | '=' | '<' | '>' |
- ':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => self.tex.push(c),
- c => {
- if let Some(sym) = unicode_math::SYMBOLS
- .iter()
- .find(|sym| sym.codepoint == c) {
- self.tex.push('\\');
- self.tex.push_str(sym.name);
- self.tex.push(' ');
- }
- }
- }
- }
+#[capability]
+trait LayoutMath {
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>;
}
-impl Texify for MathNode {
- fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
- for child in &self.children {
- child.texify(t)?;
- }
- Ok(())
+impl LayoutMath for MathNode {
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ self.body.layout_math(ctx)
}
}
-impl Texify for Content {
- fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
+impl LayoutMath for Content {
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
if self.is::<SpaceNode>() {
- t.push_space();
return Ok(());
}
if self.is::<LinebreakNode>() {
- t.push_str("\\");
+ ctx.push(MathFragment::Linebreak);
return Ok(());
}
if let Some(node) = self.to::<SymbolNode>() {
if let Some(c) = symmie::get(&node.0) {
- t.push_escaped(c);
- return Ok(());
+ return AtomNode(c.into()).layout_math(ctx);
} else if let Some(span) = self.span() {
bail!(span, "unknown symbol");
}
}
- if let Some(node) = self.to::<TextNode>() {
- t.support();
- t.push_str("\\mathrm{");
- for c in node.0.chars() {
- t.push_escaped(c);
- }
- t.push_str("}");
- t.support();
- return Ok(());
- }
-
if let Some(node) = self.to::<SequenceNode>() {
for child in &node.0 {
- child.texify(t)?;
+ child.layout_math(ctx)?;
}
return Ok(());
}
- if let Some(node) = self.with::<dyn Texify>() {
- return node.texify(t);
- }
-
- if let Some(span) = self.span() {
- bail!(span, "not allowed here");
+ if let Some(node) = self.with::<dyn LayoutMath>() {
+ return node.layout_math(ctx);
}
- Ok(())
- }
-}
-
-/// # Alignment Point
-/// A math alignment point: `&`, `&&`.
-///
-/// ## Parameters
-/// - index: usize (positional, required)
-/// The alignment point's index.
-///
-/// ## Category
-/// math
-#[func]
-#[capable(Texify)]
-#[derive(Debug, Hash)]
-pub struct AlignPointNode;
-
-#[node]
-impl AlignPointNode {
- fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
- Ok(Self.pack())
- }
-}
+ let frame = ctx.layout_non_math(self)?;
+ ctx.push(frame);
-impl Texify for AlignPointNode {
- fn texify(&self, _: &mut Texifier) -> SourceResult<()> {
Ok(())
}
}
diff --git a/library/src/math/row.rs b/library/src/math/row.rs
new file mode 100644
index 00000000..f7b2b384
--- /dev/null
+++ b/library/src/math/row.rs
@@ -0,0 +1,118 @@
+use super::*;
+
+#[derive(Debug, Default, Clone)]
+pub(super) struct MathRow(pub Vec<MathFragment>);
+
+impl MathRow {
+ pub fn new() -> Self {
+ Self(vec![])
+ }
+
+ pub fn width(&self) -> Abs {
+ self.0.iter().map(|fragment| fragment.width()).sum()
+ }
+
+ pub fn push(
+ &mut self,
+ font_size: Abs,
+ style: MathStyle,
+ fragment: impl Into<MathFragment>,
+ ) {
+ let fragment = fragment.into();
+ if let Some(fragment_class) = fragment.class() {
+ for (i, prev) in self.0.iter().enumerate().rev() {
+ if matches!(prev, MathFragment::Align) {
+ continue;
+ }
+
+ let mut amount = Abs::zero();
+ if let MathFragment::Glyph(glyph) = *prev {
+ if !glyph.italics_correction.is_zero()
+ && fragment_class != MathClass::Alphabetic
+ {
+ amount += glyph.italics_correction;
+ }
+ }
+
+ if let Some(prev_class) = prev.class() {
+ amount += spacing(prev_class, fragment_class, style).at(font_size);
+ }
+
+ if !amount.is_zero() {
+ self.0.insert(i + 1, MathFragment::Spacing(amount));
+ }
+
+ break;
+ }
+ }
+ self.0.push(fragment);
+ }
+
+ pub fn to_frame(mut self, ctx: &MathContext) -> Frame {
+ if self.0.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) {
+ let mut frame = Frame::new(Size::zero());
+ let fragments = std::mem::take(&mut self.0);
+
+ let leading = ctx.outer.chain(&ctx.map).get(ParNode::LEADING);
+ let rows: Vec<_> = fragments
+ .split(|frag| matches!(frag, MathFragment::Linebreak))
+ .map(|slice| Self(slice.to_vec()))
+ .collect();
+
+ let points = alignments(&rows);
+ for (i, row) in rows.into_iter().enumerate() {
+ let size = frame.size_mut();
+ let sub = row.to_line_frame(ctx, &points);
+ if i > 0 {
+ size.y += leading;
+ }
+ let pos = Point::with_y(size.y);
+ size.y += sub.height();
+ size.x.set_max(sub.width());
+ frame.push_frame(pos, sub);
+ }
+ frame
+ } else {
+ self.to_line_frame(ctx, &[])
+ }
+ }
+
+ pub fn to_line_frame(self, ctx: &MathContext, points: &[Abs]) -> Frame {
+ let ascent = self.0.iter().map(MathFragment::ascent).max().unwrap_or_default();
+ let descent = self.0.iter().map(MathFragment::descent).max().unwrap_or_default();
+
+ let size = Size::new(Abs::zero(), ascent + descent);
+ let mut frame = Frame::new(size);
+ let mut x = Abs::zero();
+ frame.set_baseline(ascent);
+
+ let mut fragments = self.0.into_iter().peekable();
+ let mut i = 0;
+ while let Some(fragment) = fragments.next() {
+ if matches!(fragment, MathFragment::Align) {
+ if let Some(&point) = points.get(i) {
+ x = point;
+ }
+ i += 1;
+ continue;
+ }
+
+ let y = ascent - fragment.ascent();
+ let pos = Point::new(x, y);
+ x += fragment.width();
+ frame.push_frame(pos, fragment.to_frame(ctx));
+ }
+
+ frame.size_mut().x = x;
+ frame
+ }
+}
+
+impl<T> From<T> for MathRow
+where
+ T: Into<MathFragment>,
+{
+ fn from(fragment: T) -> Self {
+ Self(vec![fragment.into()])
+ }
+}
diff --git a/src/doc.rs b/src/doc.rs
index 9e98ec88..d4ad2d93 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -7,7 +7,8 @@ use std::sync::Arc;
use crate::font::Font;
use crate::geom::{
- Abs, Align, Axes, Dir, Em, Numeric, Paint, Point, Shape, Size, Transform,
+ self, Abs, Align, Axes, Color, Dir, Em, Geometry, Numeric, Paint, Point, RgbaColor,
+ Shape, Size, Stroke, Transform,
};
use crate::image::Image;
use crate::model::{
@@ -160,7 +161,7 @@ impl Frame {
self.size.y
}
- /// The baseline of the frame.
+ /// The vertical position of the frame's baseline.
pub fn baseline(&self) -> Abs {
self.baseline.unwrap_or(self.size.y)
}
@@ -170,6 +171,19 @@ impl Frame {
self.baseline = Some(baseline);
}
+ /// The distance from the baseline to the top of the frame.
+ ///
+ /// This is the same as `baseline()`, but more in line with the terminology
+ /// used in math layout.
+ pub fn ascent(&self) -> Abs {
+ self.baseline()
+ }
+
+ /// The distance from the baseline to the bottom of the frame.
+ pub fn descent(&self) -> Abs {
+ self.size.y - self.baseline()
+ }
+
/// An iterator over the elements inside this frame alongside their
/// positions relative to the top-left of the frame.
pub fn elements(&self) -> std::slice::Iter<'_, (Point, Element)> {
diff --git a/src/model/styles.rs b/src/model/styles.rs
index e78d83cd..27c40309 100644
--- a/src/model/styles.rs
+++ b/src/model/styles.rs
@@ -48,6 +48,11 @@ impl StyleMap {
}
}
+ /// Remove the style that was last set.
+ pub fn unset(&mut self) {
+ self.0.pop();
+ }
+
/// Whether the map contains a style property for the given key.
pub fn contains<K: Key>(&self, _: K) -> bool {
self.0