Rework math attachments and accents

This commit is contained in:
Laurenz 2023-01-28 12:01:05 +01:00
parent 23238d4d44
commit 28c554ec21
20 changed files with 326 additions and 212 deletions

View File

@ -194,7 +194,10 @@ fn items() -> LangItems {
math_atom: |atom| math::AtomNode(atom).pack(), math_atom: |atom| math::AtomNode(atom).pack(),
math_align_point: || math::AlignPointNode.pack(), math_align_point: || math::AlignPointNode.pack(),
math_delimited: |open, body, close| math::LrNode(open + body + close).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(), math_frac: |num, denom| math::FracNode { num, denom }.pack(),
} }
} }

View File

@ -1,15 +1,17 @@
use typst::model::combining_accent;
use super::*; use super::*;
/// How much the accent can be shorter than the base. /// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5); const ACCENT_SHORT_FALL: Em = Em::new(0.5);
/// # Accent /// # Accent
/// An accented node. /// Attach an accent to a base.
/// ///
/// ## Example /// ## Example
/// ``` /// ```
/// $accent(a, ->) != accent(a, ~)$ \ /// $arrow(a) = accent(a, arrow)$ \
/// $accent(a, `) = accent(a, grave)$ /// $grave(a) = accent(a, `)$
/// ``` /// ```
/// ///
/// ## Parameters /// ## Parameters
@ -19,30 +21,26 @@ const ACCENT_SHORT_FALL: Em = Em::new(0.5);
/// ///
/// ### Example /// ### Example
/// ``` /// ```
/// $accent(A B C, ->)$ /// $arrow(A B C)$
/// ``` /// ```
/// ///
/// - accent: Content (positional, required) /// - accent: char (positional, required)
/// The accent to apply to the base. /// The accent to apply to the base.
/// ///
/// Supported accents include: /// Supported accents include:
/// - Plus: `` + `` /// - Grave: `grave`, `` ` ``
/// - Overline: `` - ``, `‾` /// - Acute: `acute`, `´`
/// - Dot: `.` /// - Circumflex: `circum`, `^`
/// - Circumflex: `^` /// - Tilde: `tilde`, `~`
/// - Acute: `´` /// - Macron: `macron`, `¯`
/// - Low Line: `_` /// - Breve: `breve`, `˘`
/// - Grave: `` ` `` /// - Dot: `dot`, `.`
/// - Tilde: `~` /// - Diaeresis: `diaer` `¨`
/// - Diaeresis: `¨` /// - Circle: `circle`, `∘`
/// - Macron: `¯` /// - Double acute: `acute.double`, `˝`
/// - Acute: `´` /// - Caron: `caron`, `ˇ`
/// - Cedilla: `¸` /// - Right arrow: `arrow`, `->`
/// - Caron: `ˇ` /// - Left arrow: `arrow.l`, `<-`
/// - Breve: `˘`
/// - Double acute: `˝`
/// - Left arrow: `<-`
/// - Right arrow: `->`
/// ///
/// ## Category /// ## Category
/// math /// math
@ -53,7 +51,7 @@ pub struct AccentNode {
/// The accent base. /// The accent base.
pub base: Content, pub base: Content,
/// The accent. /// The accent.
pub accent: Content, pub accent: char,
} }
#[node] #[node]
@ -78,16 +76,9 @@ impl LayoutMath for AccentNode {
_ => (base.width() + base.italics_correction()) / 2.0, _ => (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 // Forcing the accent to be at least as large as the base makes it too
// wide in many case. // wide in many case.
let c = combining_accent(self.accent).unwrap_or(self.accent);
let glyph = GlyphFragment::new(ctx, c); let glyph = GlyphFragment::new(ctx, c);
let short_fall = ACCENT_SHORT_FALL.scaled(ctx); let short_fall = ACCENT_SHORT_FALL.scaled(ctx);
let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall); 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 (advance.scaled(ctx) + italics_correction) / 2.0
}) })
} }
/// Extract a single character from content.
fn extract(accent: &Content) -> Option<char> {
let atom = accent.to::<AtomNode>()?;
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,
}
}

