From 28c554ec2185a15e22f0408ce485ed4afe035e03 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 28 Jan 2023 12:01:05 +0100 Subject: [PATCH] Rework math attachments and accents --- library/src/lib.rs | 5 +- library/src/math/accent.rs | 95 ++++------------ library/src/math/{script.rs => attach.rs} | 127 +++++++++++++++++----- library/src/math/mod.rs | 45 +++++--- library/src/math/op.rs | 8 +- src/ide/complete.rs | 6 +- src/ide/highlight.rs | 8 +- src/model/cast.rs | 11 ++ src/model/eval.rs | 41 +++++-- src/model/library.rs | 19 ++-- src/model/module.rs | 6 + src/model/ops.rs | 19 +++- src/model/str.rs | 8 +- src/model/symbol.rs | 35 +++++- src/model/value.rs | 6 +- src/syntax/ast.rs | 30 +++-- src/syntax/kind.rs | 9 +- src/syntax/parser.rs | 27 +++-- tests/ref/math/accents.png | Bin 3126 -> 1493 bytes tests/typ/math/accents.typ | 33 ++---- 20 files changed, 326 insertions(+), 212 deletions(-) rename library/src/math/{script.rs => attach.rs} (65%) 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 dba9fb453751bdd4b8bc798405f904d583bc0101..cb167533d833a80ed39d4b3e261498249da01b63 100644 GIT binary patch literal 1493 zcmV;`1uFW9P)Q}jlNOCyrlXB!juU2@k!hLZV_-#z+(Ho{5K$H#G<=(>rKva& zN+>4dC?Y<>7pV9eA1I=wP~rnoUh-V_(;t@^&)v(6t1B~B=lkb}?=I(a_jk@ciXtN; zBO@atBO~)vhyQ$}w$zr|Qd??EZK*A_rMA?T+EQC;OKqtwwWYSyZc=Tl`BEeFT!1|( z3qEd-Y60dxWG(6U0BoBc-J<@NI-q`Iwf!Gzik;WCd3G){z~YWxcI03ezs}M1x>b#G z=`lTXe&|CMUo`Y!S=D*(OuK(D>#|+%LL9{B=un2|y6S}Y(+po(>U-b$A+Mb{!tf|Q z3bN`+OmHFA_1-tdpE00(chFm2rDE+XKR-V|Kffy?yEeRy+k1A_#490o#H8LTQoZf` zd|G^QN4M6=X0zFB?*Cf#6WH6Uf3*X#@%EKUZ9~)vXmh!v(`u_Tpvu^rcI5V9IDFf_ zcc#&OIsVfB@kqOSo+qGfb&NGut9PhovpH0&fBbbkx^~vIMdOal(V?n2b3!*nU(_Ej z;S0wfUZg`6v%Z_Ub>>waOABA%zkh`el|;^)loobF$5JBZ2kcp^Kl)}Yn7A+au8t+m zh?$z2phMTEFPd;LsM1;Ogsvr&J^z7-?(|s$v(%%y?cd`*MXf&*QDKiH2v#nLZ1KA_ z6u6soCtBAX;IIBAvRMPHTyUEtqDvkbFxusw$U=s~F}LZuz1}Gsyxgjt)XsO`2YBdy z?Joxbv2Da*%5^;sv|A?DMvVZLwHMLJX1P$`YQ4yYj0d7UM0CLAZ>Va%MP&T~fv{d8 z+U`~i#cH~^U3$y}%%08;x&KQxxUL=+P@cK0(cSh4nTFCbgF zCMqiFz=vj?;s9V@Gx625LpcKU(Om+%4Fgi15#P~?N)FH^+F9)mW?*AW@eT8%S`56h zSbWIOg#xQy5I;HJwGwP?*9fSlMKrLWqgb2K+6p!9N#bUA`5uVuF4iWu14^5xin~~i z1*Y^DYrd}oH`GH;YUisz0sey!p1gMl;EH-&VCR($+Efd*Pu~PiE0=h1Q_NDfX;UrK zJo^F%)WZ4)yEa|P0QyZ4Ydr@8+g(a)AKVy|m6JfXnPSajB#`J{Q(5VhcDc)5sChXW z3zTPTENCFGq?I*u7o5;maZ^186>XPcfs#{NZNlfk>~^GO!BK4x^OTElw`D9AC~mlD zj2s7qc(D0AWN9bS3FSHzx+Y_RTMe(^!IJ_1o~+A$zI3Zv-&f zlOOIlH{=6XQy1AG>GnbhP?G`J(+LiI(w&*L6sQenOz%XhBVJ6+UkN647XVw|hV;Sx z3Ad4>G@LPAc2E)a?4g`E7~V{~kH+_dZ3DargQLnY0P0>nKuNIsnIpf!02l4?tQZ1G z{l*XmyA>bp{n-9AMS-q=?%o6Mxc&h^%qWQKJ<&GJ$-RBcWSMykQGu) zE4A9(DN%b%Od-ERneT87o%N1dShX(bTN@<$HXII-!InceO;e9TkVnm9{P1$aZEJkTt?s+jPQ!sx8J^Y_9Q4ZR8cayudg* z^JpsV%XvnYYD78L9IBlzbf}iy$V}`sqRf&e)Bg8IYD;aYEw!b#)Rx*(TWU*fsV%jo vw$zr|Qd??E?I)uxBO@atBO@at^VH-YvI}&CX=#Wi00000NkvXXu0mjfJ4n~I literal 3126 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 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) $