summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ide/complete.rs6
-rw-r--r--src/ide/highlight.rs8
-rw-r--r--src/model/cast.rs11
-rw-r--r--src/model/eval.rs41
-rw-r--r--src/model/library.rs19
-rw-r--r--src/model/module.rs6
-rw-r--r--src/model/ops.rs19
-rw-r--r--src/model/str.rs8
-rw-r--r--src/model/symbol.rs35
-rw-r--r--src/model/value.rs6
-rw-r--r--src/syntax/ast.rs30
-rw-r--r--src/syntax/kind.rs9
-rw-r--r--src/syntax/parser.rs27
13 files changed, 158 insertions, 67 deletions
diff --git a/src/ide/complete.rs b/src/ide/complete.rs
index 9302b552..83d0ca9c 100644
--- a/src/ide/complete.rs
+++ b/src/ide/complete.rs
@@ -216,7 +216,7 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
Some(SyntaxKind::Formula)
| Some(SyntaxKind::Math)
| Some(SyntaxKind::MathFrac)
- | Some(SyntaxKind::MathScript)
+ | Some(SyntaxKind::MathAttach)
) {
return false;
}
@@ -584,7 +584,7 @@ fn complete_code(ctx: &mut CompletionContext) -> bool {
None | Some(SyntaxKind::Markup)
| Some(SyntaxKind::Math)
| Some(SyntaxKind::MathFrac)
- | Some(SyntaxKind::MathScript)
+ | Some(SyntaxKind::MathAttach)
) {
return false;
}
@@ -955,7 +955,7 @@ impl<'a> CompletionContext<'a> {
Some(SyntaxKind::Formula)
| Some(SyntaxKind::Math)
| Some(SyntaxKind::MathFrac)
- | Some(SyntaxKind::MathScript)
+ | Some(SyntaxKind::MathAttach)
);
let scope = if in_math { self.math } else { self.global };
diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs
index 9261d157..7f7ad6ee 100644
--- a/src/ide/highlight.rs
+++ b/src/ide/highlight.rs
@@ -118,7 +118,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
SyntaxKind::MathAtom => None,
SyntaxKind::MathIdent => highlight_ident(node),
SyntaxKind::MathDelimited => None,
- SyntaxKind::MathScript => None,
+ SyntaxKind::MathAttach => None,
SyntaxKind::MathFrac => None,
SyntaxKind::MathAlignPoint => Some(Category::MathOperator),
@@ -143,7 +143,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
_ => Some(Category::Operator),
},
SyntaxKind::Underscore => match node.parent_kind() {
- Some(SyntaxKind::MathScript) => Some(Category::MathOperator),
+ Some(SyntaxKind::MathAttach) => Some(Category::MathOperator),
_ => None,
},
SyntaxKind::Dollar => Some(Category::MathDelimiter),
@@ -213,7 +213,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
SyntaxKind::Markup
| SyntaxKind::Math
| SyntaxKind::MathFrac
- | SyntaxKind::MathScript,
+ | SyntaxKind::MathAttach,
) => Some(Category::Interpolated),
Some(SyntaxKind::FieldAccess) => node.parent().and_then(highlight),
_ => None,
@@ -252,7 +252,7 @@ fn highlight_ident(node: &LinkedNode) -> Option<Category> {
SyntaxKind::Markup
| SyntaxKind::Math
| SyntaxKind::MathFrac
- | SyntaxKind::MathScript,
+ | SyntaxKind::MathAttach,
) => Some(Category::Interpolated),
Some(SyntaxKind::FuncCall) => Some(Category::Function),
Some(SyntaxKind::FieldAccess)
diff --git a/src/model/cast.rs b/src/model/cast.rs
index 17ed2d30..0da9906f 100644
--- a/src/model/cast.rs
+++ b/src/model/cast.rs
@@ -233,6 +233,17 @@ castable! {
}
castable! {
+ char,
+ string: Str => {
+ let mut chars = string.chars();
+ match (chars.next(), chars.next()) {
+ (Some(c), None) => c,
+ _ => Err("expected exactly one character")?,
+ }
+ },
+}
+
+castable! {
EcoString,
string: Str => string.into(),
}
diff --git a/src/model/eval.rs b/src/model/eval.rs
index 538fa687..44f08a76 100644
--- a/src/model/eval.rs
+++ b/src/model/eval.rs
@@ -8,8 +8,9 @@ use comemo::{Track, Tracked, TrackedMut};
use unicode_segmentation::UnicodeSegmentation;
use super::{
- methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Func, Label,
- LangItems, Module, Recipe, Scopes, Selector, StyleMap, Symbol, Transform, Value,
+ combining_accent, methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content,
+ Dict, Func, Label, LangItems, Module, Recipe, Scopes, Selector, StyleMap, Symbol,
+ Transform, Value,
};
use crate::diag::{
bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint,
@@ -347,7 +348,7 @@ impl Eval for ast::Expr {
Self::MathIdent(v) => v.eval(vm),
Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content),
Self::MathDelimited(v) => v.eval(vm).map(Value::Content),
- Self::MathScript(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::Ident(v) => v.eval(vm),
Self::None(v) => v.eval(vm),
@@ -593,20 +594,20 @@ impl Eval for ast::MathDelimited {
}
}
-impl Eval for ast::MathScript {
+impl Eval for ast::MathAttach {
type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let base = self.base().eval(vm)?.display_in_math();
let sub = self
- .sub()
+ .bottom()
.map(|expr| expr.eval(vm).map(Value::display_in_math))
.transpose()?;
let sup = self
- .sup()
+ .top()
.map(|expr| expr.eval(vm).map(Value::display_in_math))
.transpose()?;
- Ok((vm.items.math_script)(base, sub, sup))
+ Ok((vm.items.math_attach)(base, sub, sup))
}
}
@@ -929,13 +930,21 @@ impl Eval for ast::FuncCall {
type Output = Value;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
- let callee = self.callee();
- let callee_span = callee.span();
- let in_math = matches!(callee, ast::Expr::MathIdent(_));
- let callee = callee.eval(vm)?;
+ let callee_expr = self.callee();
+ let callee_span = callee_expr.span();
+ let callee = callee_expr.eval(vm)?;
let mut args = self.args().eval(vm)?;
- if in_math && !matches!(callee, Value::Func(_)) {
+ if in_math(&callee_expr) && !matches!(callee, Value::Func(_)) {
+ if let Value::Symbol(sym) = &callee {
+ let c = sym.get();
+ if let Some(accent) = combining_accent(c) {
+ let base = args.expect("base")?;
+ args.finish()?;
+ return Ok(Value::Content((vm.items.math_accent)(base, accent)));
+ }
+ }
+
let mut body = (vm.items.math_atom)('('.into());
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 {
@@ -952,6 +961,14 @@ impl Eval for ast::FuncCall {
}
}
+fn in_math(expr: &ast::Expr) -> bool {
+ match expr {
+ ast::Expr::MathIdent(_) => true,
+ ast::Expr::FieldAccess(access) => in_math(&access.target()),
+ _ => false,
+ }
+}
+
fn complete_call(
vm: &mut Vm,
callee: &Func,
diff --git a/src/model/library.rs b/src/model/library.rs
index 773342b3..4208a4c7 100644
--- a/src/model/library.rs
+++ b/src/model/library.rs
@@ -71,12 +71,13 @@ pub struct LangItems {
pub math_atom: fn(atom: EcoString) -> Content,
/// An alignment point in a formula: `&`.
pub math_align_point: fn() -> Content,
- /// A subsection in a math formula that is surrounded by matched delimiters:
- /// `[x + y]`.
+ /// Matched delimiters surrounding math in a formula: `[x + y]`.
pub math_delimited: fn(open: Content, body: Content, close: Content) -> Content,
- /// A base with optional sub- and superscripts in a formula: `a_1^2`.
- pub math_script:
- fn(base: Content, sub: Option<Content>, sup: Option<Content>) -> Content,
+ /// A base with optional attachments in a formula: `a_1^2`.
+ pub math_attach:
+ fn(base: Content, bottom: Option<Content>, top: Option<Content>) -> Content,
+ /// A base with an accent: `arrow(x)`.
+ pub math_accent: fn(base: Content, accent: char) -> Content,
/// A fraction in a formula: `x/2`.
pub math_frac: fn(num: Content, denom: Content) -> Content,
}
@@ -95,6 +96,8 @@ impl Hash for LangItems {
self.space.hash(state);
self.linebreak.hash(state);
self.text.hash(state);
+ self.text_id.hash(state);
+ (self.text_str as usize).hash(state);
self.smart_quote.hash(state);
self.parbreak.hash(state);
self.strong.hash(state);
@@ -108,9 +111,11 @@ impl Hash for LangItems {
self.term_item.hash(state);
self.formula.hash(state);
self.math_atom.hash(state);
- self.math_script.hash(state);
- self.math_frac.hash(state);
self.math_align_point.hash(state);
+ self.math_delimited.hash(state);
+ self.math_attach.hash(state);
+ self.math_accent.hash(state);
+ self.math_frac.hash(state);
}
}
diff --git a/src/model/module.rs b/src/model/module.rs
index 6a1c60a5..954a84f0 100644
--- a/src/model/module.rs
+++ b/src/model/module.rs
@@ -78,3 +78,9 @@ impl Debug for Module {
write!(f, "<module {}>", self.name())
}
}
+
+impl PartialEq for Module {
+ fn eq(&self, other: &Self) -> bool {
+ Arc::ptr_eq(&self.0, &other.0)
+ }
+}
diff --git a/src/model/ops.rs b/src/model/ops.rs
index 7acf917d..83137f38 100644
--- a/src/model/ops.rs
+++ b/src/model/ops.rs
@@ -2,7 +2,7 @@
use std::cmp::Ordering;
-use super::{Regex, Value};
+use super::{format_str, Regex, Value};
use crate::diag::StrResult;
use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart};
use crate::util::format_eco;
@@ -20,10 +20,15 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
Ok(match (lhs, rhs) {
(a, None) => a,
(None, b) => b,
+ (Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")),
(Str(a), Str(b)) => Str(a + b),
- (Str(a), Content(b)) => Content(item!(text)(a.into()) + b),
- (Content(a), Str(b)) => Content(a + item!(text)(b.into())),
+ (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")),
+ (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Content(a), Content(b)) => Content(a + b),
+ (Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())),
+ (Content(a), Str(b)) => Content(a + item!(text)(b.into())),
+ (Str(a), Content(b)) => Content(item!(text)(a.into()) + b),
+ (Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b),
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
(a, b) => mismatch!("cannot join {} with {}", a, b),
@@ -85,10 +90,15 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
(Fraction(a), Fraction(b)) => Fraction(a + b),
+ (Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")),
(Str(a), Str(b)) => Str(a + b),
+ (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")),
+ (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Content(a), Content(b)) => Content(a + b),
+ (Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())),
(Content(a), Str(b)) => Content(a + item!(text)(b.into())),
(Str(a), Content(b)) => Content(item!(text)(a.into()) + b),
+ (Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b),
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
@@ -326,11 +336,14 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Relative(a), Relative(b)) => a == b,
(Fraction(a), Fraction(b)) => a == b,
(Color(a), Color(b)) => a == b,
+ (Symbol(a), Symbol(b)) => a == b,
(Str(a), Str(b)) => a == b,
(Label(a), Label(b)) => a == b,
(Array(a), Array(b)) => a == b,
(Dict(a), Dict(b)) => a == b,
(Func(a), Func(b)) => a == b,
+ (Args(a), Args(b)) => a == b,
+ (Module(a), Module(b)) => a == b,
(Dyn(a), Dyn(b)) => a == b,
// Some technically different things should compare equal.
diff --git a/src/model/str.rs b/src/model/str.rs
index 9196a35a..6bfbcebd 100644
--- a/src/model/str.rs
+++ b/src/model/str.rs
@@ -1,5 +1,5 @@
use std::borrow::{Borrow, Cow};
-use std::fmt::{self, Debug, Formatter, Write};
+use std::fmt::{self, Debug, Display, Formatter, Write};
use std::hash::{Hash, Hasher};
use std::ops::{Add, AddAssign, Deref};
@@ -334,6 +334,12 @@ impl Deref for Str {
}
}
+impl Display for Str {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(self)
+ }
+}
+
impl Debug for Str {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_char('"')?;
diff --git a/src/model/symbol.rs b/src/model/symbol.rs
index 686f1b81..214fea3e 100644
--- a/src/model/symbol.rs
+++ b/src/model/symbol.rs
@@ -1,6 +1,6 @@
use std::cmp::Reverse;
use std::collections::BTreeSet;
-use std::fmt::{self, Debug, Formatter, Write};
+use std::fmt::{self, Debug, Display, Formatter, Write};
use crate::diag::StrResult;
use crate::util::EcoString;
@@ -109,6 +109,12 @@ impl Debug for Symbol {
}
}
+impl Display for Symbol {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_char(self.get())
+ }
+}
+
/// Find the best symbol from the list.
fn find(list: &[(&str, char)], modifiers: &str) -> Option<char> {
let mut best = None;
@@ -150,3 +156,30 @@ fn parts(modifiers: &str) -> impl Iterator<Item = &str> {
fn contained(modifiers: &str, m: &str) -> bool {
parts(modifiers).any(|part| part == m)
}
+
+/// Normalize an accent to a combining one.
+///
+/// https://www.w3.org/TR/mathml-core/#combining-character-equivalences
+pub fn combining_accent(c: char) -> Option<char> {
+ Some(match c {
+ '\u{0300}' | '`' => '\u{0300}',
+ '\u{0301}' | '´' => '\u{0301}',
+ '\u{0302}' | '^' | 'ˆ' => '\u{0302}',
+ '\u{0303}' | '~' | '∼' | '˜' => '\u{0303}',
+ '\u{0304}' | '¯' => '\u{0304}',
+ '\u{0305}' | '-' | '‾' | '−' => '\u{0305}',
+ '\u{0306}' | '˘' => '\u{0306}',
+ '\u{0307}' | '.' | '˙' | '⋅' => '\u{0307}',
+ '\u{0308}' | '¨' => '\u{0308}',
+ '\u{030a}' | '∘' | '○' => '\u{030a}',
+ '\u{030b}' | '˝' => '\u{030b}',
+ '\u{030c}' | 'ˇ' => '\u{030c}',
+ '\u{0327}' | '¸' => '\u{0327}',
+ '\u{0328}' | '˛' => '\u{0328}',
+ '\u{0332}' | '_' => '\u{0332}',
+ '\u{20d6}' | '←' => '\u{20d6}',
+ '\u{20d7}' | '→' | '⟶' => '\u{20d7}',
+ '⏞' | '⏟' | '⎴' | '⎵' | '⏜' | '⏝' | '⏠' | '⏡' => c,
+ _ => return None,
+ })
+}
diff --git a/src/model/value.rs b/src/model/value.rs
index d03911c6..4b9fa5f7 100644
--- a/src/model/value.rs
+++ b/src/model/value.rs
@@ -438,7 +438,11 @@ primitive! { Rel<Length>: "relative length",
primitive! { Fr: "fraction", Fraction }
primitive! { Color: "color", Color }
primitive! { Symbol: "symbol", Symbol }
-primitive! { Str: "string", Str }
+primitive! {
+ Str: "string",
+ Str,
+ Symbol(symbol) => symbol.get().into()
+}
primitive! { Label: "label", Label }
primitive! { Content: "content",
Content,
diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs
index 64f54e37..45f79685 100644
--- a/src/syntax/ast.rs
+++ b/src/syntax/ast.rs
@@ -121,11 +121,10 @@ pub enum Expr {
MathIdent(MathIdent),
/// An alignment point in a math formula: `&`.
MathAlignPoint(MathAlignPoint),
- /// A subsection in a math formula that is surrounded by matched delimiters:
- /// `[x + y]`.
+ /// Matched delimiters surrounding math in a formula: `[x + y]`.
MathDelimited(MathDelimited),
- /// A base with optional sub- and superscripts in a math formula: `a_1^2`.
- MathScript(MathScript),
+ /// A base with optional attachments in a formula: `a_1^2`.
+ MathAttach(MathAttach),
/// A fraction in a math formula: `x/2`.
MathFrac(MathFrac),
/// An identifier: `left`.
@@ -224,7 +223,7 @@ impl AstNode for Expr {
SyntaxKind::MathIdent => node.cast().map(Self::MathIdent),
SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint),
SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited),
- SyntaxKind::MathScript => node.cast().map(Self::MathScript),
+ SyntaxKind::MathAttach => node.cast().map(Self::MathAttach),
SyntaxKind::MathFrac => node.cast().map(Self::MathFrac),
SyntaxKind::Ident => node.cast().map(Self::Ident),
SyntaxKind::None => node.cast().map(Self::None),
@@ -285,7 +284,7 @@ impl AstNode for Expr {
Self::MathIdent(v) => v.as_untyped(),
Self::MathAlignPoint(v) => v.as_untyped(),
Self::MathDelimited(v) => v.as_untyped(),
- Self::MathScript(v) => v.as_untyped(),
+ Self::MathAttach(v) => v.as_untyped(),
Self::MathFrac(v) => v.as_untyped(),
Self::Ident(v) => v.as_untyped(),
Self::None(v) => v.as_untyped(),
@@ -709,8 +708,7 @@ node! {
}
node! {
- /// A subsection in a math formula that is surrounded by matched delimiters:
- /// `[x + y]`.
+ /// Matched delimiters surrounding math in a formula: `[x + y]`.
MathDelimited
}
@@ -732,26 +730,26 @@ impl MathDelimited {
}
node! {
- /// A base with an optional sub- and superscript in a formula: `a_1^2`.
- MathScript
+ /// A base with optional attachments in a formula: `a_1^2`.
+ MathAttach
}
-impl MathScript {
- /// The base of the script.
+impl MathAttach {
+ /// The base, to which things are attached.
pub fn base(&self) -> Expr {
self.0.cast_first_match().unwrap_or_default()
}
- /// The subscript.
- pub fn sub(&self) -> Option<Expr> {
+ /// The bottom attachment.
+ pub fn bottom(&self) -> Option<Expr> {
self.0
.children()
.skip_while(|node| !matches!(node.kind(), SyntaxKind::Underscore))
.find_map(SyntaxNode::cast)
}
- /// The superscript.
- pub fn sup(&self) -> Option<Expr> {
+ /// The top attachment.
+ pub fn top(&self) -> Option<Expr> {
self.0
.children()
.skip_while(|node| !matches!(node.kind(), SyntaxKind::Hat))
diff --git a/src/syntax/kind.rs b/src/syntax/kind.rs
index f0a0bc5a..aa4a5cfe 100644
--- a/src/syntax/kind.rs
+++ b/src/syntax/kind.rs
@@ -65,11 +65,10 @@ pub enum SyntaxKind {
MathIdent,
/// An alignment point in math: `&`.
MathAlignPoint,
- /// A subsection in a math formula that is surrounded by matched delimiters:
- /// `[x + y]`.
+ /// Matched delimiters surrounding math in a formula: `[x + y]`.
MathDelimited,
- /// A base with optional sub- and superscripts in math: `a_1^2`.
- MathScript,
+ /// A base with optional attachments in a formula: `a_1^2`.
+ MathAttach,
/// A fraction in math: `x/2`.
MathFrac,
@@ -349,7 +348,7 @@ impl SyntaxKind {
Self::MathAtom => "math atom",
Self::MathAlignPoint => "math alignment point",
Self::MathDelimited => "delimited math",
- Self::MathScript => "math script",
+ Self::MathAttach => "math attachments",
Self::MathFrac => "math fraction",
Self::Hashtag => "hashtag",
Self::LeftBrace => "opening brace",
diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs
index f6ed2f5d..07730533 100644
--- a/src/syntax/parser.rs
+++ b/src/syntax/parser.rs
@@ -234,21 +234,20 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
SyntaxKind::Hashtag => embedded_code_expr(p),
SyntaxKind::MathIdent => {
p.eat();
+ while p.directly_at(SyntaxKind::MathAtom)
+ && p.current_text() == "."
+ && matches!(
+ p.lexer.clone().next(),
+ SyntaxKind::MathIdent | SyntaxKind::MathAtom
+ )
+ {
+ p.convert(SyntaxKind::Dot);
+ p.convert(SyntaxKind::Ident);
+ p.wrap(m, SyntaxKind::FieldAccess);
+ }
if p.directly_at(SyntaxKind::MathAtom) && p.current_text() == "(" {
math_args(p);
p.wrap(m, SyntaxKind::FuncCall);
- } else {
- while p.directly_at(SyntaxKind::MathAtom)
- && p.current_text() == "."
- && matches!(
- p.lexer.clone().next(),
- SyntaxKind::MathIdent | SyntaxKind::MathAtom
- )
- {
- p.convert(SyntaxKind::Dot);
- p.convert(SyntaxKind::Ident);
- p.wrap(m, SyntaxKind::FieldAccess);
- }
}
}
@@ -362,10 +361,10 @@ fn math_class(text: &str) -> Option<MathClass> {
fn math_op(kind: SyntaxKind) -> Option<(SyntaxKind, SyntaxKind, ast::Assoc, usize)> {
match kind {
SyntaxKind::Underscore => {
- Some((SyntaxKind::MathScript, SyntaxKind::Hat, ast::Assoc::Right, 2))
+ Some((SyntaxKind::MathAttach, SyntaxKind::Hat, ast::Assoc::Right, 2))
}
SyntaxKind::Hat => {
- Some((SyntaxKind::MathScript, SyntaxKind::Underscore, ast::Assoc::Right, 2))
+ Some((SyntaxKind::MathAttach, SyntaxKind::Underscore, ast::Assoc::Right, 2))
}
SyntaxKind::Slash => {
Some((SyntaxKind::MathFrac, SyntaxKind::Eof, ast::Assoc::Left, 1))