From 7e295d84b55322e84695e793af8d64b6ec89e357 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 22 Jan 2023 13:32:58 +0100 Subject: [PATCH] Math delimiter grouping --- library/src/lib.rs | 1 + library/src/math/lr.rs | 78 +++++++++++++++++++++++++++++++++++++++++ library/src/math/mod.rs | 2 ++ src/ide/highlight.rs | 1 + src/model/eval.rs | 74 +++++++++++++++++++++++++++++++++----- src/model/library.rs | 5 ++- src/syntax/ast.rs | 24 ++++++++++--- src/syntax/kind.rs | 4 +++ src/syntax/parser.rs | 43 +++++++++++++++-------- 9 files changed, 204 insertions(+), 28 deletions(-) create mode 100644 library/src/math/lr.rs diff --git a/library/src/lib.rs b/library/src/lib.rs index f714c72be..c2f51ee40 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -189,6 +189,7 @@ fn items() -> LangItems { }, math: |body, block| math::MathNode { body, block }.pack(), math_atom: |atom| math::AtomNode(atom).pack(), + math_delimited: |body| math::LrNode(body).pack(), math_script: |base, sub, sup| math::ScriptNode { base, sub, sup }.pack(), math_frac: |num, denom| math::FracNode { num, denom }.pack(), math_align_point: || math::AlignPointNode.pack(), diff --git a/library/src/math/lr.rs b/library/src/math/lr.rs new file mode 100644 index 000000000..9cfc6e5fb --- /dev/null +++ b/library/src/math/lr.rs @@ -0,0 +1,78 @@ +use super::*; + +/// How much less high scaled delimiters can be than what they wrap. +const DELIM_SHORT_FALL: Em = Em::new(0.1); + +/// # Left-Right +/// Scales delimiters. +/// +/// While matched delimiters scale by default, this can be used to scale +/// unmatched delimiters and to control the delimiter scaling more precisely. +/// +/// ## Example +/// ``` +/// $ lr(]a, b/2]) $ +/// ``` +/// +/// ## Parameters +/// - body: Content (positional, variadic) +/// The delimited content, including the delimiters. +/// +/// ## Category +/// math +#[func] +#[capable(LayoutMath)] +#[derive(Debug, Hash)] +pub struct LrNode(pub Content); + +#[node] +impl LrNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + let mut body = Content::empty(); + for (i, arg) in args.all::()?.into_iter().enumerate() { + if i > 0 { + body += AtomNode(','.into()).pack(); + } + body += arg; + } + Ok(Self(body).pack()) + } +} + +impl LayoutMath for LrNode { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let mut row = ctx.layout_row(&self.0)?; + + let axis = scaled!(ctx, axis_height); + let max_extent = row + .0 + .iter() + .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) + .max() + .unwrap_or_default(); + + let height = 2.0 * max_extent; + if let [first, .., last] = row.0.as_mut_slice() { + for fragment in [first, last] { + if !matches!( + fragment.class(), + Some(MathClass::Opening | MathClass::Closing | MathClass::Fence) + ) { + continue; + } + + let MathFragment::Glyph(glyph) = *fragment else { continue }; + let short_fall = DELIM_SHORT_FALL.at(glyph.font_size); + *fragment = MathFragment::Variant( + glyph.stretch_vertical(ctx, height, short_fall), + ); + } + } + + for fragment in row.0 { + ctx.push(fragment); + } + + Ok(()) + } +} diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index ae6604055..44b52e961 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -8,6 +8,7 @@ mod atom; mod braced; mod frac; mod fragment; +mod lr; mod matrix; mod op; mod root; @@ -50,6 +51,7 @@ use crate::text::{families, variant, FallbackList, FontFamily, SpaceNode, Symbol /// Hook up all math definitions. pub fn define(scope: &mut Scope) { scope.def_func::("math"); + scope.def_func::("lr"); scope.def_func::("accent"); scope.def_func::("frac"); scope.def_func::("binom"); diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs index 42c050028..5f615d041 100644 --- a/src/ide/highlight.rs +++ b/src/ide/highlight.rs @@ -115,6 +115,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::TermMarker => Some(Category::ListMarker), SyntaxKind::Math => None, SyntaxKind::Atom => None, + SyntaxKind::Delimited => None, SyntaxKind::Script => None, SyntaxKind::Frac => None, SyntaxKind::AlignPoint => Some(Category::MathOperator), diff --git a/src/model/eval.rs b/src/model/eval.rs index b037a1bd7..911129495 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -280,6 +280,7 @@ impl Eval for ast::Expr { Self::Enum(v) => v.eval(vm).map(Value::Content), Self::Term(v) => v.eval(vm).map(Value::Content), Self::Atom(v) => v.eval(vm).map(Value::Content), + Self::Delimited(v) => v.eval(vm).map(Value::Content), Self::Script(v) => v.eval(vm).map(Value::Content), Self::Frac(v) => v.eval(vm).map(Value::Content), Self::AlignPoint(v) => v.eval(vm).map(Value::Content), @@ -325,10 +326,19 @@ impl ast::Expr { Self::Shorthand(v) => v.eval_in_math(vm)?, Self::Symbol(v) => v.eval_in_math(vm)?, Self::Ident(v) => v.eval_in_math(vm)?, + Self::FuncCall(v) => v.eval_in_math(vm)?, _ => self.eval(vm)?.display_in_math(), } .spanned(self.span())) } + + fn eval_without_parens(&self, vm: &mut Vm) -> SourceResult { + Ok(match self { + Self::Delimited(v) => v.eval_without_parens(vm)?, + _ => self.eval_in_math(vm)?, + } + .spanned(self.span())) + } } impl Eval for ast::Text { @@ -401,7 +411,7 @@ impl Eval for ast::Symbol { impl ast::Symbol { fn eval_in_math(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.symbol)(EcoString::from(self.get()) + ":op".into())) + Ok((vm.items.symbol)(EcoString::from(self.get()) + ":op:square".into())) } } @@ -511,7 +521,7 @@ impl Eval for ast::Math { .map(|expr| expr.eval_in_math(vm)) .collect::>()?; let block = self.block(); - Ok((vm.items.math)(seq, block)) + Ok((vm.items.math)(Content::sequence(seq), block)) } } @@ -523,13 +533,44 @@ impl Eval for ast::Atom { } } +impl Eval for ast::Delimited { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult { + let seq = self + .exprs() + .map(|expr| expr.eval_in_math(vm)) + .collect::>()?; + Ok((vm.items.math_delimited)(Content::sequence(seq))) + } +} + +impl ast::Delimited { + fn eval_without_parens(&self, vm: &mut Vm) -> SourceResult { + let exprs: Vec<_> = self.exprs().collect(); + let mut slice = exprs.as_slice(); + if let (Some(ast::Expr::Atom(first)), Some(ast::Expr::Atom(last))) = + (exprs.first(), exprs.last()) + { + if first.get() == "(" && last.get() == ")" { + slice = &exprs[1..exprs.len() - 1]; + } + } + let seq = slice + .iter() + .map(|expr| expr.eval_in_math(vm)) + .collect::>()?; + Ok((vm.items.math_delimited)(Content::sequence(seq))) + } +} + impl Eval for ast::Script { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { let base = self.base().eval_in_math(vm)?; - let sub = self.sub().map(|expr| expr.eval_in_math(vm)).transpose()?; - let sup = self.sup().map(|expr| expr.eval_in_math(vm)).transpose()?; + let sub = self.sub().map(|expr| expr.eval_without_parens(vm)).transpose()?; + let sup = self.sup().map(|expr| expr.eval_without_parens(vm)).transpose()?; Ok((vm.items.math_script)(base, sub, sup)) } } @@ -538,8 +579,8 @@ impl Eval for ast::Frac { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - let num = self.num().eval_in_math(vm)?; - let denom = self.denom().eval_in_math(vm)?; + let num = self.num().eval_without_parens(vm)?; + let denom = self.denom().eval_without_parens(vm)?; Ok((vm.items.math_frac)(num, denom)) } } @@ -878,12 +919,29 @@ impl Eval for ast::FuncCall { type Output = Value; fn eval(&self, vm: &mut Vm) -> SourceResult { + let callee = self.callee(); + let callee = callee.eval(vm)?.cast::().at(callee.span())?; + self.eval_with_callee(vm, callee) + } +} + +impl ast::FuncCall { + fn eval_in_math(&self, vm: &mut Vm) -> SourceResult { + let callee = self.callee().eval(vm)?; + if let Value::Func(callee) = callee { + Ok(self.eval_with_callee(vm, callee)?.display_in_math()) + } else { + Ok(callee.display_in_math() + + (vm.items.math_atom)("(".into()) + + (vm.items.math_atom)(")".into())) + } + } + + fn eval_with_callee(&self, vm: &mut Vm, callee: Func) -> SourceResult { if vm.depth >= MAX_CALL_DEPTH { bail!(self.span(), "maximum function call depth exceeded"); } - let callee = self.callee(); - let callee = callee.eval(vm)?.cast::().at(callee.span())?; let args = self.args().eval(vm)?; let point = || Tracepoint::Call(callee.name().map(Into::into)); callee.call(vm, args).trace(vm.world, point, self.span()) diff --git a/src/model/library.rs b/src/model/library.rs index 96218bb10..54eeeb5b8 100644 --- a/src/model/library.rs +++ b/src/model/library.rs @@ -66,7 +66,10 @@ pub struct LangItems { /// An item in a term list: `/ Term: Details`. pub term_item: fn(term: Content, description: Content) -> Content, /// A mathematical formula: `$x$`, `$ x^2 $`. - pub math: fn(children: Vec, block: bool) -> Content, + pub math: fn(body: Content, block: bool) -> Content, + /// A subsection in a math formula that is surrounded by matched delimiters: + /// `[x + y]`. + pub math_delimited: fn(body: Content) -> Content, /// An atom in a formula: `x`, `+`, `12`. pub math_atom: fn(atom: EcoString) -> Content, /// A base with optional sub- and superscripts in a formula: `a_1^2`. diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index d70c4ae40..ceda2d57c 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -117,6 +117,9 @@ pub enum Expr { Math(Math), /// An atom in a math formula: `x`, `+`, `12`. Atom(Atom), + /// A subsection in a math formula that is surrounded by matched delimiters: + /// `[x + y]`. + Delimited(Delimited), /// A base with optional sub- and superscripts in a math formula: `a_1^2`. Script(Script), /// A fraction in a math formula: `x/2`. @@ -216,6 +219,7 @@ impl AstNode for Expr { SyntaxKind::TermItem => node.cast().map(Self::Term), SyntaxKind::Math => node.cast().map(Self::Math), SyntaxKind::Atom => node.cast().map(Self::Atom), + SyntaxKind::Delimited => node.cast().map(Self::Delimited), SyntaxKind::Script => node.cast().map(Self::Script), SyntaxKind::Frac => node.cast().map(Self::Frac), SyntaxKind::AlignPoint => node.cast().map(Self::AlignPoint), @@ -275,6 +279,7 @@ impl AstNode for Expr { Self::Term(v) => v.as_untyped(), Self::Math(v) => v.as_untyped(), Self::Atom(v) => v.as_untyped(), + Self::Delimited(v) => v.as_untyped(), Self::Script(v) => v.as_untyped(), Self::Frac(v) => v.as_untyped(), Self::AlignPoint(v) => v.as_untyped(), @@ -657,6 +662,19 @@ impl Atom { } } +node! { + /// A subsection in a math formula that is surrounded by matched delimiters: + /// `[x + y]`. + Delimited +} + +impl Delimited { + /// The contents, including the delimiters. + pub fn exprs(&self) -> impl DoubleEndedIterator + '_ { + self.0.children().filter_map(Expr::cast_with_space) + } +} + node! { /// A base with an optional sub- and superscript in a formula: `a_1^2`. Script @@ -673,8 +691,7 @@ impl Script { self.0 .children() .skip_while(|node| !matches!(node.kind(), SyntaxKind::Underscore)) - .nth(1) - .map(|node| node.cast().expect("script node has invalid subscript")) + .find_map(SyntaxNode::cast) } /// The superscript. @@ -682,8 +699,7 @@ impl Script { self.0 .children() .skip_while(|node| !matches!(node.kind(), SyntaxKind::Hat)) - .nth(1) - .map(|node| node.cast().expect("script node has invalid superscript")) + .find_map(SyntaxNode::cast) } } diff --git a/src/syntax/kind.rs b/src/syntax/kind.rs index 5928fa0a6..206df9113 100644 --- a/src/syntax/kind.rs +++ b/src/syntax/kind.rs @@ -61,6 +61,9 @@ pub enum SyntaxKind { Math, /// An atom in math: `x`, `+`, `12`. Atom, + /// A subsection in a math formula that is surrounded by matched delimiters: + /// `[x + y]`. + Delimited, /// A base with optional sub- and superscripts in math: `a_1^2`. Script, /// A fraction in math: `x/2`. @@ -336,6 +339,7 @@ impl SyntaxKind { Self::TermItem => "term list item", Self::TermMarker => "term marker", Self::Math => "math formula", + Self::Delimited => "delimited math", Self::Atom => "math atom", Self::Script => "script", Self::Frac => "fraction", diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index a379500c9..15839e18b 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -1,6 +1,8 @@ use std::collections::HashSet; use std::ops::Range; +use unicode_math_class::MathClass; + use super::{ast, is_newline, ErrorPos, LexMode, Lexer, SyntaxKind, SyntaxNode}; use crate::util::{format_eco, EcoString}; @@ -233,12 +235,13 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { } } - SyntaxKind::Atom => match p.current_text() { - "(" => math_delimited(p, ")"), - "{" => math_delimited(p, "}"), - "[" => math_delimited(p, "]"), - _ => p.eat(), - }, + SyntaxKind::Atom if math_class(p.current_text()) == Some(MathClass::Fence) => { + math_delimited(p, MathClass::Fence) + } + + SyntaxKind::Atom if math_class(p.current_text()) == Some(MathClass::Opening) => { + math_delimited(p, MathClass::Closing) + } SyntaxKind::Let | SyntaxKind::Set @@ -254,7 +257,8 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { | SyntaxKind::LeftBrace | SyntaxKind::LeftBracket => embedded_code_expr(p), - SyntaxKind::Linebreak + SyntaxKind::Atom + | SyntaxKind::Linebreak | SyntaxKind::Escape | SyntaxKind::Shorthand | SyntaxKind::Symbol @@ -288,21 +292,30 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { } } -fn math_delimited(p: &mut Parser, closing: &str) { +fn math_delimited(p: &mut Parser, stop: MathClass) { let m = p.marker(); p.expect(SyntaxKind::Atom); - while !p.eof() - && !p.at(SyntaxKind::Dollar) - && (!p.at(SyntaxKind::Atom) || p.current_text() != closing) - { + while !p.eof() && !p.at(SyntaxKind::Dollar) { + if math_class(p.current_text()) == Some(stop) { + p.eat(); + p.wrap(m, SyntaxKind::Delimited); + return; + } + let prev = p.prev_end(); math_expr(p); if !p.progress(prev) { p.unexpected(); } } - p.expect(SyntaxKind::Atom); - p.wrap(m, SyntaxKind::Math); +} + +fn math_class(text: &str) -> Option { + let mut chars = text.chars(); + chars + .next() + .filter(|_| chars.next().is_none()) + .and_then(unicode_math_class::class) } fn math_op(kind: SyntaxKind) -> Option<(SyntaxKind, SyntaxKind, ast::Assoc, usize)> { @@ -324,7 +337,7 @@ fn math_args(p: &mut Parser) { p.expect(SyntaxKind::Atom); let m = p.marker(); let mut m2 = p.marker(); - while !p.eof() { + while !p.eof() && !p.at(SyntaxKind::Dollar) { match p.current_text() { ")" => break, "," => {