View File

@ -1,86 +1,153 @@
use super::*; use super::*;
/// # Script /// # Attachment
/// A mathematical sub- and/or superscript. /// A base with optional attachments.
/// ///
/// ## Syntax /// ## Syntax
/// This function also has dedicated syntax: Use the underscore (`_`) to /// 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 /// ## Example
/// ``` /// ```
/// $ a_i = 2^(1+i) $ /// $ sum_(i=0)^n a_i = 2^(1+i) $
/// ``` /// ```
/// ///
/// ## Parameters /// ## Parameters
/// - base: Content (positional, required) /// - 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) /// - top: Content (named)
/// The subscript. /// The top attachment.
/// ///
/// - sup: Content (named) /// - bottom: Content (named)
/// The superscript. /// The bottom attachment.
/// ///
/// ## Category /// ## Category
/// math /// math
#[func] #[func]
#[capable(LayoutMath)] #[capable(LayoutMath)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct ScriptNode { pub struct AttachNode {
/// The base. /// The base.
pub base: Content, pub base: Content,
/// The subscript. /// The top attachment.
pub sub: Option<Content>, pub top: Option<Content>,
/// The superscript. /// The bottom attachment.
pub sup: Option<Content>, pub bottom: Option<Content>,
} }
#[node] #[node]
impl ScriptNode { impl AttachNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let base = args.expect("base")?; let base = args.expect("base")?;
let sub = args.named("sub")?; let top = args.named("top")?;
let sup = args.named("sup")?; let bottom = args.named("bottom")?;
Ok(Self { base, sub, sup }.pack()) Ok(Self { base, top, bottom }.pack())
} }
} }
impl LayoutMath for ScriptNode { impl LayoutMath for AttachNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let base = ctx.layout_fragment(&self.base)?; let base = ctx.layout_fragment(&self.base)?;
let mut sub = Frame::new(Size::zero()); 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()); ctx.style(ctx.style.for_subscript());
sub = ctx.layout_frame(node)?; sub = ctx.layout_frame(node)?;
ctx.unstyle(); ctx.unstyle();
} }
let mut sup = Frame::new(Size::zero()); 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()); ctx.style(ctx.style.for_superscript());
sup = ctx.layout_frame(node)?; sup = ctx.layout_frame(node)?;
ctx.unstyle(); ctx.unstyle();
} }
let render_limits = ctx.style.size == MathSize::Display let render_limits = self.base.is::<LimitsNode>()
&& base.class() == Some(MathClass::Large) || (!self.base.is::<ScriptsNode>()
&& match &base { && ctx.style.size == MathSize::Display
MathFragment::Variant(variant) => LIMITS.contains(&variant.c), && base.class() == Some(MathClass::Large)
MathFragment::Frame(fragment) => fragment.limits, && match &base {
_ => false, MathFragment::Variant(variant) => LIMITS.contains(&variant.c),
}; MathFragment::Frame(fragment) => fragment.limits,
_ => false,
});
if render_limits { if render_limits {
limits(ctx, base, sub, sup) limits(ctx, base, sub, sup)
} else { } 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<Content> {
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<Content> {
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( fn scripts(
ctx: &mut MathContext, ctx: &mut MathContext,
base: MathFragment, base: MathFragment,

View File

@ -5,6 +5,7 @@ mod ctx;
mod accent; mod accent;
mod align; mod align;
mod atom; mod atom;
mod attach;
mod braced; mod braced;
mod frac; mod frac;
mod fragment; mod fragment;
@ -13,7 +14,6 @@ mod matrix;
mod op; mod op;
mod root; mod root;
mod row; mod row;
mod script;
mod spacing; mod spacing;
mod stretch; mod stretch;
mod style; mod style;
@ -22,13 +22,13 @@ mod symbols;
pub use self::accent::*; pub use self::accent::*;
pub use self::align::*; pub use self::align::*;
pub use self::atom::*; pub use self::atom::*;
pub use self::attach::*;
pub use self::braced::*; pub use self::braced::*;
pub use self::frac::*; pub use self::frac::*;
pub use self::lr::*; pub use self::lr::*;
pub use self::matrix::*; pub use self::matrix::*;
pub use self::op::*; pub use self::op::*;
pub use self::root::*; pub use self::root::*;
pub use self::script::*;
pub use self::style::*; pub use self::style::*;
use ttf_parser::GlyphId; use ttf_parser::GlyphId;
@ -54,22 +54,33 @@ use crate::text::{families, variant, FallbackList, FontFamily, SpaceNode};
pub fn module(sym: &Module) -> Module { pub fn module(sym: &Module) -> Module {
let mut math = Scope::deduplicating(); let mut math = Scope::deduplicating();
math.def_func::<FormulaNode>("formula"); math.def_func::<FormulaNode>("formula");
// Grouping.
math.def_func::<LrNode>("lr"); math.def_func::<LrNode>("lr");
math.def_func::<OpNode>("op");
math.def_func::<FloorFunc>("floor");
math.def_func::<CeilFunc>("ceil");
math.def_func::<AbsFunc>("abs"); math.def_func::<AbsFunc>("abs");
math.def_func::<NormFunc>("norm"); math.def_func::<NormFunc>("norm");
math.def_func::<FloorFunc>("floor");
math.def_func::<CeilFunc>("ceil");
// Attachments and accents.
math.def_func::<AttachNode>("attach");
math.def_func::<ScriptsNode>("scripts");
math.def_func::<LimitsNode>("limits");
math.def_func::<AccentNode>("accent"); math.def_func::<AccentNode>("accent");
math.def_func::<FracNode>("frac");
math.def_func::<BinomNode>("binom");
math.def_func::<ScriptNode>("script");
math.def_func::<SqrtNode>("sqrt");
math.def_func::<RootNode>("root");
math.def_func::<VecNode>("vec");
math.def_func::<CasesNode>("cases");
math.def_func::<UnderbraceNode>("underbrace"); math.def_func::<UnderbraceNode>("underbrace");
math.def_func::<OverbraceNode>("overbrace"); math.def_func::<OverbraceNode>("overbrace");
// Fractions and matrix-likes.
math.def_func::<FracNode>("frac");
math.def_func::<BinomNode>("binom");
math.def_func::<VecNode>("vec");
math.def_func::<CasesNode>("cases");
// Roots.
math.def_func::<SqrtNode>("sqrt");
math.def_func::<RootNode>("root");
// Styles.
math.def_func::<BoldNode>("bold"); math.def_func::<BoldNode>("bold");
math.def_func::<ItalicNode>("italic"); math.def_func::<ItalicNode>("italic");
math.def_func::<SerifNode>("serif"); math.def_func::<SerifNode>("serif");
@ -78,10 +89,16 @@ pub fn module(sym: &Module) -> Module {
math.def_func::<FrakNode>("frak"); math.def_func::<FrakNode>("frak");
math.def_func::<MonoNode>("mono"); math.def_func::<MonoNode>("mono");
math.def_func::<BbNode>("bb"); math.def_func::<BbNode>("bb");
spacing::define(&mut math);
symbols::define(&mut math); // Text operators.
math.def_func::<OpNode>("op");
op::define(&mut math); op::define(&mut math);
// Symbols and spacing.
symbols::define(&mut math);
spacing::define(&mut math);
math.copy_from(sym.scope()); math.copy_from(sym.scope());
Module::new("math").with_scope(math) Module::new("math").with_scope(math)
} }

View File

@ -9,9 +9,9 @@ use super::*;
/// - text: EcoString (positional, required) /// - text: EcoString (positional, required)
/// The operator's text. /// The operator's text.
/// - limits: bool (named) /// - 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 /// ## Category
/// math /// math
@ -21,7 +21,7 @@ use super::*;
pub struct OpNode { pub struct OpNode {
/// The operator's text. /// The operator's text.
pub text: EcoString, 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, pub limits: bool,
} }
@ -30,7 +30,7 @@ impl OpNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self { Ok(Self {
text: args.expect("text")?, text: args.expect("text")?,
limits: args.named("limits")?.unwrap_or(true), limits: args.named("limits")?.unwrap_or(false),
} }
.pack()) .pack())
} }

View File

@ -216,7 +216,7 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
Some(SyntaxKind::Formula) Some(SyntaxKind::Formula)
| Some(SyntaxKind::Math) | Some(SyntaxKind::Math)
| Some(SyntaxKind::MathFrac) | Some(SyntaxKind::MathFrac)
| Some(SyntaxKind::MathScript) | Some(SyntaxKind::MathAttach)
) { ) {
return false; return false;
} }
@ -584,7 +584,7 @@ fn complete_code(ctx: &mut CompletionContext) -> bool {
None | Some(SyntaxKind::Markup) None | Some(SyntaxKind::Markup)
| Some(SyntaxKind::Math) | Some(SyntaxKind::Math)
| Some(SyntaxKind::MathFrac) | Some(SyntaxKind::MathFrac)
| Some(SyntaxKind::MathScript) | Some(SyntaxKind::MathAttach)
) { ) {
return false; return false;
} }
@ -955,7 +955,7 @@ impl<'a> CompletionContext<'a> {
Some(SyntaxKind::Formula) Some(SyntaxKind::Formula)
| Some(SyntaxKind::Math) | Some(SyntaxKind::Math)
| Some(SyntaxKind::MathFrac) | Some(SyntaxKind::MathFrac)
| Some(SyntaxKind::MathScript) | Some(SyntaxKind::MathAttach)
); );
let scope = if in_math { self.math } else { self.global }; let scope = if in_math { self.math } else { self.global };

