From 4809e685a231a3ade2c78b75685ee859196c38c1 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 28 Jan 2023 15:35:56 +0100 Subject: [PATCH] More capable math calls --- library/src/lib.rs | 7 ++-- library/src/math/accent.rs | 16 +++++++- library/src/math/atom.rs | 58 -------------------------- library/src/math/attach.rs | 54 +++++++++++++----------- library/src/math/ctx.rs | 40 ++++++++++++++++++ library/src/math/lr.rs | 80 +++++++++++++++++++++++++----------- library/src/math/mod.rs | 7 +++- library/src/math/root.rs | 2 +- library/src/prelude.rs | 2 +- src/diag.rs | 1 - src/ide/analyze.rs | 4 +- src/ide/complete.rs | 12 ++++-- src/ide/highlight.rs | 25 ++++++----- src/model/content.rs | 10 ++++- src/model/dict.rs | 21 ++++------ src/model/eval.rs | 40 +++++++----------- src/model/func.rs | 10 +++++ src/model/library.rs | 3 -- src/model/methods.rs | 76 +++++++++++++++++++++++----------- src/model/value.rs | 14 +------ src/syntax/ast.rs | 63 +++++++++++++++++++--------- src/syntax/kind.rs | 3 -- src/syntax/lexer.rs | 19 ++++++--- src/syntax/node.rs | 8 ---- src/syntax/parser.rs | 56 ++++++++++++++++++++----- src/syntax/reparser.rs | 7 ++-- tests/ref/math/simple.png | Bin 8334 -> 8333 bytes tests/typ/compiler/dict.typ | 2 +- tests/typ/compiler/for.typ | 2 +- 29 files changed, 375 insertions(+), 267 deletions(-) delete mode 100644 library/src/math/atom.rs diff --git a/library/src/lib.rs b/library/src/lib.rs index 17f3de6da..41c621cba 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -191,12 +191,11 @@ fn items() -> LangItems { layout::ListItem::Term(basics::TermItem { term, description }).pack() }, formula: |body, block| math::FormulaNode { body, block }.pack(), - 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_attach: |base, sub, sup| { - math::AttachNode { base, top: sub, bottom: sup }.pack() + math_delimited: |open, body, close| { + math::LrNode { body: open + body + close, size: None }.pack() }, + math_attach: |base, bottom, top| math::AttachNode { base, bottom, top }.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 90e64b961..fdb59c5c8 100644 --- a/library/src/math/accent.rs +++ b/library/src/math/accent.rs @@ -10,8 +10,9 @@ const ACCENT_SHORT_FALL: Em = Em::new(0.5); /// /// ## Example /// ``` +/// $grave(a) = accent(a, `)$ \ /// $arrow(a) = accent(a, arrow)$ \ -/// $grave(a) = accent(a, `)$ +/// $tilde(a) = accent(a, \u{0303})$ /// ``` /// /// ## Parameters @@ -58,11 +59,22 @@ pub struct AccentNode { impl AccentNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let base = args.expect("base")?; - let accent = args.expect("accent")?; + let accent = args.expect::("accent")?.0; Ok(Self { base, accent }.pack()) } } +struct Accent(char); + +castable! { + Accent, + v: char => Self(v), + v: Content => match v.to::() { + Some(text) => Self(Value::Str(text.0.clone().into()).cast()?), + None => Err("expected text")?, + }, +} + impl LayoutMath for AccentNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { ctx.style(ctx.style.with_cramped(true)); diff --git a/library/src/math/atom.rs b/library/src/math/atom.rs deleted file mode 100644 index 6d7359bff..000000000 --- a/library/src/math/atom.rs +++ /dev/null @@ -1,58 +0,0 @@ -use super::*; - -/// # Atom -/// An atom in a math formula: `x`, `+`, `12`. -/// -/// ## Parameters -/// - text: EcoString (positional, required) -/// The atom's text. -/// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] -pub struct AtomNode(pub EcoString); - -#[node] -impl AtomNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("text")?).pack()) - } -} - -impl LayoutMath for AtomNode { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let mut chars = self.0.chars(); - if let Some(glyph) = chars - .next() - .filter(|_| chars.next().is_none()) - .and_then(|c| GlyphFragment::try_new(ctx, c)) - { - // A single letter that is available in the math font. - if ctx.style.size == MathSize::Display - && glyph.class == Some(MathClass::Large) - { - let height = scaled!(ctx, display_operator_min_height); - ctx.push(glyph.stretch_vertical(ctx, height, Abs::zero())); - } else { - ctx.push(glyph); - } - } else if self.0.chars().all(|c| c.is_ascii_digit()) { - // A number that should respect math styling and can therefore - // not fall back to the normal text layout. - let mut vec = vec![]; - for c in self.0.chars() { - vec.push(GlyphFragment::new(ctx, c).into()); - } - let frame = MathRow(vec).to_frame(ctx); - ctx.push(frame); - } else { - // Anything else is handled by Typst's standard text layout. - let frame = ctx.layout_non_math(&TextNode(self.0.clone()).pack())?; - ctx.push(FrameFragment::new(frame).with_class(MathClass::Alphabetic)); - } - - Ok(()) - } -} diff --git a/library/src/math/attach.rs b/library/src/math/attach.rs index 2205e556e..0d7748399 100644 --- a/library/src/math/attach.rs +++ b/library/src/math/attach.rs @@ -51,17 +51,17 @@ 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()); + let mut top = Frame::new(Size::zero()); if let Some(node) = &self.top { ctx.style(ctx.style.for_subscript()); - sub = ctx.layout_frame(node)?; + top = ctx.layout_frame(node)?; ctx.unstyle(); } - let mut sup = Frame::new(Size::zero()); + let mut bottom = Frame::new(Size::zero()); if let Some(node) = &self.bottom { ctx.style(ctx.style.for_superscript()); - sup = ctx.layout_frame(node)?; + bottom = ctx.layout_frame(node)?; ctx.unstyle(); } @@ -76,9 +76,9 @@ impl LayoutMath for AttachNode { }); if render_limits { - limits(ctx, base, sub, sup) + limits(ctx, base, top, bottom) } else { - scripts(ctx, base, sub, sup, self.top.is_some() && self.bottom.is_some()) + scripts(ctx, base, top, bottom, self.top.is_some() && self.bottom.is_some()) } } } @@ -151,8 +151,8 @@ impl LayoutMath for LimitsNode { fn scripts( ctx: &mut MathContext, base: MathFragment, - sub: Frame, sup: Frame, + sub: Frame, both: bool, ) -> SourceResult<()> { let sup_shift_up = if ctx.style.cramped { @@ -191,21 +191,28 @@ fn scripts( } } - let delta = base.italics_correction(); + let italics = base.italics_correction(); + let top_delta = match base.class() { + Some(MathClass::Large) => Abs::zero(), + _ => italics, + }; + let bottom_delta = -italics; let ascent = shift_up + sup.ascent(); let descent = shift_down + sub.descent(); let height = ascent + descent; - let width = base.width() + sup.width().max(sub.width() - delta) + space_after; + let width = base.width() + + (sup.width() + top_delta).max(sub.width() + bottom_delta) + + space_after; let base_pos = Point::with_y(ascent - base.ascent()); - let sup_pos = Point::with_x(base.width()); - let sub_pos = Point::new(base.width() - delta, height - sub.height()); + let sup_pos = Point::with_x(base.width() + top_delta); + let sub_pos = Point::new(base.width() + bottom_delta, height - sub.height()); let class = base.class().unwrap_or(MathClass::Normal); let mut frame = Frame::new(Size::new(width, height)); frame.set_baseline(ascent); frame.push_frame(base_pos, base.to_frame(ctx)); - frame.push_frame(sub_pos, sub); frame.push_frame(sup_pos, sup); + frame.push_frame(sub_pos, sub); ctx.push(FrameFragment::new(frame).with_class(class)); Ok(()) @@ -215,30 +222,31 @@ fn scripts( fn limits( ctx: &mut MathContext, base: MathFragment, - sub: Frame, - sup: Frame, + top: Frame, + bottom: Frame, ) -> SourceResult<()> { let upper_gap_min = scaled!(ctx, upper_limit_gap_min); let upper_rise_min = scaled!(ctx, upper_limit_baseline_rise_min); let lower_gap_min = scaled!(ctx, lower_limit_gap_min); let lower_drop_min = scaled!(ctx, lower_limit_baseline_drop_min); - let sup_gap = upper_gap_min.max(upper_rise_min - sup.descent()); - let sub_gap = lower_gap_min.max(lower_drop_min - sub.ascent()); + let top_gap = upper_gap_min.max(upper_rise_min - top.descent()); + let bottom_gap = lower_gap_min.max(lower_drop_min - bottom.ascent()); let delta = base.italics_correction() / 2.0; - let width = base.width().max(sup.width()).max(sub.width()); - let height = sup.height() + sup_gap + base.height() + sub_gap + sub.height(); - let base_pos = Point::new((width - base.width()) / 2.0, sup.height() + sup_gap); - let sup_pos = Point::with_x((width - sup.width()) / 2.0 + delta); - let sub_pos = Point::new((width - sub.width()) / 2.0 - delta, height - sub.height()); + let width = base.width().max(top.width()).max(bottom.width()); + let height = top.height() + top_gap + base.height() + bottom_gap + bottom.height(); + let base_pos = Point::new((width - base.width()) / 2.0, top.height() + top_gap); + let sup_pos = Point::with_x((width - top.width()) / 2.0 + delta); + let sub_pos = + Point::new((width - bottom.width()) / 2.0 - delta, height - bottom.height()); let class = base.class().unwrap_or(MathClass::Normal); let mut frame = Frame::new(Size::new(width, height)); frame.set_baseline(base_pos.y + base.ascent()); frame.push_frame(base_pos, base.to_frame(ctx)); - frame.push_frame(sub_pos, sub); - frame.push_frame(sup_pos, sup); + frame.push_frame(sub_pos, bottom); + frame.push_frame(sup_pos, top); ctx.push(FrameFragment::new(frame).with_class(class)); Ok(()) diff --git a/library/src/math/ctx.rs b/library/src/math/ctx.rs index 551d6cd74..547d3cc85 100644 --- a/library/src/math/ctx.rs +++ b/library/src/math/ctx.rs @@ -1,4 +1,5 @@ use ttf_parser::math::MathValue; +use unicode_segmentation::UnicodeSegmentation; use super::*; @@ -129,6 +130,45 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { Ok(self.layout_fragment(node)?.to_frame(self)) } + pub fn layout_text(&mut self, text: &EcoString) -> SourceResult<()> { + let mut chars = text.chars(); + if let Some(glyph) = chars + .next() + .filter(|_| chars.next().is_none()) + .and_then(|c| GlyphFragment::try_new(self, c)) + { + // A single letter that is available in the math font. + if self.style.size == MathSize::Display + && glyph.class == Some(MathClass::Large) + { + let height = scaled!(self, display_operator_min_height); + self.push(glyph.stretch_vertical(self, height, Abs::zero())); + } else { + self.push(glyph); + } + } else if text.chars().all(|c| c.is_ascii_digit()) { + // A number that should respect math styling and can therefore + // not fall back to the normal text layout. + let mut vec = vec![]; + for c in text.chars() { + vec.push(GlyphFragment::new(self, c).into()); + } + let frame = MathRow(vec).to_frame(self); + self.push(frame); + } else { + // Anything else is handled by Typst's standard text layout. + let spaced = text.graphemes(true).count() > 1; + let frame = self.layout_non_math(&TextNode::packed(text.clone()))?; + self.push( + FrameFragment::new(frame) + .with_class(MathClass::Alphabetic) + .with_spaced(spaced), + ); + } + + Ok(()) + } + pub fn size(&self) -> Abs { self.scaled_size } diff --git a/library/src/math/lr.rs b/library/src/math/lr.rs index d5951050f..a33810df2 100644 --- a/library/src/math/lr.rs +++ b/library/src/math/lr.rs @@ -12,18 +12,29 @@ pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); /// ## Example /// ``` /// $ lr(]a, b/2]) $ +/// $ lr(]sum_(x=1)^n] x, #size: 50%) $ /// ``` /// /// ## Parameters /// - body: Content (positional, variadic) /// The delimited content, including the delimiters. /// +/// - size: Rel (named) +/// The size of the brackets, relative to the height of the wrapped content. +/// +/// Defaults to `{100%}`. +/// /// ## Category /// math #[func] #[capable(LayoutMath)] #[derive(Debug, Hash)] -pub struct LrNode(pub Content); +pub struct LrNode { + /// The delimited content, including the delimiters. + pub body: Content, + /// The size of the brackets. + pub size: Option>, +} #[node] impl LrNode { @@ -31,17 +42,18 @@ impl LrNode { let mut body = Content::empty(); for (i, arg) in args.all::()?.into_iter().enumerate() { if i > 0 { - body += AtomNode(','.into()).pack(); + body += TextNode::packed(','); } body += arg; } - Ok(Self(body).pack()) + let size = args.named("size")?; + Ok(Self { body, size }.pack()) } } impl LayoutMath for LrNode { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let mut row = ctx.layout_row(&self.0)?; + let mut row = ctx.layout_row(&self.body)?; let axis = scaled!(ctx, axis_height); let max_extent = row @@ -51,22 +63,19 @@ impl LayoutMath for LrNode { .max() .unwrap_or_default(); - let height = 2.0 * max_extent; - if let [first, .., last] = row.0.as_mut_slice() { - for fragment in [first, last] { - if !matches!( - fragment.class(), - Some(MathClass::Opening | MathClass::Closing | MathClass::Fence) - ) { - continue; - } + let height = self + .size + .unwrap_or(Rel::one()) + .resolve(ctx.outer.chain(&ctx.map)) + .relative_to(2.0 * max_extent); - let MathFragment::Glyph(glyph) = *fragment else { continue }; - let short_fall = DELIM_SHORT_FALL.scaled(ctx); - *fragment = MathFragment::Variant( - glyph.stretch_vertical(ctx, height, short_fall), - ); + match row.0.as_mut_slice() { + [one] => scale(ctx, one, height, None), + [first, .., last] => { + scale(ctx, first, height, Some(MathClass::Opening)); + scale(ctx, last, height, Some(MathClass::Closing)); } + _ => {} } ctx.extend(row); @@ -75,6 +84,28 @@ impl LayoutMath for LrNode { } } +/// Scale a math fragment to a height. +fn scale( + ctx: &mut MathContext, + fragment: &mut MathFragment, + height: Abs, + apply: Option, +) { + if matches!( + fragment.class(), + Some(MathClass::Opening | MathClass::Closing | MathClass::Fence) + ) { + let MathFragment::Glyph(glyph) = *fragment else { return }; + let short_fall = DELIM_SHORT_FALL.scaled(ctx); + *fragment = + MathFragment::Variant(glyph.stretch_vertical(ctx, height, short_fall)); + + if let Some(class) = apply { + fragment.set_class(class); + } + } +} + /// # Floor /// Floor an expression. /// @@ -153,11 +184,14 @@ pub fn norm(args: &mut Args) -> SourceResult { fn delimited(args: &mut Args, left: char, right: char) -> SourceResult { Ok(Value::Content( - LrNode(Content::sequence(vec![ - AtomNode(left.into()).pack(), - args.expect::("body")?, - AtomNode(right.into()).pack(), - ])) + LrNode { + body: Content::sequence(vec![ + TextNode::packed(left), + args.expect::("body")?, + TextNode::packed(right), + ]), + size: None, + } .pack(), )) } diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 636affed1..4c002cead 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -4,7 +4,6 @@ mod ctx; mod accent; mod align; -mod atom; mod attach; mod braced; mod frac; @@ -21,7 +20,6 @@ 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::*; @@ -263,6 +261,11 @@ impl LayoutMath for Content { return Ok(()); } + if let Some(node) = self.to::() { + ctx.layout_text(&node.0)?; + return Ok(()); + } + if let Some(node) = self.to::() { for child in &node.0 { child.layout_math(ctx)?; diff --git a/library/src/math/root.rs b/library/src/math/root.rs index bae3bbb67..79bcfe385 100644 --- a/library/src/math/root.rs +++ b/library/src/math/root.rs @@ -155,7 +155,7 @@ fn layout( /// Select a precomposed radical, if the font has it. fn precomposed(ctx: &MathContext, index: Option<&Content>, target: Abs) -> Option { - let node = index?.to::()?; + let node = index?.to::()?; let c = match node.0.as_str() { "3" => '∛', "4" => '∜', diff --git a/library/src/prelude.rs b/library/src/prelude.rs index 36d49dbff..b8f6025ae 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -18,7 +18,7 @@ pub use typst::model::{ array, capability, capable, castable, dict, format_str, func, node, Args, Array, AutoValue, Cast, CastInfo, Content, Dict, Finalize, Fold, Func, Introspector, Label, Node, NodeId, NoneValue, Prepare, Resolve, Selector, Show, StabilityProvider, Str, - StyleChain, StyleMap, StyleVec, Unlabellable, Value, Vm, Vt, + StyleChain, StyleMap, StyleVec, Symbol, Unlabellable, Value, Vm, Vt, }; #[doc(no_inline)] pub use typst::syntax::{Span, Spanned}; diff --git a/src/diag.rs b/src/diag.rs index 054a7b03b..1cf2f85b5 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -68,7 +68,6 @@ impl SourceError { /// Create a new, bare error. #[track_caller] pub fn new(span: Span, message: impl Into) -> Self { - assert!(!span.is_detached()); Self { span, pos: ErrorPos::Full, diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs index a1ac5778a..c170186fe 100644 --- a/src/ide/analyze.rs +++ b/src/ide/analyze.rs @@ -7,7 +7,9 @@ use crate::World; /// Try to determine a set of possible values for an expression. pub fn analyze(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { match node.cast::() { - Some(ast::Expr::Ident(_) | ast::Expr::MathIdent(_)) => { + Some( + ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::MethodCall(_), + ) => { if let Some(parent) = node.parent() { if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 { return analyze(world, parent); diff --git a/src/ide/complete.rs b/src/ide/complete.rs index 83d0ca9c2..83202e30f 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -229,7 +229,7 @@ fn complete_math(ctx: &mut CompletionContext) -> bool { } // Behind existing atom or identifier: "$a|$" or "$abc|$". - if matches!(ctx.leaf.kind(), SyntaxKind::MathAtom | SyntaxKind::MathIdent) { + if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { ctx.from = ctx.leaf.offset(); math_completions(ctx); return true; @@ -274,7 +274,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { // Behind an expression plus dot: "emoji.|". if_chain! { if ctx.leaf.kind() == SyntaxKind::Dot - || (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathAtom) + || (ctx.leaf.kind() == SyntaxKind::Text && ctx.leaf.text() == "."); if ctx.leaf.range().end == ctx.cursor; if let Some(prev) = ctx.leaf.prev_sibling(); @@ -326,11 +326,15 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { } } _ => { - for &method in methods_on(value.type_name()) { + for &(method, args) in methods_on(value.type_name()) { ctx.completions.push(Completion { kind: CompletionKind::Func, label: method.into(), - apply: Some(format_eco!("{method}(${{}})")), + apply: Some(if args { + format_eco!("{method}(${{}})") + } else { + format_eco!("{method}()${{}}") + }), detail: None, }) } diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs index 7f7ad6eec..d8f15f000 100644 --- a/src/ide/highlight.rs +++ b/src/ide/highlight.rs @@ -1,4 +1,4 @@ -use crate::syntax::{LinkedNode, SyntaxKind}; +use crate::syntax::{ast, LinkedNode, SyntaxKind}; /// Syntax highlighting categories. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -115,19 +115,17 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::Formula => None, SyntaxKind::Math => None, - SyntaxKind::MathAtom => None, SyntaxKind::MathIdent => highlight_ident(node), SyntaxKind::MathDelimited => None, SyntaxKind::MathAttach => None, SyntaxKind::MathFrac => None, SyntaxKind::MathAlignPoint => Some(Category::MathOperator), - SyntaxKind::Hashtag if node.before_error() => None, SyntaxKind::Hashtag => node - .next_leaf() - .filter(|node| node.kind() != SyntaxKind::Dollar) - .as_ref() - .and_then(highlight), + .next_sibling() + .filter(|node| node.cast::().map_or(false, |e| e.hashtag())) + .and_then(|node| node.leftmost_leaf()) + .and_then(|node| highlight(&node)), SyntaxKind::LeftBrace => Some(Category::Punctuation), SyntaxKind::RightBrace => Some(Category::Punctuation), @@ -248,12 +246,6 @@ pub fn highlight(node: &LinkedNode) -> Option { /// Highlight an identifier based on context. fn highlight_ident(node: &LinkedNode) -> Option { match node.parent_kind() { - Some( - SyntaxKind::Markup - | SyntaxKind::Math - | SyntaxKind::MathFrac - | SyntaxKind::MathAttach, - ) => Some(Category::Interpolated), Some(SyntaxKind::FuncCall) => Some(Category::Function), Some(SyntaxKind::FieldAccess) if node.parent().and_then(|p| p.parent_kind()) @@ -287,6 +279,13 @@ fn highlight_ident(node: &LinkedNode) -> Option { { Some(Category::Function) } + Some( + SyntaxKind::Markup + | SyntaxKind::Math + | SyntaxKind::MathFrac + | SyntaxKind::MathAttach, + ) => Some(Category::Interpolated), + _ if node.kind() == SyntaxKind::MathIdent => Some(Category::Interpolated), _ => None, } } diff --git a/src/model/content.rs b/src/model/content.rs index df910a581..143f97aab 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -183,12 +183,18 @@ impl Content { } /// Whether the contained node is of type `T`. - pub fn is(&self) -> bool { + pub fn is(&self) -> bool + where + T: Capable + 'static, + { (*self.obj).as_any().is::() } /// Cast to `T` if the contained node is of type `T`. - pub fn to(&self) -> Option<&T> { + pub fn to(&self) -> Option<&T> + where + T: Capable + 'static, + { (*self.obj).as_any().downcast_ref::() } diff --git a/src/model/dict.rs b/src/model/dict.rs index 76d194a89..7165fbbe4 100644 --- a/src/model/dict.rs +++ b/src/model/dict.rs @@ -3,8 +3,8 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::ops::{Add, AddAssign}; use std::sync::Arc; -use super::{Args, Array, Func, Str, Value, Vm}; -use crate::diag::{bail, SourceResult, StrResult}; +use super::{array, Array, Str, Value}; +use crate::diag::StrResult; use crate::syntax::is_ident; use crate::util::{format_eco, ArcExt, EcoString}; @@ -104,17 +104,12 @@ impl Dict { self.0.values().cloned().collect() } - /// Transform each pair in the dictionary with a function. - pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult { - if func.argc().map_or(false, |count| count != 2) { - bail!(func.span(), "function must have exactly two parameters"); - } - self.iter() - .map(|(key, value)| { - let args = - Args::new(func.span(), [Value::Str(key.clone()), value.clone()]); - func.call(vm, args) - }) + /// Return the values of the dictionary as an array of pairs (arrays of + /// length two). + pub fn pairs(&self) -> Array { + self.0 + .iter() + .map(|(k, v)| Value::Array(array![k.clone(), v.clone()])) .collect() } diff --git a/src/model/eval.rs b/src/model/eval.rs index 44f08a76b..b63069bfe 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -344,7 +344,6 @@ impl Eval for ast::Expr { Self::Term(v) => v.eval(vm).map(Value::Content), Self::Formula(v) => v.eval(vm).map(Value::Content), Self::Math(v) => v.eval(vm).map(Value::Content), - Self::MathAtom(v) => v.eval(vm).map(Value::Content), Self::MathIdent(v) => v.eval(vm), Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), Self::MathDelimited(v) => v.eval(vm).map(Value::Content), @@ -552,21 +551,13 @@ impl Eval for ast::Math { fn eval(&self, vm: &mut Vm) -> SourceResult { Ok(Content::sequence( self.exprs() - .map(|expr| Ok(expr.eval(vm)?.display_in_math())) + .map(|expr| Ok(expr.eval(vm)?.display())) .collect::>()?, ) .spanned(self.span())) } } -impl Eval for ast::MathAtom { - type Output = Content; - - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.math_atom)(self.get().clone())) - } -} - impl Eval for ast::MathIdent { type Output = Value; @@ -587,9 +578,9 @@ impl Eval for ast::MathDelimited { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - let open = self.open().eval(vm)?.display_in_math(); + let open = self.open().eval(vm)?.display(); let body = self.body().eval(vm)?; - let close = self.close().eval(vm)?.display_in_math(); + let close = self.close().eval(vm)?.display(); Ok((vm.items.math_delimited)(open, body, close)) } } @@ -598,16 +589,13 @@ 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 + let base = self.base().eval(vm)?.display(); + let bottom = self .bottom() - .map(|expr| expr.eval(vm).map(Value::display_in_math)) + .map(|expr| expr.eval(vm).map(Value::display)) .transpose()?; - let sup = self - .top() - .map(|expr| expr.eval(vm).map(Value::display_in_math)) - .transpose()?; - Ok((vm.items.math_attach)(base, sub, sup)) + let top = self.top().map(|expr| expr.eval(vm).map(Value::display)).transpose()?; + Ok((vm.items.math_attach)(base, bottom, top)) } } @@ -615,8 +603,8 @@ impl Eval for ast::MathFrac { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - let num = self.num().eval(vm)?.display_in_math(); - let denom = self.denom().eval(vm)?.display_in_math(); + let num = self.num().eval(vm)?.display(); + let denom = self.denom().eval(vm)?.display(); Ok((vm.items.math_frac)(num, denom)) } } @@ -945,15 +933,15 @@ impl Eval for ast::FuncCall { } } - let mut body = (vm.items.math_atom)('('.into()); + let mut body = (vm.items.text)('('.into()); for (i, arg) in args.all::()?.into_iter().enumerate() { if i > 0 { - body += (vm.items.math_atom)(','.into()); + body += (vm.items.text)(','.into()); } body += arg; } - body += (vm.items.math_atom)(')'.into()); - return Ok(Value::Content(callee.display_in_math() + body)); + body += (vm.items.text)(')'.into()); + return Ok(Value::Content(callee.display() + body)); } let callee = callee.cast::().at(callee_span)?; diff --git a/src/model/func.rs b/src/model/func.rs index 8cf3ea991..1ccb01070 100644 --- a/src/model/func.rs +++ b/src/model/func.rs @@ -389,6 +389,7 @@ impl<'a> CapturesVisitor<'a> { // actually bind a new name are handled below (individually through // the expressions that contain them). Some(ast::Expr::Ident(ident)) => self.capture(ident), + Some(ast::Expr::MathIdent(ident)) => self.capture_in_math(ident), // Code and content blocks create a scope. Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { @@ -483,6 +484,15 @@ impl<'a> CapturesVisitor<'a> { } } } + + /// Capture a variable in math mode if it isn't internal. + fn capture_in_math(&mut self, ident: ast::MathIdent) { + if self.internal.get(&ident).is_err() { + if let Ok(value) = self.external.get_in_math(&ident) { + self.captures.define_captured(ident.take(), value.clone()); + } + } + } } #[cfg(test)] diff --git a/src/model/library.rs b/src/model/library.rs index 4208a4c74..c87ca095c 100644 --- a/src/model/library.rs +++ b/src/model/library.rs @@ -67,8 +67,6 @@ pub struct LangItems { pub term_item: fn(term: Content, description: Content) -> Content, /// A mathematical formula: `$x$`, `$ x^2 $`. pub formula: fn(body: Content, block: bool) -> Content, - /// An atom in a formula: `x`, `+`, `12`. - pub math_atom: fn(atom: EcoString) -> Content, /// An alignment point in a formula: `&`. pub math_align_point: fn() -> Content, /// Matched delimiters surrounding math in a formula: `[x + y]`. @@ -110,7 +108,6 @@ impl Hash for LangItems { self.enum_item.hash(state); self.term_item.hash(state); self.formula.hash(state); - self.math_atom.hash(state); self.math_align_point.hash(state); self.math_delimited.hash(state); self.math_attach.hash(state); diff --git a/src/model/methods.rs b/src/model/methods.rs index 1671a5c43..5da64fa2f 100644 --- a/src/model/methods.rs +++ b/src/model/methods.rs @@ -107,7 +107,7 @@ pub fn call( "at" => dict.at(&args.expect::("key")?).cloned().at(span)?, "keys" => Value::Array(dict.keys()), "values" => Value::Array(dict.values()), - "pairs" => Value::Array(dict.map(vm, args.expect("function")?)?), + "pairs" => Value::Array(dict.pairs()), _ => return missing(), }, @@ -211,35 +211,61 @@ fn missing_method(type_name: &str, method: &str) -> String { format!("type {type_name} has no method `{method}`") } -/// List the available methods for a type. -pub fn methods_on(type_name: &str) -> &[&'static str] { +/// List the available methods for a type and whether they take arguments. +pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { match type_name { - "color" => &["lighten", "darken", "negate"], + "color" => &[("lighten", true), ("darken", true), ("negate", false)], "string" => &[ - "len", - "at", - "contains", - "ends-with", - "find", - "first", - "last", - "match", - "matches", - "position", - "replace", - "slice", - "split", - "starts-with", - "trim", + ("len", false), + ("at", true), + ("contains", true), + ("ends-with", true), + ("find", true), + ("first", false), + ("last", false), + ("match", true), + ("matches", true), + ("position", true), + ("replace", true), + ("slice", true), + ("split", true), + ("starts-with", true), + ("trim", true), ], "array" => &[ - "all", "any", "at", "contains", "filter", "find", "first", "flatten", "fold", - "insert", "join", "last", "len", "map", "pop", "position", "push", "remove", - "rev", "slice", "sorted", + ("all", true), + ("any", true), + ("at", true), + ("contains", true), + ("filter", true), + ("find", true), + ("first", false), + ("flatten", false), + ("fold", true), + ("insert", true), + ("join", true), + ("last", false), + ("len", false), + ("map", true), + ("pop", false), + ("position", true), + ("push", true), + ("remove", true), + ("rev", false), + ("slice", true), + ("sorted", false), ], - "dictionary" => &["at", "insert", "keys", "len", "pairs", "remove", "values"], - "function" => &["where", "with"], - "arguments" => &["named", "pos"], + "dictionary" => &[ + ("at", true), + ("insert", true), + ("keys", false), + ("len", false), + ("pairs", false), + ("remove", true), + ("values", false), + ], + "function" => &[("where", true), ("with", true)], + "arguments" => &[("named", false), ("pos", false)], _ => &[], } } diff --git a/src/model/value.rs b/src/model/value.rs index 4b9fa5f74..ea17349e1 100644 --- a/src/model/value.rs +++ b/src/model/value.rs @@ -145,16 +145,6 @@ impl Value { } } - /// Return the display representation of the value in math mode. - pub fn display_in_math(self) -> Content { - match self { - Self::Int(v) => item!(math_atom)(format_eco!("{}", v)), - Self::Float(v) => item!(math_atom)(format_eco!("{}", v)), - Self::Symbol(v) => item!(math_atom)(v.get().into()), - _ => self.display(), - } - } - /// Try to extract documentation for the value. pub fn docs(&self) -> Option<&'static str> { match self { @@ -447,8 +437,8 @@ primitive! { Label: "label", Label } primitive! { Content: "content", Content, None => Content::empty(), - Symbol(symbol) => item!(text)(symbol.get().into()), - Str(text) => item!(text)(text.into()) + Symbol(v) => item!(text)(v.get().into()), + Str(v) => item!(text)(v.into()) } primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 45f79685d..5704f1712 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -115,8 +115,6 @@ pub enum Expr { Formula(Formula), /// A math formula: `$x$`, `$ x^2 $`. Math(Math), - /// An atom in a math formula: `x`, `+`, `12`. - MathAtom(MathAtom), /// An identifier in a math formula: `pi`. MathIdent(MathIdent), /// An alignment point in a math formula: `&`. @@ -219,7 +217,6 @@ impl AstNode for Expr { SyntaxKind::TermItem => node.cast().map(Self::Term), SyntaxKind::Formula => node.cast().map(Self::Formula), SyntaxKind::Math => node.cast().map(Self::Math), - SyntaxKind::MathAtom => node.cast().map(Self::MathAtom), SyntaxKind::MathIdent => node.cast().map(Self::MathIdent), SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint), SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited), @@ -280,7 +277,6 @@ impl AstNode for Expr { Self::Term(v) => v.as_untyped(), Self::Formula(v) => v.as_untyped(), Self::Math(v) => v.as_untyped(), - Self::MathAtom(v) => v.as_untyped(), Self::MathIdent(v) => v.as_untyped(), Self::MathAlignPoint(v) => v.as_untyped(), Self::MathDelimited(v) => v.as_untyped(), @@ -320,6 +316,42 @@ impl AstNode for Expr { } } +impl Expr { + /// Can this expression be embedded into markup with a hashtag? + pub fn hashtag(&self) -> bool { + match self { + Self::Ident(_) => true, + Self::None(_) => true, + Self::Auto(_) => true, + Self::Bool(_) => true, + Self::Int(_) => true, + Self::Float(_) => true, + Self::Numeric(_) => true, + Self::Str(_) => true, + Self::Code(_) => true, + Self::Content(_) => true, + Self::Array(_) => true, + Self::Dict(_) => true, + Self::Parenthesized(_) => true, + Self::FieldAccess(_) => true, + Self::FuncCall(_) => true, + Self::MethodCall(_) => true, + Self::Let(_) => true, + Self::Set(_) => true, + Self::Show(_) => true, + Self::Conditional(_) => true, + Self::While(_) => true, + Self::For(_) => true, + Self::Import(_) => true, + Self::Include(_) => true, + Self::Break(_) => true, + Self::Continue(_) => true, + Self::Return(_) => true, + _ => false, + } + } +} + impl Default for Expr { fn default() -> Self { Expr::Space(Space::default()) @@ -393,18 +425,23 @@ impl Shorthand { "..." => '…', "*" => '∗', "!=" => '≠', + "<<" => '≪', + "<<<" => '⋘', + ">>" => '≫', + ">>>" => '⋙', "<=" => '≤', ">=" => '≥', "<-" => '←', "->" => '→', "=>" => '⇒', + "|->" => '↦', + "|=>" => '⤇', + "<->" => '↔', + "<=>" => '⇔', ":=" => '≔', "[|" => '⟦', "|]" => '⟧', "||" => '‖', - "|->" => '↦', - "<->" => '↔', - "<=>" => '⇔', _ => char::default(), } } @@ -660,18 +697,6 @@ impl Math { } } -node! { - /// A atom in a formula: `x`, `+`, `12`. - MathAtom -} - -impl MathAtom { - /// Get the atom's text. - pub fn get(&self) -> &EcoString { - self.0.text() - } -} - node! { /// An identifier in a math formula: `pi`. MathIdent diff --git a/src/syntax/kind.rs b/src/syntax/kind.rs index aa4a5cfe6..b2b65a626 100644 --- a/src/syntax/kind.rs +++ b/src/syntax/kind.rs @@ -59,8 +59,6 @@ pub enum SyntaxKind { /// Mathematical markup. Math, - /// An atom in math: `x`, `+`, `12`. - MathAtom, /// An identifier in math: `pi`. MathIdent, /// An alignment point in math: `&`. @@ -345,7 +343,6 @@ impl SyntaxKind { Self::Formula => "math formula", Self::Math => "math", Self::MathIdent => "math identifier", - Self::MathAtom => "math atom", Self::MathAlignPoint => "math alignment point", Self::MathDelimited => "delimited math", Self::MathAttach => "math attachments", diff --git a/src/syntax/lexer.rs b/src/syntax/lexer.rs index d4548b8b6..d267a05bd 100644 --- a/src/syntax/lexer.rs +++ b/src/syntax/lexer.rs @@ -380,21 +380,28 @@ impl Lexer<'_> { '\\' => self.backslash(), '"' => self.string(), + '*' => SyntaxKind::Shorthand, '.' if self.s.eat_if("..") => SyntaxKind::Shorthand, '|' if self.s.eat_if("->") => SyntaxKind::Shorthand, - '<' if self.s.eat_if("->") => SyntaxKind::Shorthand, - '<' if self.s.eat_if("=>") => SyntaxKind::Shorthand, + '|' if self.s.eat_if("=>") => SyntaxKind::Shorthand, '!' if self.s.eat_if('=') => SyntaxKind::Shorthand, + '<' if self.s.eat_if("<<") => SyntaxKind::Shorthand, + '<' if self.s.eat_if('<') => SyntaxKind::Shorthand, + '>' if self.s.eat_if(">>") => SyntaxKind::Shorthand, + '>' if self.s.eat_if('>') => SyntaxKind::Shorthand, + + '<' if self.s.eat_if("=>") => SyntaxKind::Shorthand, + '<' if self.s.eat_if("->") => SyntaxKind::Shorthand, '<' if self.s.eat_if('=') => SyntaxKind::Shorthand, '>' if self.s.eat_if('=') => SyntaxKind::Shorthand, '<' if self.s.eat_if('-') => SyntaxKind::Shorthand, '-' if self.s.eat_if('>') => SyntaxKind::Shorthand, '=' if self.s.eat_if('>') => SyntaxKind::Shorthand, + ':' if self.s.eat_if('=') => SyntaxKind::Shorthand, '[' if self.s.eat_if('|') => SyntaxKind::Shorthand, '|' if self.s.eat_if(']') => SyntaxKind::Shorthand, '|' if self.s.eat_if('|') => SyntaxKind::Shorthand, - '*' => SyntaxKind::Shorthand, '#' if !self.s.at(char::is_whitespace) => SyntaxKind::Hashtag, '_' => SyntaxKind::Underscore, @@ -410,11 +417,11 @@ impl Lexer<'_> { } // Other math atoms. - _ => self.atom(start, c), + _ => self.math_text(start, c), } } - fn atom(&mut self, start: usize, c: char) -> SyntaxKind { + fn math_text(&mut self, start: usize, c: char) -> SyntaxKind { // Keep numbers and grapheme clusters together. if c.is_numeric() { self.s.eat_while(char::is_numeric); @@ -427,7 +434,7 @@ impl Lexer<'_> { .map_or(0, str::len); self.s.jump(start + len); } - SyntaxKind::MathAtom + SyntaxKind::Text } } diff --git a/src/syntax/node.rs b/src/syntax/node.rs index a0fa5e1e3..ed0007882 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -713,14 +713,6 @@ impl<'a> LinkedNode<'a> { Some(next) } } - - /// Whether an error follows directly after the node. - pub fn before_error(&self) -> bool { - let Some(parent) = self.parent() else { return false }; - let Some(index) = self.index.checked_add(1) else { return false }; - let Some(node) = parent.node.children().nth(index) else { return false }; - node.kind().is_error() - } } /// Access to leafs. diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index 077305337..1b5c10a3f 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -234,24 +234,24 @@ 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) + while p.directly_at(SyntaxKind::Text) && p.current_text() == "." && matches!( p.lexer.clone().next(), - SyntaxKind::MathIdent | SyntaxKind::MathAtom + SyntaxKind::MathIdent | SyntaxKind::Text ) { 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::Text) && p.current_text() == "(" { math_args(p); p.wrap(m, SyntaxKind::FuncCall); } } - SyntaxKind::MathAtom | SyntaxKind::Shorthand => { + SyntaxKind::Text | SyntaxKind::Shorthand => { if math_class(p.current_text()) == Some(MathClass::Fence) { math_delimited(p, MathClass::Fence) } else if math_class(p.current_text()) == Some(MathClass::Opening) { @@ -374,16 +374,32 @@ fn math_op(kind: SyntaxKind) -> Option<(SyntaxKind, SyntaxKind, ast::Assoc, usiz } fn math_args(p: &mut Parser) { - p.assert(SyntaxKind::MathAtom); + p.assert(SyntaxKind::Text); + let m = p.marker(); - let mut m2 = p.marker(); + let mut arg = p.marker(); + let mut namable = true; + let mut named = None; + while !p.eof() && !p.at(SyntaxKind::Dollar) { + if namable + && (p.at(SyntaxKind::MathIdent) || p.at(SyntaxKind::Text)) + && p.text[p.current_end()..].starts_with(':') + { + p.convert(SyntaxKind::Ident); + p.convert(SyntaxKind::Colon); + named = Some(arg); + arg = p.marker(); + } + match p.current_text() { ")" => break, "," => { - p.wrap(m2, SyntaxKind::Math); + maybe_wrap_in_math(p, arg, named); p.convert(SyntaxKind::Comma); - m2 = p.marker(); + arg = p.marker(); + namable = true; + named = None; continue; } _ => {} @@ -394,12 +410,30 @@ fn math_args(p: &mut Parser) { if !p.progress(prev) { p.unexpected(); } + + namable = false; } - if m2 != p.marker() { - p.wrap(m2, SyntaxKind::Math); + + if arg != p.marker() { + maybe_wrap_in_math(p, arg, named); } + p.wrap(m, SyntaxKind::Args); - p.expect(SyntaxKind::MathAtom); + if !p.eat_if(SyntaxKind::Text) { + p.expected("closing paren"); + p.balanced = false; + } +} + +fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, named: Option) { + let exprs = p.post_process(arg).filter(|node| node.is::()).count(); + if exprs != 1 { + p.wrap(arg, SyntaxKind::Math); + } + + if let Some(m) = named { + p.wrap(m, SyntaxKind::Named); + } } fn code(p: &mut Parser, mut stop: impl FnMut(SyntaxKind) -> bool) { diff --git a/src/syntax/reparser.rs b/src/syntax/reparser.rs index de845abf1..a876e86b5 100644 --- a/src/syntax/reparser.rs +++ b/src/syntax/reparser.rs @@ -99,9 +99,9 @@ fn try_reparse( && (parent_kind.is_none() || parent_kind == Some(SyntaxKind::ContentBlock)) && !overlap.is_empty() { - // Add one node of slack in both directions. + // Add slack in both directions. let children = node.children_mut(); - let mut start = overlap.start.saturating_sub(1); + let mut start = overlap.start.saturating_sub(2); let mut end = (overlap.end + 1).min(children.len()); // Expand to the left. @@ -242,7 +242,7 @@ mod tests { #[test] fn test_reparse_markup() { - test("abc~def~ghi", 5..6, "+", true); + test("abc~def~gh~", 5..6, "+", true); test("~~~~~~~", 3..4, "A", true); test("abc~~", 1..2, "", true); test("#var. hello", 5..6, " ", false); @@ -264,7 +264,6 @@ mod tests { test("#show f: a => b..", 16..16, "c", false); test("#for", 4..4, "//", false); test("a\n#let \nb", 7..7, "i", true); - test("#let x = (1, 2 + ;~ Five\r\n\r", 20..23, "2.", true); test(r"#{{let x = z}; a = 1} b", 7..7, "//", false); test(r#"a ```typst hello```"#, 16..17, "", false); } diff --git a/tests/ref/math/simple.png b/tests/ref/math/simple.png index 4daa52e166b05482adde5c90388beac1fec6e398..a0a864af0cfde64f54adf0baa4e45190689b5693 100644 GIT binary patch literal 8333 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 J%Ah8J{{>scpEv*j literal 8334 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 Ndl@C^N=c)@{{^r|rCtC4 diff --git a/tests/typ/compiler/dict.typ b/tests/typ/compiler/dict.typ index c34b54786..02097724b 100644 --- a/tests/typ/compiler/dict.typ +++ b/tests/typ/compiler/dict.typ @@ -49,7 +49,7 @@ #test("c" in dict, true) #test(dict.len(), 3) #test(dict.values(), (3, 1, 2)) -#test(dict.pairs((k, v) => k + str(v)).join(), "a3b1c2") +#test(dict.pairs().map(p => p.first() + str(p.last())).join(), "a3b1c2") #{ dict.remove("c") } #test("c" in dict, false) diff --git a/tests/typ/compiler/for.typ b/tests/typ/compiler/for.typ index cf7174111..7833aad70 100644 --- a/tests/typ/compiler/for.typ +++ b/tests/typ/compiler/for.typ @@ -34,7 +34,7 @@ // Map captured arguments. #let f1(..args) = args.pos().map(repr) -#let f2(..args) = args.named().pairs((k, v) => repr(k) + ": " + repr(v)) +#let f2(..args) = args.named().pairs().map(p => repr(p.first()) + ": " + repr(p.last())) #let f(..args) = (f1(..args) + f2(..args)).join(", ") #f(1, a: 2)