summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-01-22 13:32:58 +0100
committerLaurenz <laurmaedje@gmail.com>2023-01-22 13:32:58 +0100
commit7e295d84b55322e84695e793af8d64b6ec89e357 (patch)
tree4570ee01286e69846ed1be382e30d1d3b0ed9bed
parent953bdc1859f7acdbecbb7b819bc5b113a50849d0 (diff)
Math delimiter grouping
-rw-r--r--library/src/lib.rs1
-rw-r--r--library/src/math/lr.rs78
-rw-r--r--library/src/math/mod.rs2
-rw-r--r--src/ide/highlight.rs1
-rw-r--r--src/model/eval.rs74
-rw-r--r--src/model/library.rs5
-rw-r--r--src/syntax/ast.rs24
-rw-r--r--src/syntax/kind.rs4
-rw-r--r--src/syntax/parser.rs43
9 files changed, 204 insertions, 28 deletions
diff --git a/library/src/lib.rs b/library/src/lib.rs
index f714c72b..c2f51ee4 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 00000000..9cfc6e5f
--- /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<Content> {
+ let mut body = Content::empty();
+ for (i, arg) in args.all::<Content>()?.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 ae660405..44b52e96 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::<MathNode>("math");
+ scope.def_func::<LrNode>("lr");
scope.def_func::<AccentNode>("accent");
scope.def_func::<FracNode>("frac");
scope.def_func::<BinomNode>("binom");
diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs
index 42c05002..5f615d04 100644
--- a/src/ide/highlight.rs
+++ b/src/ide/highlight.rs
@@ -115,6 +115,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
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 b037a1bd..91112949 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<Content> {
+ 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<Content> {
- 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::<SourceResult<_>>()?;
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<Self::Output> {
+ let seq = self
+ .exprs()
+ .map(|expr| expr.eval_in_math(vm))
+ .collect::<SourceResult<_>>()?;
+ Ok((vm.items.math_delimited)(Content::sequence(seq)))
+ }
+}
+
+impl ast::Delimited {
+ fn eval_without_parens(&self, vm: &mut Vm) -> SourceResult<Content> {
+ 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::<SourceResult<_>>()?;
+ Ok((vm.items.math_delimited)(Content::sequence(seq)))
+ }
+}
+
impl Eval for ast::Script {
type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
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<Self::Output> {
- 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<Self::Output> {
+ let callee = self.callee();
+ let callee = callee.eval(vm)?.cast::<Func>().at(callee.span())?;
+ self.eval_with_callee(vm, callee)
+ }
+}
+
+impl ast::FuncCall {
+ fn eval_in_math(&self, vm: &mut Vm) -> SourceResult<Content> {
+ 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<Value> {
if vm.depth >= MAX_CALL_DEPTH {
bail!(self.span(), "maximum function call depth exceeded");
}
- let callee = self.callee();
- let callee = callee.eval(vm)?.cast::<Func>().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 96218bb1..54eeeb5b 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<Content>, 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 d70c4ae4..ceda2d57 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(),
@@ -658,6 +663,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<Item = Expr> + '_ {
+ 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 5928fa0a..206df911 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 a379500c..15839e18 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<MathClass> {
+ 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,
"," => {