View File

@ -118,7 +118,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
SyntaxKind::MathAtom => None, SyntaxKind::MathAtom => None,
SyntaxKind::MathIdent => highlight_ident(node), SyntaxKind::MathIdent => highlight_ident(node),
SyntaxKind::MathDelimited => None, SyntaxKind::MathDelimited => None,
SyntaxKind::MathScript => None, SyntaxKind::MathAttach => None,
SyntaxKind::MathFrac => None, SyntaxKind::MathFrac => None,
SyntaxKind::MathAlignPoint => Some(Category::MathOperator), SyntaxKind::MathAlignPoint => Some(Category::MathOperator),
@ -143,7 +143,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
_ => Some(Category::Operator), _ => Some(Category::Operator),
}, },
SyntaxKind::Underscore => match node.parent_kind() { SyntaxKind::Underscore => match node.parent_kind() {
Some(SyntaxKind::MathScript) => Some(Category::MathOperator), Some(SyntaxKind::MathAttach) => Some(Category::MathOperator),
_ => None, _ => None,
}, },
SyntaxKind::Dollar => Some(Category::MathDelimiter), SyntaxKind::Dollar => Some(Category::MathDelimiter),
@ -213,7 +213,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
SyntaxKind::Markup SyntaxKind::Markup
| SyntaxKind::Math | SyntaxKind::Math
| SyntaxKind::MathFrac | SyntaxKind::MathFrac
| SyntaxKind::MathScript, | SyntaxKind::MathAttach,
) => Some(Category::Interpolated), ) => Some(Category::Interpolated),
Some(SyntaxKind::FieldAccess) => node.parent().and_then(highlight), Some(SyntaxKind::FieldAccess) => node.parent().and_then(highlight),
_ => None, _ => None,
@ -252,7 +252,7 @@ fn highlight_ident(node: &LinkedNode) -> Option<Category> {
SyntaxKind::Markup SyntaxKind::Markup
| SyntaxKind::Math | SyntaxKind::Math
| SyntaxKind::MathFrac | SyntaxKind::MathFrac
| SyntaxKind::MathScript, | SyntaxKind::MathAttach,
) => Some(Category::Interpolated), ) => Some(Category::Interpolated),
Some(SyntaxKind::FuncCall) => Some(Category::Function), Some(SyntaxKind::FuncCall) => Some(Category::Function),
Some(SyntaxKind::FieldAccess) Some(SyntaxKind::FieldAccess)

