diff --git a/library/src/lib.rs b/library/src/lib.rs index 76245b9e0..17f3de6da 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -194,7 +194,10 @@ fn items() -> LangItems { math_atom: |atom| math::AtomNode(atom).pack(), math_align_point: || math::AlignPointNode.pack(), math_delimited: |open, body, close| math::LrNode(open + body + close).pack(), - math_script: |base, sub, sup| math::ScriptNode { base, sub, sup }.pack(), + math_attach: |base, sub, sup| { + math::AttachNode { base, top: sub, bottom: sup }.pack() + }, + math_accent: |base, accent| math::AccentNode { base, accent }.pack(), math_frac: |num, denom| math::FracNode { num, denom }.pack(), } } diff --git a/library/src/math/accent.rs b/library/src/math/accent.rs index 6829554cd..90e64b961 100644 --- a/library/src/math/accent.rs +++ b/library/src/math/accent.rs @@ -1,15 +1,17 @@ +use typst::model::combining_accent; + use super::*; /// How much the accent can be shorter than the base. const ACCENT_SHORT_FALL: Em = Em::new(0.5); /// # Accent -/// An accented node. +/// Attach an accent to a base. /// /// ## Example /// ``` -/// $accent(a, ->) != accent(a, ~)$ \ -/// $accent(a, `) = accent(a, grave)$ +/// $arrow(a) = accent(a, arrow)$ \ +/// $grave(a) = accent(a, `)$ /// ``` /// /// ## Parameters @@ -19,30 +21,26 @@ const ACCENT_SHORT_FALL: Em = Em::new(0.5); /// /// ### Example /// ``` -/// $accent(A B C, ->)$ +/// $arrow(A B C)$ /// ``` /// -/// - accent: Content (positional, required) +/// - accent: char (positional, required) /// The accent to apply to the base. /// /// Supported accents include: -/// - Plus: `` + `` -/// - Overline: `` - ``, `‾` -/// - Dot: `.` -/// - Circumflex: `^` -/// - Acute: `´` -/// - Low Line: `_` -/// - Grave: `` ` `` -/// - Tilde: `~` -/// - Diaeresis: `¨` -/// - Macron: `¯` -/// - Acute: `´` -/// - Cedilla: `¸` -/// - Caron: `ˇ` -/// - Breve: `˘` -/// - Double acute: `˝` -/// - Left arrow: `<-` -/// - Right arrow: `->` +/// - Grave: `grave`, `` ` `` +/// - Acute: `acute`, `´` +/// - Circumflex: `circum`, `^` +/// - Tilde: `tilde`, `~` +/// - Macron: `macron`, `¯` +/// - Breve: `breve`, `˘` +/// - Dot: `dot`, `.` +/// - Diaeresis: `diaer` `¨` +/// - Circle: `circle`, `∘` +/// - Double acute: `acute.double`, `˝` +/// - Caron: `caron`, `ˇ` +/// - Right arrow: `arrow`, `->` +/// - Left arrow: `arrow.l`, `<-` /// /// ## Category /// math @@ -53,7 +51,7 @@ pub struct AccentNode { /// The accent base. pub base: Content, /// The accent. - pub accent: Content, + pub accent: char, } #[node] @@ -78,16 +76,9 @@ impl LayoutMath for AccentNode { _ => (base.width() + base.italics_correction()) / 2.0, }; - let Some(c) = extract(&self.accent) else { - ctx.push(base); - if let Some(span) = self.accent.span() { - bail!(span, "not an accent"); - } - return Ok(()); - }; - // Forcing the accent to be at least as large as the base makes it too // wide in many case. + let c = combining_accent(self.accent).unwrap_or(self.accent); let glyph = GlyphFragment::new(ctx, c); let short_fall = ACCENT_SHORT_FALL.scaled(ctx); let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall); @@ -130,45 +121,3 @@ fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs { (advance.scaled(ctx) + italics_correction) / 2.0 }) } - -/// Extract a single character from content. -fn extract(accent: &Content) -> Option { - let atom = accent.to::()?; - let mut chars = atom.0.chars(); - let c = chars.next().filter(|_| chars.next().is_none())?; - Some(combining(c)) -} - -/// Convert from a non-combining accent to a combining one. -/// -/// https://www.w3.org/TR/mathml-core/#combining-character-equivalences -fn combining(c: char) -> char { - match c { - '\u{002b}' => '\u{031f}', - '\u{002d}' => '\u{0305}', - '\u{002e}' => '\u{0307}', - '\u{005e}' => '\u{0302}', - '\u{005f}' => '\u{0332}', - '\u{0060}' => '\u{0300}', - '\u{007e}' => '\u{0303}', - '\u{00a8}' => '\u{0308}', - '\u{00af}' => '\u{0304}', - '\u{00b4}' => '\u{0301}', - '\u{00b8}' => '\u{0327}', - '\u{02c6}' => '\u{0302}', - '\u{02c7}' => '\u{030c}', - '\u{02d8}' => '\u{0306}', - '\u{02d9}' => '\u{0307}', - '\u{02db}' => '\u{0328}', - '\u{02dc}' => '\u{0303}', - '\u{02dd}' => '\u{030b}', - '\u{203e}' => '\u{0305}', - '\u{2190}' => '\u{20d6}', - '\u{2192}' => '\u{20d7}', - '\u{2212}' => '\u{0305}', - '\u{223C}' => '\u{0303}', - '\u{22C5}' => '\u{0307}', - '\u{27f6}' => '\u{20d7}', - _ => c, - } -} diff --git a/library/src/math/script.rs b/library/src/math/attach.rs similarity index 65% rename from library/src/math/script.rs rename to library/src/math/attach.rs index 2c765fbf1..2205e556e 100644 --- a/library/src/math/script.rs +++ b/library/src/math/attach.rs @@ -1,86 +1,153 @@ use super::*; -/// # Script -/// A mathematical sub- and/or superscript. +/// # Attachment +/// A base with optional attachments. /// /// ## Syntax /// This function also has dedicated syntax: Use the underscore (`_`) to -/// indicate a subscript and the circumflex (`^`) to indicate a superscript. +/// indicate a bottom attachment and the circumflex (`^`) to indicate a top +/// attachment. /// /// ## Example /// ``` -/// $ a_i = 2^(1+i) $ +/// $ sum_(i=0)^n a_i = 2^(1+i) $ /// ``` /// /// ## Parameters /// - base: Content (positional, required) -/// The base to which the applies the sub- and/or superscript. +/// The base to which things are attached. /// -/// - sub: Content (named) -/// The subscript. +/// - top: Content (named) +/// The top attachment. /// -/// - sup: Content (named) -/// The superscript. +/// - bottom: Content (named) +/// The bottom attachment. /// /// ## Category /// math #[func] #[capable(LayoutMath)] #[derive(Debug, Hash)] -pub struct ScriptNode { +pub struct AttachNode { /// The base. pub base: Content, - /// The subscript. - pub sub: Option, - /// The superscript. - pub sup: Option, + /// The top attachment. + pub top: Option, + /// The bottom attachment. + pub bottom: Option, } #[node] -impl ScriptNode { +impl AttachNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let base = args.expect("base")?; - let sub = args.named("sub")?; - let sup = args.named("sup")?; - Ok(Self { base, sub, sup }.pack()) + let top = args.named("top")?; + let bottom = args.named("bottom")?; + Ok(Self { base, top, bottom }.pack()) } } -impl LayoutMath for ScriptNode { +impl LayoutMath for AttachNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { let base = ctx.layout_fragment(&self.base)?; let mut sub = Frame::new(Size::zero()); - if let Some(node) = &self.sub { + if let Some(node) = &self.top { ctx.style(ctx.style.for_subscript()); sub = ctx.layout_frame(node)?; ctx.unstyle(); } let mut sup = Frame::new(Size::zero()); - if let Some(node) = &self.sup { + if let Some(node) = &self.bottom { ctx.style(ctx.style.for_superscript()); sup = ctx.layout_frame(node)?; ctx.unstyle(); } - let render_limits = ctx.style.size == MathSize::Display - && base.class() == Some(MathClass::Large) - && match &base { - MathFragment::Variant(variant) => LIMITS.contains(&variant.c), - MathFragment::Frame(fragment) => fragment.limits, - _ => false, - }; + let render_limits = self.base.is::() + || (!self.base.is::() + && ctx.style.size == MathSize::Display + && base.class() == Some(MathClass::Large) + && match &base { + MathFragment::Variant(variant) => LIMITS.contains(&variant.c), + MathFragment::Frame(fragment) => fragment.limits, + _ => false, + }); if render_limits { limits(ctx, base, sub, sup) } else { - scripts(ctx, base, sub, sup, self.sub.is_some() && self.sup.is_some()) + scripts(ctx, base, sub, sup, self.top.is_some() && self.bottom.is_some()) } } } -/// Layout normal sub- and superscripts. +/// # Scripts +/// Force a base to display attachments as scripts. +/// +/// ## Example +/// ``` +/// $ scripts(sum)_1^2 != sum_1^2 $ +/// ``` +/// +/// ## Parameters +/// - base: Content (positional, required) +/// The base to attach the scripts to. +/// +/// ## Category +/// math +#[func] +#[capable(LayoutMath)] +#[derive(Debug, Hash)] +pub struct ScriptsNode(Content); + +#[node] +impl ScriptsNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("base")?).pack()) + } +} + +impl LayoutMath for ScriptsNode { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + self.0.layout_math(ctx) + } +} + +/// # Limits +/// Force a base to display attachments as limits. +/// +/// ## Example +/// ``` +/// $ limits(A)_1^2 != A_1^2 $ +/// ``` +/// +/// ## Parameters +/// - base: Content (positional, required) +/// The base to attach the limits to. +/// +/// ## Category +/// math +#[func] +#[capable(LayoutMath)] +#[derive(Debug, Hash)] +pub struct LimitsNode(Content); + +#[node] +impl LimitsNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("base")?).pack()) + } +} + +impl LayoutMath for LimitsNode { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + self.0.layout_math(ctx) + } +} + +/// Layout sub- and superscripts. fn scripts( ctx: &mut MathContext, base: MathFragment, diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 1d94661a9..636affed1 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -5,6 +5,7 @@ mod ctx; mod accent; mod align; mod atom; +mod attach; mod braced; mod frac; mod fragment; @@ -13,7 +14,6 @@ mod matrix; mod op; mod root; mod row; -mod script; mod spacing; mod stretch; mod style; @@ -22,13 +22,13 @@ mod symbols; pub use self::accent::*; pub use self::align::*; pub use self::atom::*; +pub use self::attach::*; pub use self::braced::*; pub use self::frac::*; pub use self::lr::*; pub use self::matrix::*; pub use self::op::*; pub use self::root::*; -pub use self::script::*; pub use self::style::*; use ttf_parser::GlyphId; @@ -54,22 +54,33 @@ use crate::text::{families, variant, FallbackList, FontFamily, SpaceNode}; pub fn module(sym: &Module) -> Module { let mut math = Scope::deduplicating(); math.def_func::("formula"); + + // Grouping. math.def_func::("lr"); - math.def_func::("op"); - math.def_func::("floor"); - math.def_func::("ceil"); math.def_func::("abs"); math.def_func::("norm"); + math.def_func::("floor"); + math.def_func::("ceil"); + + // Attachments and accents. + math.def_func::("attach"); + math.def_func::("scripts"); + math.def_func::("limits"); math.def_func::("accent"); - math.def_func::("frac"); - math.def_func::("binom"); - math.def_func::("script"); - math.def_func::("sqrt"); - math.def_func::("root"); - math.def_func::("vec"); - math.def_func::("cases"); math.def_func::("underbrace"); math.def_func::("overbrace"); + + // Fractions and matrix-likes. + math.def_func::("frac"); + math.def_func::("binom"); + math.def_func::("vec"); + math.def_func::("cases"); + + // Roots. + math.def_func::("sqrt"); + math.def_func::("root"); + + // Styles. math.def_func::("bold"); math.def_func::("italic"); math.def_func::("serif"); @@ -78,10 +89,16 @@ pub fn module(sym: &Module) -> Module { math.def_func::("frak"); math.def_func::("mono"); math.def_func::("bb"); - spacing::define(&mut math); - symbols::define(&mut math); + + // Text operators. + math.def_func::("op"); op::define(&mut math); + + // Symbols and spacing. + symbols::define(&mut math); + spacing::define(&mut math); math.copy_from(sym.scope()); + Module::new("math").with_scope(math) } diff --git a/library/src/math/op.rs b/library/src/math/op.rs index 22daee658..766d63819 100644 --- a/library/src/math/op.rs +++ b/library/src/math/op.rs @@ -9,9 +9,9 @@ use super::*; /// - text: EcoString (positional, required) /// The operator's text. /// - limits: bool (named) -/// Whether the operator should display sub- and superscripts as limits. +/// Whether the operator should force attachments to display as limits. /// -/// Defaults to `true`. +/// Defaults to `false`. /// /// ## Category /// math @@ -21,7 +21,7 @@ use super::*; pub struct OpNode { /// The operator's text. pub text: EcoString, - /// Whether the operator should display sub- and superscripts as limits. + /// Whether the operator should force attachments to display as limits. pub limits: bool, } @@ -30,7 +30,7 @@ impl OpNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { Ok(Self { text: args.expect("text")?, - limits: args.named("limits")?.unwrap_or(true), + limits: args.named("limits")?.unwrap_or(false), } .pack()) } diff --git a/src/ide/complete.rs b/src/ide/complete.rs index 9302b552a..83d0ca9c2 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 9261d1570..7f7ad6eec 100644 --- a/src/ide/highlight.rs +++ b/src/ide/highlight.rs @@ -118,7 +118,7 @@ pub fn highlight(node: &LinkedNode) -> Option { 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 { _ => 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 { 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 { 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 17ed2d308..0da9906f6 100644 --- a/src/model/cast.rs +++ b/src/model/cast.rs @@ -232,6 +232,17 @@ castable! { color: Color => Self::Solid(color), } +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 538fa6874..44f08a76b 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 { 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 { - 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::()?.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 773342b3c..4208a4c74 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, sup: Option) -> Content, + /// A base with optional attachments in a formula: `a_1^2`. + pub math_attach: + fn(base: Content, bottom: Option, top: Option) -> 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 6a1c60a5d..954a84f06 100644 --- a/src/model/module.rs +++ b/src/model/module.rs @@ -78,3 +78,9 @@ impl Debug for Module { write!(f, "", 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 7acf917db..83137f38f 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 { 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 { (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 9196a35a1..6bfbcebdd 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 686f1b815..214fea3e8 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 { let mut best = None; @@ -150,3 +156,30 @@ fn parts(modifiers: &str) -> impl Iterator { 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 { + 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 d03911c61..4b9fa5f74 100644 --- a/src/model/value.rs +++ b/src/model/value.rs @@ -438,7 +438,11 @@ primitive! { Rel: "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 64f54e37c..45f79685d 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 { + /// The bottom attachment. + pub fn bottom(&self) -> Option { self.0 .children() .skip_while(|node| !matches!(node.kind(), SyntaxKind::Underscore)) .find_map(SyntaxNode::cast) } - /// The superscript. - pub fn sup(&self) -> Option { + /// The top attachment. + pub fn top(&self) -> Option { self.0 .children() .skip_while(|node| !matches!(node.kind(), SyntaxKind::Hat)) diff --git a/src/syntax/kind.rs b/src/syntax/kind.rs index f0a0bc5a0..aa4a5cfe6 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 f6ed2f5d7..077305337 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 { 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)) diff --git a/tests/ref/math/accents.png b/tests/ref/math/accents.png index dba9fb453..cb167533d 100644 Binary files a/tests/ref/math/accents.png and b/tests/ref/math/accents.png differ diff --git a/tests/typ/math/accents.typ b/tests/typ/math/accents.typ index 0cd0b16e5..7a896e5d8 100644 --- a/tests/typ/math/accents.typ +++ b/tests/typ/math/accents.typ @@ -3,26 +3,13 @@ --- #set page(width: auto) -$ accent(a,`), - accent(a,´), - accent(a,\^), - accent(a,~), - accent(a,¯), - accent(a,‾), - accent(a,˘), - accent(a,.), - accent(a,¨), - accent(a,ˇ), - accent(a,->) $ - -$ accent(a, grave), - accent(a, acute), - accent(a, circum), - accent(a, tilde), - accent(a, macron), - accent(a, overline), - accent(a, breve), - accent(a, dot), - accent(a, diaer), - accent(a, caron), - accent(a, arrow) $ +$ grave(a), + acute(a), + circum(a), + tilde(a), + macron(a), + breve(a), + dot(a), + diaer(a), + caron(a), + arrow(a) $