mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Math framework
This commit is contained in:
parent
a2d77e36ba
commit
83b6858146
@ -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(),
|
||||
|
169
library/src/math/ctx.rs
Normal file
169
library/src/math/ctx.rs
Normal file
@ -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)
|
||||
}
|
||||
}
|
239
library/src/math/fragment.rs
Normal file
239
library/src/math/fragment.rs
Normal file
@ -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))
|
||||
}
|
@ -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(())
|
||||
}
|
||||
}
|
@ -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 ttf_parser::GlyphId;
|
||||
use ttf_parser::Rect;
|
||||
use typst::font::Font;
|
||||
use typst::model::{Guard, Scope, SequenceNode};
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
use self::tex::layout_tex;
|
||||
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()))))
|
||||
// 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())))
|
||||
};
|
||||
|
||||
let mut ctx = MathContext::new(vt, styles, regions, &font, self.block);
|
||||
let frame = ctx.layout_frame(self)?;
|
||||
Ok(Fragment::frame(frame))
|
||||
}
|
||||
}
|
||||
|
||||
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<()>;
|
||||
trait LayoutMath {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> 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
|
||||
};
|
||||
|
||||
t.push_str(&unparened);
|
||||
Ok(())
|
||||
impl LayoutMath for MathNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
self.body.layout_math(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Texify for MathNode {
|
||||
fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
|
||||
for child in &self.children {
|
||||
child.texify(t)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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(node) = self.with::<dyn LayoutMath>() {
|
||||
return node.layout_math(ctx);
|
||||
}
|
||||
|
||||
if let Some(span) = self.span() {
|
||||
bail!(span, "not allowed here");
|
||||
}
|
||||
let frame = ctx.layout_non_math(self)?;
|
||||
ctx.push(frame);
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
impl Texify for AlignPointNode {
|
||||
fn texify(&self, _: &mut Texifier) -> SourceResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
118
library/src/math/row.rs
Normal file
118
library/src/math/row.rs
Normal file
@ -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()])
|
||||
}
|
||||
}
|
18
src/doc.rs
18
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)> {
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user