diff --git a/Cargo.lock b/Cargo.lock index 8fe3dba2e..9de15487b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1027,7 +1027,7 @@ dependencies = [ [[package]] name = "symmie" version = "0.1.0" -source = "git+https://github.com/typst/symmie#8504bf7ec0d8996d160832c2724ba024ab6e988a" +source = "git+https://github.com/typst/symmie#6280fb20455cb63e6886ba5bb35b95a4b376da68" [[package]] name = "syn" diff --git a/library/src/lib.rs b/library/src/lib.rs index 29a6cc949..e41e7c0d9 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -51,10 +51,22 @@ fn scope() -> Scope { // Math. std.def_node::("math"); std.def_node::("atom"); + std.def_node::("acc"); std.def_node::("frac"); + std.def_node::("binom"); std.def_node::("sqrt"); + std.def_node::("floor"); + std.def_node::("ceil"); std.def_node::("vec"); std.def_node::("cases"); + std.def_node::("serif"); + std.def_node::("sans"); + std.def_node::("bold"); + std.def_node::("ital"); + std.def_node::("cal"); + std.def_node::("frak"); + std.def_node::("mono"); + std.def_node::("bb"); // Layout. std.def_node::("page"); diff --git a/library/src/math/matrix.rs b/library/src/math/matrix.rs new file mode 100644 index 000000000..d835b3485 --- /dev/null +++ b/library/src/math/matrix.rs @@ -0,0 +1,84 @@ +use super::*; + +/// A column vector in a mathematical formula. +#[derive(Debug, Hash)] +pub struct VecNode(Vec); + +#[node(Texify)] +impl VecNode { + /// The kind of delimiter. + pub const DELIM: Delimiter = Delimiter::Paren; + + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.all()?).pack()) + } +} + +impl Texify for VecNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + let kind = match t.styles.get(Self::DELIM) { + Delimiter::Paren => "pmatrix", + Delimiter::Bracket => "bmatrix", + Delimiter::Brace => "Bmatrix", + Delimiter::Bar => "vmatrix", + }; + + t.push_str("\\begin{"); + t.push_str(kind); + t.push_str("}"); + + for component in &self.0 { + component.texify(t)?; + t.push_str("\\\\"); + } + t.push_str("\\end{"); + t.push_str(kind); + t.push_str("}"); + + Ok(()) + } +} + +/// A vector / matrix delimiter. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Delimiter { + Paren, + Bracket, + Brace, + Bar, +} + +castable! { + Delimiter, + Expected: "type of bracket or bar", + Value::Str(s) => match s.as_str() { + "(" => Self::Paren, + "[" => Self::Bracket, + "{" => Self::Brace, + "|" => Self::Bar, + _ => Err("expected \"(\", \"[\", \"{\", or \"|\"")?, + }, +} + +/// A case distinction in a mathematical formula. +#[derive(Debug, Hash)] +pub struct CasesNode(Vec); + +#[node(Texify)] +impl CasesNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.all()?).pack()) + } +} + +impl Texify for CasesNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\begin{cases}"); + for component in &self.0 { + component.texify(t)?; + t.push_str("\\\\"); + } + t.push_str("\\end{cases}"); + Ok(()) + } +} diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index c613ea2a9..4eb72e23b 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -1,7 +1,12 @@ //! Mathematical formulas. +mod matrix; +mod style; mod tex; +pub use self::matrix::*; +pub use self::style::*; + use typst::model::{Guard, SequenceNode}; use unicode_segmentation::UnicodeSegmentation; @@ -272,6 +277,79 @@ impl Texify for AtomNode { } } +/// An accented node. +#[derive(Debug, Hash)] +pub struct AccNode { + /// The accent base. + pub base: Content, + /// The Unicode accent character. + pub accent: char, +} + +#[node(Texify)] +impl AccNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + let base = args.expect("base")?; + let Spanned { v, span } = args.expect::>("accent")?; + let accent = match extract(&v) { + Some(Ok(c)) => c, + Some(Err(msg)) => bail!(span, "{}", msg), + None => bail!(span, "not an accent"), + }; + Ok(Self { base, accent }.pack()) + } +} + +#[rustfmt::skip] +fn extract(content: &Content) -> Option> { + let MathNode { children, .. } = content.to::()?; + let [child] = children.as_slice() else { return None }; + let c = if let Some(atom) = child.to::() { + let mut chars = atom.0.chars(); + chars.next().filter(|_| chars.next().is_none())? + } else if let Some(symbol) = child.to::() { + match symmie::get(&symbol.0) { + Some(c) => c, + None => return Some(Err("unknown symbol")), + } + } else { + return None; + }; + + Some(Ok(match c { + '`' | '\u{300}' => '\u{300}', // Grave + '´' | '\u{301}' => '\u{301}', // Acute + '^' | '\u{302}' => '\u{302}', // Circumflex + '~' | '\u{223C}' | '\u{303}' => '\u{303}', // Tilde + '¯' | '\u{304}' => '\u{304}', // Macron + '‾' | '\u{305}' => '\u{305}', // Overline + '˘' | '\u{306}' => '\u{306}', // Breve + '.' | '\u{22C5}' | '\u{307}' => '\u{307}', // Dot + '¨' | '\u{308}' => '\u{308}', // Diaeresis + 'ˇ' | '\u{30C}' => '\u{30C}', // Caron + '→' | '\u{20D7}' => '\u{20D7}', // Arrow + _ => return None, + })) +} + +impl Texify for AccNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + if let Some(sym) = unicode_math::SYMBOLS.iter().find(|sym| { + sym.codepoint == self.accent + && sym.atom_type == unicode_math::AtomType::Accent + }) { + t.push_str("\\"); + t.push_str(sym.name); + t.push_str("{"); + self.base.texify(t)?; + t.push_str("}"); + } else { + self.base.texify(t)?; + } + Ok(()) + } +} + /// A fraction in a mathematical formula. #[derive(Debug, Hash)] pub struct FracNode { @@ -301,6 +379,35 @@ impl Texify for FracNode { } } +/// A binomial in a mathematical formula. +#[derive(Debug, Hash)] +pub struct BinomNode { + /// The upper index. + pub upper: Content, + /// The lower index. + pub lower: Content, +} + +#[node(Texify)] +impl BinomNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + let upper = args.expect("upper index")?; + let lower = args.expect("lower index")?; + Ok(Self { upper, lower }.pack()) + } +} + +impl Texify for BinomNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\binom{"); + self.upper.texify(t)?; + t.push_str("}{"); + self.lower.texify(t)?; + t.push_str("}"); + Ok(()) + } +} + /// A sub- and/or superscript in a mathematical formula. #[derive(Debug, Hash)] pub struct ScriptNode { @@ -348,7 +455,7 @@ impl Texify for AlignNode { } } -/// A square root. +/// A square root in a mathematical formula. #[derive(Debug, Hash)] pub struct SqrtNode(Content); @@ -362,91 +469,48 @@ impl SqrtNode { impl Texify for SqrtNode { fn texify(&self, t: &mut Texifier) -> SourceResult<()> { t.push_str("\\sqrt{"); - self.0.texify_unparen(t)?; + self.0.texify(t)?; t.push_str("}"); Ok(()) } } -/// A column vector. +/// A floored expression in a mathematical formula. #[derive(Debug, Hash)] -pub struct VecNode(Vec); +pub struct FloorNode(Content); #[node(Texify)] -impl VecNode { - /// The kind of delimiter. - pub const DELIM: Delimiter = Delimiter::Paren; - +impl FloorNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.all()?).pack()) + Ok(Self(args.expect("body")?).pack()) } } -impl Texify for VecNode { +impl Texify for FloorNode { fn texify(&self, t: &mut Texifier) -> SourceResult<()> { - let kind = match t.styles.get(Self::DELIM) { - Delimiter::Paren => "pmatrix", - Delimiter::Bracket => "bmatrix", - Delimiter::Brace => "Bmatrix", - Delimiter::Bar => "vmatrix", - }; - - t.push_str("\\begin{"); - t.push_str(kind); - t.push_str("}"); - - for component in &self.0 { - component.texify_unparen(t)?; - t.push_str("\\\\"); - } - t.push_str("\\end{"); - t.push_str(kind); - t.push_str("}"); - + t.push_str("\\left\\lfloor "); + self.0.texify(t)?; + t.push_str("\\right\\rfloor "); Ok(()) } } -/// A vector / matrix delimiter. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Delimiter { - Paren, - Bracket, - Brace, - Bar, -} - -castable! { - Delimiter, - Expected: "type of bracket or bar", - Value::Str(s) => match s.as_str() { - "(" => Self::Paren, - "[" => Self::Bracket, - "{" => Self::Brace, - "|" => Self::Bar, - _ => Err("expected \"(\", \"[\", \"{\", or \"|\"")?, - }, -} - -/// A case distinction. +/// A ceiled expression in a mathematical formula. #[derive(Debug, Hash)] -pub struct CasesNode(Vec); +pub struct CeilNode(Content); #[node(Texify)] -impl CasesNode { +impl CeilNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.all()?).pack()) + Ok(Self(args.expect("body")?).pack()) } } -impl Texify for CasesNode { +impl Texify for CeilNode { fn texify(&self, t: &mut Texifier) -> SourceResult<()> { - t.push_str("\\begin{cases}"); - for component in &self.0 { - component.texify_unparen(t)?; - t.push_str("\\\\"); - } - t.push_str("\\end{cases}"); + t.push_str("\\left\\lceil "); + self.0.texify(t)?; + t.push_str("\\right\\rceil "); Ok(()) } } diff --git a/library/src/math/style.rs b/library/src/math/style.rs new file mode 100644 index 000000000..3db2e631b --- /dev/null +++ b/library/src/math/style.rs @@ -0,0 +1,161 @@ +use super::*; + +/// Serif (roman) font style. +#[derive(Debug, Hash)] +pub struct SerifNode(Content); + +#[node(Texify)] +impl SerifNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Texify for SerifNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\mathrm{"); + self.0.texify_unparen(t)?; + t.push_str("}"); + Ok(()) + } +} + +/// Sans-serif font style. +#[derive(Debug, Hash)] +pub struct SansNode(Content); + +#[node(Texify)] +impl SansNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Texify for SansNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\mathsf{"); + self.0.texify_unparen(t)?; + t.push_str("}"); + Ok(()) + } +} + +/// Bold font style. +#[derive(Debug, Hash)] +pub struct BoldNode(Content); + +#[node(Texify)] +impl BoldNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Texify for BoldNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\mathbf{"); + self.0.texify_unparen(t)?; + t.push_str("}"); + Ok(()) + } +} + +/// Italic font style. +#[derive(Debug, Hash)] +pub struct ItalNode(Content); + +#[node(Texify)] +impl ItalNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Texify for ItalNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\mathit{"); + self.0.texify_unparen(t)?; + t.push_str("}"); + Ok(()) + } +} + +/// Calligraphic font style. +#[derive(Debug, Hash)] +pub struct CalNode(Content); + +#[node(Texify)] +impl CalNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Texify for CalNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\mathcal{"); + self.0.texify_unparen(t)?; + t.push_str("}"); + Ok(()) + } +} + +/// Fraktur font style. +#[derive(Debug, Hash)] +pub struct FrakNode(Content); + +#[node(Texify)] +impl FrakNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Texify for FrakNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\mathfrak{"); + self.0.texify_unparen(t)?; + t.push_str("}"); + Ok(()) + } +} + +/// Monospace font style. +#[derive(Debug, Hash)] +pub struct MonoNode(Content); + +#[node(Texify)] +impl MonoNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Texify for MonoNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\mathtt{"); + self.0.texify_unparen(t)?; + t.push_str("}"); + Ok(()) + } +} + +/// Blackboard bold (double-struck) font style. +#[derive(Debug, Hash)] +pub struct BbNode(Content); + +#[node(Texify)] +impl BbNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Texify for BbNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\mathbb{"); + self.0.texify_unparen(t)?; + t.push_str("}"); + Ok(()) + } +} diff --git a/library/src/math/tex.rs b/library/src/math/tex.rs index e8917f309..f17134b79 100644 --- a/library/src/math/tex.rs +++ b/library/src/math/tex.rs @@ -125,6 +125,10 @@ impl Backend for FrameBackend { } fn rule(&mut self, pos: Cursor, width: f64, height: f64) { + if height == 0.0 { + return; + } + self.frame.push( self.transform(pos) + Point::with_y(Abs::pt(height) / 2.0), Element::Shape(Shape { diff --git a/src/model/eval.rs b/src/model/eval.rs index 204dfbd46..0ae8a0b1d 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -434,7 +434,7 @@ impl Eval for ast::MathNode { Self::Expr(v) => { if let ast::Expr::Ident(ident) = v { if self.as_untyped().len() == ident.len() - && !vm.scopes.get(ident).is_ok() + && matches!(vm.scopes.get(ident), Ok(Value::Func(_)) | Err(_)) { let node = (vm.items.symbol)(ident.get().clone() + ":op".into()); return Ok(node.spanned(self.span())); diff --git a/src/model/realize.rs b/src/model/realize.rs index b4e38e304..862bc8b6f 100644 --- a/src/model/realize.rs +++ b/src/model/realize.rs @@ -7,6 +7,10 @@ pub fn applicable(target: &Content, styles: StyleChain) -> bool { return true; } + if target.has::() && target.is_pristine() { + return true; + } + // Find out how many recipes there are. let mut n = styles.recipes().count(); diff --git a/tests/ref/math/accents.png b/tests/ref/math/accents.png new file mode 100644 index 000000000..7e93d9b5e Binary files /dev/null and b/tests/ref/math/accents.png differ diff --git a/tests/ref/math/matrix.png b/tests/ref/math/matrix.png index ed763710a..3bd177152 100644 Binary files a/tests/ref/math/matrix.png and b/tests/ref/math/matrix.png differ diff --git a/tests/ref/math/simple.png b/tests/ref/math/simple.png index 6c7fde558..ebd55dcb4 100644 Binary files a/tests/ref/math/simple.png and b/tests/ref/math/simple.png differ diff --git a/tests/ref/math/style.png b/tests/ref/math/style.png new file mode 100644 index 000000000..8dc11b312 Binary files /dev/null and b/tests/ref/math/style.png differ diff --git a/tests/typ/math/accents.typ b/tests/typ/math/accents.typ new file mode 100644 index 000000000..04e6e7234 --- /dev/null +++ b/tests/typ/math/accents.typ @@ -0,0 +1,28 @@ +// Test math accents. + +--- +#set page(width: auto) + +$ acc(a,`), + acc(a,´), + acc(a,\^), + acc(a,~), + acc(a,¯), + acc(a,‾), + acc(a,˘), + acc(a,.), + acc(a,¨), + acc(a,ˇ), + acc(a,->) $ + +$ acc(a, grave), + acc(a, acute), + acc(a, circum), + acc(a, tilde), + acc(a, macron), + acc(a, overline), + acc(a, breve), + acc(a, dot), + acc(a, dia), + acc(a, caron), + acc(a, arrow) $ diff --git a/tests/typ/math/matrix.typ b/tests/typ/math/matrix.typ index ec84778c7..3d67800dd 100644 --- a/tests/typ/math/matrix.typ +++ b/tests/typ/math/matrix.typ @@ -4,12 +4,11 @@ $ v = vec(1, 2+3, 4) $ --- -#set vec(delim: "|") -$ vec(1, 2) $ +$ binom(n, 1) = 1/2 n (n-1) $ --- -// Error: 17-20 expected "(", "[", "{", or "|" -#set vec(delim: "%") +#set vec(delim: "|") +$ vec(1, 2) $ --- $ f(x, y) := cases( @@ -18,3 +17,11 @@ $ f(x, y) := cases( 3 "if" x "is even", 4 "else", ) $ + +--- +// Error: 17-20 expected "(", "[", "{", or "|" +#set vec(delim: "%") + +--- +// Error: 9-12 missing argument: lower index +$ binom(x^2) $ diff --git a/tests/typ/math/simple.typ b/tests/typ/math/simple.typ index 55a853cff..1b63cbfc9 100644 --- a/tests/typ/math/simple.typ +++ b/tests/typ/math/simple.typ @@ -11,9 +11,13 @@ $ a^2 + b^2 = c^2 $ Prove by induction: $ sum_(k=0)^n k = (n(n+1))/2 $ +--- +We know that: +$ floor(x/2) <= ceil(x/2) $ + --- // Test that blackboard style looks nice. -$ f: NN arrow RR $ +$ f: NN -> RR $ --- // Error: 1:3 expected dollar sign diff --git a/tests/typ/math/style.typ b/tests/typ/math/style.typ new file mode 100644 index 000000000..c9238a9ae --- /dev/null +++ b/tests/typ/math/style.typ @@ -0,0 +1,15 @@ +#let part = $ a b A B $ +#let kinds = (serif, sans, cal, frak, mono, bb) +#let modifiers = (v => v, ital, bold, v => ital(bold(v))) + +#let cells = ([:triangle:nested:], [--], [`ital`], [`bold`], [both]) +#for k in kinds { + cells.push(raw(repr(k).trim(""))) + for m in modifiers { + cells.push($ #m(#k(part)) $) + } +} + +#set page(width: auto) +#set par(align: center) +#table(columns: 1 + modifiers.len(), ..cells)