View File

@ -232,6 +232,17 @@ castable! {
color: Color => Self::Solid(color), 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! { castable! {
EcoString, EcoString,
string: Str => string.into(), string: Str => string.into(),

View File

@ -8,8 +8,9 @@ use comemo::{Track, Tracked, TrackedMut};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use super::{ use super::{
methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Func, Label, combining_accent, methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content,
LangItems, Module, Recipe, Scopes, Selector, StyleMap, Symbol, Transform, Value, Dict, Func, Label, LangItems, Module, Recipe, Scopes, Selector, StyleMap, Symbol,
Transform, Value,
}; };
use crate::diag::{ use crate::diag::{
bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint, bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint,
@ -347,7 +348,7 @@ impl Eval for ast::Expr {
Self::MathIdent(v) => v.eval(vm), Self::MathIdent(v) => v.eval(vm),
Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content),
Self::MathDelimited(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::MathFrac(v) => v.eval(vm).map(Value::Content),
Self::Ident(v) => v.eval(vm), Self::Ident(v) => v.eval(vm),
Self::None(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; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let base = self.base().eval(vm)?.display_in_math(); let base = self.base().eval(vm)?.display_in_math();
let sub = self let sub = self
.sub() .bottom()
.map(|expr| expr.eval(vm).map(Value::display_in_math)) .map(|expr| expr.eval(vm).map(Value::display_in_math))
.transpose()?; .transpose()?;
let sup = self let sup = self
.sup() .top()
.map(|expr| expr.eval(vm).map(Value::display_in_math)) .map(|expr| expr.eval(vm).map(Value::display_in_math))
.transpose()?; .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; type Output = Value;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let callee = self.callee(); let callee_expr = self.callee();
let callee_span = callee.span(); let callee_span = callee_expr.span();
let in_math = matches!(callee, ast::Expr::MathIdent(_)); let callee = callee_expr.eval(vm)?;
let callee = callee.eval(vm)?;
let mut args = self.args().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()); let mut body = (vm.items.math_atom)('('.into());
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 { 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( fn complete_call(
vm: &mut Vm, vm: &mut Vm,
callee: &Func, callee: &Func,

View File

@ -71,12 +71,13 @@ pub struct LangItems {
pub math_atom: fn(atom: EcoString) -> Content, pub math_atom: fn(atom: EcoString) -> Content,
/// An alignment point in a formula: `&`. /// An alignment point in a formula: `&`.
pub math_align_point: fn() -> Content, pub math_align_point: fn() -> Content,
/// A subsection in a math formula that is surrounded by matched delimiters: /// Matched delimiters surrounding math in a formula: `[x + y]`.
/// `[x + y]`.
pub math_delimited: fn(open: Content, body: Content, close: Content) -> Content, pub math_delimited: fn(open: Content, body: Content, close: Content) -> Content,
/// A base with optional sub- and superscripts in a formula: `a_1^2`. /// A base with optional attachments in a formula: `a_1^2`.
pub math_script: pub math_attach:
fn(base: Content, sub: Option<Content>, sup: Option<Content>) -> Content, 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`. /// A fraction in a formula: `x/2`.
pub math_frac: fn(num: Content, denom: Content) -> Content, pub math_frac: fn(num: Content, denom: Content) -> Content,
} }
@ -95,6 +96,8 @@ impl Hash for LangItems {
self.space.hash(state); self.space.hash(state);
self.linebreak.hash(state); self.linebreak.hash(state);
self.text.hash(state); self.text.hash(state);
self.text_id.hash(state);
(self.text_str as usize).hash(state);
self.smart_quote.hash(state); self.smart_quote.hash(state);
self.parbreak.hash(state); self.parbreak.hash(state);
self.strong.hash(state); self.strong.hash(state);
@ -108,9 +111,11 @@ impl Hash for LangItems {
self.term_item.hash(state); self.term_item.hash(state);
self.formula.hash(state); self.formula.hash(state);
self.math_atom.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_align_point.hash(state);
self.math_delimited.hash(state);
self.math_attach.hash(state);
self.math_accent.hash(state);
self.math_frac.hash(state);
} }
} }

View File

@ -78,3 +78,9 @@ impl Debug for Module {
write!(f, "<module {}>", self.name()) write!(f, "<module {}>", self.name())
} }
} }
impl PartialEq for Module {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}

View File

@ -2,7 +2,7 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use super::{Regex, Value}; use super::{format_str, Regex, Value};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart}; use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart};
use crate::util::format_eco; use crate::util::format_eco;
@ -20,10 +20,15 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
Ok(match (lhs, rhs) { Ok(match (lhs, rhs) {
(a, None) => a, (a, None) => a,
(None, b) => b, (None, b) => b,
(Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")),
(Str(a), Str(b)) => Str(a + b), (Str(a), Str(b)) => Str(a + b),
(Str(a), Content(b)) => Content(item!(text)(a.into()) + b), (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")),
(Content(a), Str(b)) => Content(a + item!(text)(b.into())), (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Content(a), Content(b)) => Content(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), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b), (Dict(a), Dict(b)) => Dict(a + b),
(a, b) => mismatch!("cannot join {} with {}", 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), (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), 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), 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())), (Content(a), Str(b)) => Content(a + item!(text)(b.into())),
(Str(a), Content(b)) => Content(item!(text)(a.into()) + b), (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), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(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, (Relative(a), Relative(b)) => a == b,
(Fraction(a), Fraction(b)) => a == b, (Fraction(a), Fraction(b)) => a == b,
(Color(a), Color(b)) => a == b, (Color(a), Color(b)) => a == b,
(Symbol(a), Symbol(b)) => a == b,
(Str(a), Str(b)) => a == b, (Str(a), Str(b)) => a == b,
(Label(a), Label(b)) => a == b, (Label(a), Label(b)) => a == b,
(Array(a), Array(b)) => a == b, (Array(a), Array(b)) => a == b,
(Dict(a), Dict(b)) => a == b, (Dict(a), Dict(b)) => a == b,
(Func(a), Func(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, (Dyn(a), Dyn(b)) => a == b,
// Some technically different things should compare equal. // Some technically different things should compare equal.

View File

@ -1,5 +1,5 @@
use std::borrow::{Borrow, Cow}; 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::hash::{Hash, Hasher};
use std::ops::{Add, AddAssign, Deref}; 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 { impl Debug for Str {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_char('"')?; f.write_char('"')?;

View File

@ -1,6 +1,6 @@
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::BTreeSet; 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::diag::StrResult;
use crate::util::EcoString; 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. /// Find the best symbol from the list.
fn find(list: &[(&str, char)], modifiers: &str) -> Option<char> { fn find(list: &[(&str, char)], modifiers: &str) -> Option<char> {
let mut best = None; let mut best = None;
@ -150,3 +156,30 @@ fn parts(modifiers: &str) -> impl Iterator<Item = &str> {
fn contained(modifiers: &str, m: &str) -> bool { fn contained(modifiers: &str, m: &str) -> bool {
parts(modifiers).any(|part| part == m) 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,
})
}

View File

@ -438,7 +438,11 @@ primitive! { Rel<Length>: "relative length",
primitive! { Fr: "fraction", Fraction } primitive! { Fr: "fraction", Fraction }
primitive! { Color: "color", Color } primitive! { Color: "color", Color }
primitive! { Symbol: "symbol", Symbol } primitive! { Symbol: "symbol", Symbol }
primitive! { Str: "string", Str } primitive! {
Str: "string",
Str,
Symbol(symbol) => symbol.get().into()
}
primitive! { Label: "label", Label } primitive! { Label: "label", Label }
primitive! { Content: "content", primitive! { Content: "content",
Content, Content,

View File

@ -121,11 +121,10 @@ pub enum Expr {
MathIdent(MathIdent), MathIdent(MathIdent),
/// An alignment point in a math formula: `&`. /// An alignment point in a math formula: `&`.
MathAlignPoint(MathAlignPoint), MathAlignPoint(MathAlignPoint),
/// A subsection in a math formula that is surrounded by matched delimiters: /// Matched delimiters surrounding math in a formula: `[x + y]`.
/// `[x + y]`.
MathDelimited(MathDelimited), MathDelimited(MathDelimited),
/// A base with optional sub- and superscripts in a math formula: `a_1^2`. /// A base with optional attachments in a formula: `a_1^2`.
MathScript(MathScript), MathAttach(MathAttach),
/// A fraction in a math formula: `x/2`. /// A fraction in a math formula: `x/2`.
MathFrac(MathFrac), MathFrac(MathFrac),
/// An identifier: `left`. /// An identifier: `left`.
@ -224,7 +223,7 @@ impl AstNode for Expr {
SyntaxKind::MathIdent => node.cast().map(Self::MathIdent), SyntaxKind::MathIdent => node.cast().map(Self::MathIdent),
SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint), SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint),
SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited), 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::MathFrac => node.cast().map(Self::MathFrac),
SyntaxKind::Ident => node.cast().map(Self::Ident), SyntaxKind::Ident => node.cast().map(Self::Ident),
SyntaxKind::None => node.cast().map(Self::None), SyntaxKind::None => node.cast().map(Self::None),
@ -285,7 +284,7 @@ impl AstNode for Expr {
Self::MathIdent(v) => v.as_untyped(), Self::MathIdent(v) => v.as_untyped(),
Self::MathAlignPoint(v) => v.as_untyped(), Self::MathAlignPoint(v) => v.as_untyped(),
Self::MathDelimited(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::MathFrac(v) => v.as_untyped(),
Self::Ident(v) => v.as_untyped(), Self::Ident(v) => v.as_untyped(),
Self::None(v) => v.as_untyped(), Self::None(v) => v.as_untyped(),
@ -709,8 +708,7 @@ node! {
} }
node! { node! {
/// A subsection in a math formula that is surrounded by matched delimiters: /// Matched delimiters surrounding math in a formula: `[x + y]`.
/// `[x + y]`.
MathDelimited MathDelimited
} }
@ -732,26 +730,26 @@ impl MathDelimited {
} }
node! { node! {
/// A base with an optional sub- and superscript in a formula: `a_1^2`. /// A base with optional attachments in a formula: `a_1^2`.
MathScript MathAttach
} }
impl MathScript { impl MathAttach {
/// The base of the script. /// The base, to which things are attached.
pub fn base(&self) -> Expr { pub fn base(&self) -> Expr {
self.0.cast_first_match().unwrap_or_default() self.0.cast_first_match().unwrap_or_default()
} }
/// The subscript. /// The bottom attachment.
pub fn sub(&self) -> Option<Expr> { pub fn bottom(&self) -> Option<Expr> {
self.0 self.0
.children() .children()
.skip_while(|node| !matches!(node.kind(), SyntaxKind::Underscore)) .skip_while(|node| !matches!(node.kind(), SyntaxKind::Underscore))
.find_map(SyntaxNode::cast) .find_map(SyntaxNode::cast)
} }
/// The superscript. /// The top attachment.
pub fn sup(&self) -> Option<Expr> { pub fn top(&self) -> Option<Expr> {
self.0 self.0
.children() .children()
.skip_while(|node| !matches!(node.kind(), SyntaxKind::Hat)) .skip_while(|node| !matches!(node.kind(), SyntaxKind::Hat))

View File

@ -65,11 +65,10 @@ pub enum SyntaxKind {
MathIdent, MathIdent,
/// An alignment point in math: `&`. /// An alignment point in math: `&`.
MathAlignPoint, MathAlignPoint,
/// A subsection in a math formula that is surrounded by matched delimiters: /// Matched delimiters surrounding math in a formula: `[x + y]`.
/// `[x + y]`.
MathDelimited, MathDelimited,
/// A base with optional sub- and superscripts in math: `a_1^2`. /// A base with optional attachments in a formula: `a_1^2`.
MathScript, MathAttach,
/// A fraction in math: `x/2`. /// A fraction in math: `x/2`.
MathFrac, MathFrac,
@ -349,7 +348,7 @@ impl SyntaxKind {
Self::MathAtom => "math atom", Self::MathAtom => "math atom",
Self::MathAlignPoint => "math alignment point", Self::MathAlignPoint => "math alignment point",
Self::MathDelimited => "delimited math", Self::MathDelimited => "delimited math",
Self::MathScript => "math script", Self::MathAttach => "math attachments",
Self::MathFrac => "math fraction", Self::MathFrac => "math fraction",
Self::Hashtag => "hashtag", Self::Hashtag => "hashtag",
Self::LeftBrace => "opening brace", Self::LeftBrace => "opening brace",

View File

@ -234,21 +234,20 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
SyntaxKind::Hashtag => embedded_code_expr(p), SyntaxKind::Hashtag => embedded_code_expr(p),
SyntaxKind::MathIdent => { SyntaxKind::MathIdent => {
p.eat(); 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() == "(" { if p.directly_at(SyntaxKind::MathAtom) && p.current_text() == "(" {
math_args(p); math_args(p);
p.wrap(m, SyntaxKind::FuncCall); 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)> { fn math_op(kind: SyntaxKind) -> Option<(SyntaxKind, SyntaxKind, ast::Assoc, usize)> {
match kind { match kind {
SyntaxKind::Underscore => { SyntaxKind::Underscore => {
Some((SyntaxKind::MathScript, SyntaxKind::Hat, ast::Assoc::Right, 2)) Some((SyntaxKind::MathAttach, SyntaxKind::Hat, ast::Assoc::Right, 2))
} }
SyntaxKind::Hat => { SyntaxKind::Hat => {
Some((SyntaxKind::MathScript, SyntaxKind::Underscore, ast::Assoc::Right, 2)) Some((SyntaxKind::MathAttach, SyntaxKind::Underscore, ast::Assoc::Right, 2))
} }
SyntaxKind::Slash => { SyntaxKind::Slash => {
Some((SyntaxKind::MathFrac, SyntaxKind::Eof, ast::Assoc::Left, 1)) Some((SyntaxKind::MathFrac, SyntaxKind::Eof, ast::Assoc::Left, 1))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -3,26 +3,13 @@
--- ---
#set page(width: auto) #set page(width: auto)
$ accent(a,`), $ grave(a),
accent(a,´), acute(a),
accent(a,\^), circum(a),
accent(a,~), tilde(a),
accent(a,¯), macron(a),
accent(a,‾), breve(a),
accent(a,˘), dot(a),
accent(a,.), diaer(a),
accent(a,¨), caron(a),
accent(a,ˇ), arrow(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) $