diff --git a/library/src/lib.rs b/library/src/lib.rs index 3701894d5..785390f4e 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -254,6 +254,9 @@ fn items() -> LangItems { math::AccentElem::new(base, math::Accent::new(accent)).pack() }, math_frac: |num, denom| math::FracElem::new(num, denom).pack(), + math_root: |index, radicand| { + math::RootElem::new(radicand).with_index(index).pack() + }, library_method: |vm, dynamic, method, args, span| { if let Some(counter) = dynamic.downcast::().cloned() { counter.call_method(vm, method, args, span) diff --git a/library/src/math/root.rs b/library/src/math/root.rs index 8b272160a..7e00c45a8 100644 --- a/library/src/math/root.rs +++ b/library/src/math/root.rs @@ -31,11 +31,11 @@ pub fn sqrt( pub struct RootElem { /// Which root of the radicand to take. #[positional] - index: Option, + pub index: Option, /// The expression to take the root of. #[required] - radicand: Content, + pub radicand: Content, } impl LayoutMath for RootElem { diff --git a/src/eval/library.rs b/src/eval/library.rs index 13825d7ee..25f715f8b 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -103,6 +103,8 @@ pub struct LangItems { pub math_accent: fn(base: Content, accent: char) -> Content, /// A fraction in math: `x/2`. pub math_frac: fn(num: Content, denom: Content) -> Content, + /// A root in math: `√x`, `∛x` or `∜x`. + pub math_root: fn(index: Option, radicand: Content) -> Content, /// Dispatch a method on a library value. pub library_method: fn( vm: &mut Vm, @@ -134,9 +136,12 @@ impl Hash for LangItems { self.strong.hash(state); self.emph.hash(state); self.raw.hash(state); + self.raw_languages.hash(state); self.link.hash(state); self.reference.hash(state); + (self.bibliography_keys as usize).hash(state); self.heading.hash(state); + self.heading_func.hash(state); self.list_item.hash(state); self.enum_item.hash(state); self.term_item.hash(state); @@ -146,6 +151,8 @@ impl Hash for LangItems { self.math_attach.hash(state); self.math_accent.hash(state); self.math_frac.hash(state); + self.math_root.hash(state); + (self.library_method as usize).hash(state); } } diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 2719c2989..fe1fac3b8 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -438,6 +438,7 @@ impl Eval for ast::Expr { Self::MathDelimited(v) => v.eval(vm).map(Value::Content), Self::MathAttach(v) => v.eval(vm).map(Value::Content), Self::MathFrac(v) => v.eval(vm).map(Value::Content), + Self::MathRoot(v) => v.eval(vm).map(Value::Content), Self::Ident(v) => v.eval(vm), Self::None(v) => v.eval(vm), Self::Auto(v) => v.eval(vm), @@ -726,6 +727,16 @@ impl Eval for ast::MathFrac { } } +impl Eval for ast::MathRoot { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult { + let index = self.index().map(|i| (vm.items.text)(eco_format!("{i}"))); + let radicand = self.radicand().eval_display(vm)?; + Ok((vm.items.math_root)(index, radicand)) + } +} + impl Eval for ast::Ident { type Output = Value; diff --git a/src/ide/complete.rs b/src/ide/complete.rs index 0c80c8df7..e20229a60 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -701,6 +701,7 @@ fn complete_code(ctx: &mut CompletionContext) -> bool { | Some(SyntaxKind::Math) | Some(SyntaxKind::MathFrac) | Some(SyntaxKind::MathAttach) + | Some(SyntaxKind::MathRoot) ) { return false; } diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs index b7b063a6e..2db636e32 100644 --- a/src/ide/highlight.rs +++ b/src/ide/highlight.rs @@ -146,6 +146,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::MathDelimited => None, SyntaxKind::MathAttach => None, SyntaxKind::MathFrac => None, + SyntaxKind::MathRoot => None, SyntaxKind::Hashtag => highlight_hashtag(node), SyntaxKind::LeftBrace => Some(Tag::Punctuation), @@ -190,6 +191,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::SlashEq => Some(Tag::Operator), SyntaxKind::Dots => Some(Tag::Operator), SyntaxKind::Arrow => Some(Tag::Operator), + SyntaxKind::Root => Some(Tag::MathOperator), SyntaxKind::Not => Some(Tag::Keyword), SyntaxKind::And => Some(Tag::Keyword), diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 53e92949b..6e55d1064 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -126,6 +126,8 @@ pub enum Expr { MathAttach(MathAttach), /// A fraction in math: `x/2`. MathFrac(MathFrac), + /// A root in math: `√x`, `∛x` or `∜x`. + MathRoot(MathRoot), /// An identifier: `left`. Ident(Ident), /// The `none` literal. @@ -223,6 +225,7 @@ impl AstNode for Expr { SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited), SyntaxKind::MathAttach => node.cast().map(Self::MathAttach), SyntaxKind::MathFrac => node.cast().map(Self::MathFrac), + SyntaxKind::MathRoot => node.cast().map(Self::MathRoot), SyntaxKind::Ident => node.cast().map(Self::Ident), SyntaxKind::None => node.cast().map(Self::None), SyntaxKind::Auto => node.cast().map(Self::Auto), @@ -283,6 +286,7 @@ impl AstNode for Expr { Self::MathDelimited(v) => v.as_untyped(), Self::MathAttach(v) => v.as_untyped(), Self::MathFrac(v) => v.as_untyped(), + Self::MathRoot(v) => v.as_untyped(), Self::Ident(v) => v.as_untyped(), Self::None(v) => v.as_untyped(), Self::Auto(v) => v.as_untyped(), @@ -856,6 +860,28 @@ impl MathFrac { } } +node! { + /// A root in math: `√x`, `∛x` or `∜x`. + MathRoot +} + +impl MathRoot { + /// The index of the root. + pub fn index(&self) -> Option { + match self.0.children().next().map(|node| node.text().as_str()) { + Some("∜") => Some(4), + Some("∛") => Some(3), + Some("√") => Option::None, + _ => Option::None, + } + } + + /// The radicand. + pub fn radicand(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } +} + node! { /// An identifier: `it`. Ident diff --git a/src/syntax/kind.rs b/src/syntax/kind.rs index 0717e16c4..111fb2605 100644 --- a/src/syntax/kind.rs +++ b/src/syntax/kind.rs @@ -67,6 +67,8 @@ pub enum SyntaxKind { MathAttach, /// A fraction in math: `x/2`. MathFrac, + /// A root in math: `√x`, `∛x` or `∜x`. + MathRoot, /// A hashtag that switches into code mode: `#`. Hashtag, @@ -134,6 +136,8 @@ pub enum SyntaxKind { Dots, /// An arrow between a closure's parameters and body: `=>`. Arrow, + /// A root: `√`, `∛` or `∜`. + Root, /// The `not` operator. Not, @@ -347,6 +351,7 @@ impl SyntaxKind { Self::MathDelimited => "delimited math", Self::MathAttach => "math attachments", Self::MathFrac => "math fraction", + Self::MathRoot => "math root", Self::Hashtag => "hashtag", Self::LeftBrace => "opening brace", Self::RightBrace => "closing brace", @@ -378,6 +383,7 @@ impl SyntaxKind { Self::SlashEq => "divide-assign operator", Self::Dots => "dots", Self::Arrow => "arrow", + Self::Root => "root", Self::Not => "operator `not`", Self::And => "operator `and`", Self::Or => "operator `or`", diff --git a/src/syntax/lexer.rs b/src/syntax/lexer.rs index dc75a902a..eb19d8d96 100644 --- a/src/syntax/lexer.rs +++ b/src/syntax/lexer.rs @@ -437,6 +437,7 @@ impl Lexer<'_> { '/' => SyntaxKind::Slash, '^' => SyntaxKind::Hat, '&' => SyntaxKind::MathAlignPoint, + '√' | '∛' | '∜' => SyntaxKind::Root, // Identifiers. c if is_math_id_start(c) && self.s.at(is_math_id_continue) => { diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index 1198774b6..13c7bd474 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -282,6 +282,16 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { p.eat(); } + SyntaxKind::Root => { + if min_prec < 3 { + p.eat(); + let m2 = p.marker(); + math_expr_prec(p, 2, stop); + math_unparen(p, m2); + p.wrap(m, SyntaxKind::MathRoot); + } + } + _ => p.expected("expression"), } diff --git a/tests/ref/math/root.png b/tests/ref/math/root.png index ccd284a61..b7d2807d0 100644 Binary files a/tests/ref/math/root.png and b/tests/ref/math/root.png differ diff --git a/tests/typ/math/root.typ b/tests/typ/math/root.typ index fdb37bc1b..6eba1275b 100644 --- a/tests/typ/math/root.typ +++ b/tests/typ/math/root.typ @@ -37,3 +37,9 @@ $ root(2, x) quad root(3/(2/1), x) quad root(1/11, x) quad root(1/2/3, 1) $ + +--- +// Test shorthand. +$ √2^3 = sqrt(2^3) $ +$ √(x+y) quad ∛x quad ∜x $ +$ (√2+3) = (sqrt(2)+3) $