From 3ecb0c754bc1777e002a43e4c34b27e676f9a95c Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 6 Dec 2022 12:37:08 +0100 Subject: [PATCH] More math syntax --- Cargo.lock | 1 + library/Cargo.toml | 1 + library/src/lib.rs | 6 +- library/src/math/mod.rs | 284 +++++++++++++--- library/src/math/tex.rs | 33 +- library/src/text/raw.rs | 2 +- src/model/eval.rs | 30 +- src/syntax/ast.rs | 13 +- src/syntax/highlight.rs | 11 +- src/syntax/mod.rs | 17 +- src/syntax/parser.rs | 28 +- src/syntax/parsing.rs | 78 ++++- src/syntax/tests.rs | 483 +++++++++++++++++++++++++++ src/syntax/tokens.rs | 581 ++++----------------------------- tests/ref/math/simple.png | Bin 6554 -> 6555 bytes tests/ref/math/syntax.png | Bin 0 -> 53462 bytes tests/typ/math/syntax.typ | 24 ++ tools/test-helper/extension.js | 8 +- 18 files changed, 945 insertions(+), 655 deletions(-) create mode 100644 src/syntax/tests.rs create mode 100644 tests/ref/math/syntax.png create mode 100644 tests/typ/math/syntax.typ diff --git a/Cargo.lock b/Cargo.lock index fe522d844..37fe60d17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1188,6 +1188,7 @@ dependencies = [ "unicode-bidi", "unicode-math", "unicode-script", + "unicode-segmentation", "xi-unicode", ] diff --git a/library/Cargo.toml b/library/Cargo.toml index 2410cb0c7..f5377d642 100644 --- a/library/Cargo.toml +++ b/library/Cargo.toml @@ -28,4 +28,5 @@ typed-arena = "2" unicode-bidi = "0.3.5" unicode-math = { git = "https://github.com/s3bk/unicode-math/" } unicode-script = "0.5" +unicode-segmentation = "1" xi-unicode = "0.3" diff --git a/library/src/lib.rs b/library/src/lib.rs index d549c1cda..af5c252bc 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -52,11 +52,7 @@ fn scope() -> Scope { std.def_node::("math"); std.def_node::("atom"); std.def_node::("frac"); - std.define("sum", "∑"); - std.define("in", "∈"); - std.define("arrow", "→"); - std.define("NN", "ℕ"); - std.define("RR", "ℝ"); + std.def_node::("sqrt"); // Layout. std.def_node::("page"); diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index a276908d3..1e8145cce 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -2,13 +2,12 @@ mod tex; -use std::fmt::Write; +use typst::model::{Guard, SequenceNode}; +use unicode_segmentation::UnicodeSegmentation; -use typst::model::Guard; - -use self::tex::{layout_tex, Texify}; +use self::tex::layout_tex; use crate::prelude::*; -use crate::text::FontFamily; +use crate::text::{FontFamily, LinebreakNode, SpaceNode, SymbolNode, TextNode}; /// A piece of a mathematical formula. #[derive(Debug, Clone, Hash)] @@ -55,15 +54,182 @@ impl Layout for MathNode { styles: StyleChain, _: &Regions, ) -> SourceResult { - layout_tex(vt, &self.texify(), self.display, styles) + let mut t = Texifier::new(); + self.texify(&mut t)?; + layout_tex(vt, &t.finish(), self.display, styles) } } impl Inline for MathNode {} +/// Turn a math node into TeX math code. +#[capability] +trait Texify { + /// Perform the conversion. + fn texify(&self, t: &mut Texifier) -> SourceResult<()>; + + /// Texify the node, but trim parentheses.. + fn texify_unparen(&self, t: &mut Texifier) -> SourceResult<()> { + let s = { + let mut sub = Texifier::new(); + self.texify(&mut sub)?; + sub.finish() + }; + + let unparened = if s.starts_with("\\left(") && s.ends_with("\\right)") { + s[6..s.len() - 7].into() + } else { + s + }; + + t.push_str(&unparened); + Ok(()) + } +} + +/// Builds the TeX representation of the formula. +struct Texifier { + tex: EcoString, + support: bool, + space: bool, +} + +impl Texifier { + /// Create a new texifier. + fn new() -> Self { + Self { + tex: EcoString::new(), + support: false, + space: false, + } + } + + /// Finish texifier and return the TeX string. + fn finish(self) -> EcoString { + self.tex + } + + /// Push a weak space. + fn push_space(&mut self) { + self.space = !self.tex.is_empty(); + } + + /// Mark this position as supportive. This allows a space before or after + /// to exist. + fn support(&mut self) { + self.support = true; + } + + /// Flush a space. + fn flush(&mut self) { + if self.space && self.support { + self.tex.push_str("\\ "); + } + + self.space = false; + self.support = false; + } + + /// Push a string. + fn push_str(&mut self, s: &str) { + self.flush(); + self.tex.push_str(s); + } + + /// Escape and push a char for TeX usage. + #[rustfmt::skip] + fn push_escaped(&mut self, c: char) { + self.flush(); + match c { + ' ' => self.tex.push_str("\\ "), + '%' | '&' | '$' | '#' => { + self.tex.push('\\'); + self.tex.push(c); + self.tex.push(' '); + } + '{' => self.tex.push_str("\\left\\{"), + '}' => self.tex.push_str("\\right\\}"), + '[' | '(' => { + self.tex.push_str("\\left"); + self.tex.push(c); + } + ']' | ')' => { + self.tex.push_str("\\right"); + self.tex.push(c); + } + 'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' | + '*' | '+' | '-' | '?' | '!' | '=' | '<' | '>' | + ':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => self.tex.push(c), + c => { + if let Some(sym) = unicode_math::SYMBOLS + .iter() + .find(|sym| sym.codepoint == c) { + self.tex.push('\\'); + self.tex.push_str(sym.name); + self.tex.push(' '); + } + } + } + } +} + impl Texify for MathNode { - fn texify(&self) -> EcoString { - self.children.iter().map(Texify::texify).collect() + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + for child in &self.children { + child.texify(t)?; + } + Ok(()) + } +} + +impl Texify for Content { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + if self.is::() { + t.push_space(); + return Ok(()); + } + + if self.is::() { + t.push_str("\\"); + return Ok(()); + } + + if let Some(node) = self.to::() { + if let Some(c) = symmie::get(&node.0) { + t.push_escaped(c); + return Ok(()); + } else if let Some(span) = self.span() { + bail!(span, "unknown symbol"); + } + } + + if let Some(node) = self.to::() { + t.support(); + t.push_str("\\mathrm{"); + for c in node.0.chars() { + t.push_escaped(c); + } + t.push_str("}"); + t.support(); + return Ok(()); + } + + if let Some(node) = self.to::() { + for child in &node.0 { + child.texify(t)?; + } + return Ok(()); + } + + if let Some(node) = self.with::() { + return node.texify(t); + } + + if let Some(span) = self.span() { + bail!(span, "not allowed here"); + } + + Ok(()) } } @@ -72,11 +238,35 @@ impl Texify for MathNode { pub struct AtomNode(pub EcoString); #[node(Texify)] -impl AtomNode {} +impl AtomNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("text")?).pack()) + } +} impl Texify for AtomNode { - fn texify(&self) -> EcoString { - self.0.chars().map(escape_char).collect() + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + let multi = self.0.graphemes(true).count() > 1; + if multi { + t.push_str("\\mathrm{"); + } + + for c in self.0.chars() { + let supportive = c == '|'; + if supportive { + t.support(); + } + t.push_escaped(c); + if supportive { + t.support(); + } + } + + if multi { + t.push_str("}"); + } + + Ok(()) } } @@ -90,15 +280,22 @@ pub struct FracNode { } #[node(Texify)] -impl FracNode {} +impl FracNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + let num = args.expect("numerator")?; + let denom = args.expect("denominator")?; + Ok(Self { num, denom }.pack()) + } +} impl Texify for FracNode { - fn texify(&self) -> EcoString { - format_eco!( - "\\frac{{{}}}{{{}}}", - unparen(self.num.texify()), - unparen(self.denom.texify()) - ) + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\frac{"); + self.num.texify_unparen(t)?; + t.push_str("}{"); + self.denom.texify_unparen(t)?; + t.push_str("}"); + Ok(()) } } @@ -117,18 +314,22 @@ pub struct ScriptNode { impl ScriptNode {} impl Texify for ScriptNode { - fn texify(&self) -> EcoString { - let mut tex = self.base.texify(); + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + self.base.texify(t)?; if let Some(sub) = &self.sub { - write!(tex, "_{{{}}}", unparen(sub.texify())).unwrap(); + t.push_str("_{"); + sub.texify_unparen(t)?; + t.push_str("}"); } if let Some(sup) = &self.sup { - write!(tex, "^{{{}}}", unparen(sup.texify())).unwrap(); + t.push_str("^{"); + sup.texify_unparen(t)?; + t.push_str("}"); } - tex + Ok(()) } } @@ -140,32 +341,27 @@ pub struct AlignNode(pub usize); impl AlignNode {} impl Texify for AlignNode { - fn texify(&self) -> EcoString { - EcoString::new() + fn texify(&self, _: &mut Texifier) -> SourceResult<()> { + Ok(()) } } -/// Escape a char for TeX usage. -#[rustfmt::skip] -fn escape_char(c: char) -> EcoString { - match c { - '{' | '}' | '%' | '&' | '$' | '#' => format_eco!(" \\{c} "), - 'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' | - '*' | '+' | '-' | '[' | '(' | ']' | ')' | '?' | '!' | '=' | '<' | '>' | - ':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => c.into(), - c => unicode_math::SYMBOLS - .iter() - .find(|sym| sym.codepoint == c) - .map(|sym| format_eco!("\\{} ", sym.name)) - .unwrap_or_default(), +/// A square root node. +#[derive(Debug, Hash)] +pub struct SqrtNode(Content); + +#[node(Texify)] +impl SqrtNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) } } -/// Trim grouping parenthesis≤. -fn unparen(s: EcoString) -> EcoString { - if s.starts_with('(') && s.ends_with(')') { - s[1..s.len() - 1].into() - } else { - s +impl Texify for SqrtNode { + fn texify(&self, t: &mut Texifier) -> SourceResult<()> { + t.push_str("\\sqrt{"); + self.0.texify_unparen(t)?; + t.push_str("}"); + Ok(()) } } diff --git a/library/src/math/tex.rs b/library/src/math/tex.rs index b2b6486e2..da07f1d62 100644 --- a/library/src/math/tex.rs +++ b/library/src/math/tex.rs @@ -6,32 +6,7 @@ use rex::render::{Backend, Cursor, Renderer}; use typst::font::Font; use crate::prelude::*; -use crate::text::{families, variant, LinebreakNode, SpaceNode, TextNode}; - -/// Turn a math node into TeX math code. -#[capability] -pub trait Texify { - /// Perform the conversion. - fn texify(&self) -> EcoString; -} - -impl Texify for Content { - fn texify(&self) -> EcoString { - if self.is::() { - return EcoString::new(); - } - - if self.is::() { - return r"\\".into(); - } - - if let Some(node) = self.with::() { - return node.texify(); - } - - panic!("{self:?} is not math"); - } -} +use crate::text::{families, variant, TextNode}; /// Layout a TeX formula into a frame. pub fn layout_tex( @@ -63,13 +38,15 @@ pub fn layout_tex( let style = if display { Style::Display } else { Style::Text }; let settings = LayoutSettings::new(&ctx, em.to_pt(), style); let renderer = Renderer::new(); - let layout = renderer + let Ok(layout) = renderer .layout(&tex, settings) .map_err(|err| match err { Error::Parse(err) => err.to_string(), Error::Layout(LayoutError::Font(err)) => err.to_string(), }) - .expect("failed to layout with rex"); + else { + panic!("failed to layout with rex: {tex}"); + }; // Determine the metrics. let (x0, y0, x1, y1) = renderer.size(&layout); diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs index a043019a7..7c1e36001 100644 --- a/library/src/text/raw.rs +++ b/library/src/text/raw.rs @@ -169,7 +169,7 @@ pub static THEME: Lazy = Lazy::new(|| Theme { item("entity.name, variable.function, support", Some("#4b69c6"), None), item("support.macro", Some("#16718d"), None), item("meta.annotation", Some("#301414"), None), - item("entity.other, meta.interpolation", Some("#8b41b1"), None), + item("entity.other, meta.interpolation, constant.symbol.typst", Some("#8b41b1"), None), item("invalid", Some("#ff0000"), None), ], }); diff --git a/src/model/eval.rs b/src/model/eval.rs index 1d942dd05..a32b0cd25 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -271,7 +271,6 @@ impl Eval for ast::MarkupNode { Self::Emph(v) => v.eval(vm)?, Self::Link(v) => v.eval(vm)?, Self::Raw(v) => v.eval(vm)?, - Self::Math(v) => v.eval(vm)?, Self::Heading(v) => v.eval(vm)?, Self::List(v) => v.eval(vm)?, Self::Enum(v) => v.eval(vm)?, @@ -426,19 +425,29 @@ impl Eval for ast::MathNode { Self::Linebreak(v) => v.eval(vm)?, Self::Escape(v) => (vm.items.math_atom)(v.get().into()), Self::Atom(v) => v.eval(vm)?, + Self::Symbol(v) => (vm.items.symbol)(v.get().clone()), Self::Script(v) => v.eval(vm)?, Self::Frac(v) => v.eval(vm)?, Self::Align(v) => v.eval(vm)?, Self::Group(v) => v.eval(vm)?, - Self::Expr(v) => match v.eval(vm)? { - Value::None => Content::empty(), - Value::Int(v) => (vm.items.math_atom)(format_eco!("{}", v)), - Value::Float(v) => (vm.items.math_atom)(format_eco!("{}", v)), - Value::Str(v) => (vm.items.math_atom)(v.into()), - Value::Content(v) => v, - _ => bail!(v.span(), "unexpected garbage"), - }, - }) + Self::Expr(v) => { + if let ast::Expr::Ident(ident) = v { + if self.as_untyped().len() == ident.len() + && !vm.scopes.get(ident).is_ok() + { + let node = (vm.items.symbol)(ident.get().clone()); + return Ok(node.spanned(self.span())); + } + } + + match v.eval(vm)? { + Value::Int(v) => (vm.items.math_atom)(format_eco!("{}", v)), + Value::Float(v) => (vm.items.math_atom)(format_eco!("{}", v)), + v => v.display(), + } + } + } + .spanned(self.span())) } } @@ -494,6 +503,7 @@ impl Eval for ast::Expr { Self::Ident(v) => v.eval(vm), Self::Code(v) => v.eval(vm), Self::Content(v) => v.eval(vm).map(Value::Content), + Self::Math(v) => v.eval(vm).map(Value::Content), Self::Array(v) => v.eval(vm).map(Value::Array), Self::Dict(v) => v.eval(vm).map(Value::Dict), Self::Parenthesized(v) => v.eval(vm), diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index c44fa2a0a..55586feb8 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -107,8 +107,6 @@ pub enum MarkupNode { Enum(EnumItem), /// An item in a description list: `/ Term: Details`. Desc(DescItem), - /// A math formula: `$x$`, `$ x^2 $`. - Math(Math), /// An expression. Expr(Expr), } @@ -132,7 +130,6 @@ impl AstNode for MarkupNode { SyntaxKind::ListItem => node.cast().map(Self::List), SyntaxKind::EnumItem => node.cast().map(Self::Enum), SyntaxKind::DescItem => node.cast().map(Self::Desc), - SyntaxKind::Math => node.cast().map(Self::Math), _ => node.cast().map(Self::Expr), } } @@ -155,7 +152,6 @@ impl AstNode for MarkupNode { Self::List(v) => v.as_untyped(), Self::Enum(v) => v.as_untyped(), Self::Desc(v) => v.as_untyped(), - Self::Math(v) => v.as_untyped(), Self::Expr(v) => v.as_untyped(), } } @@ -447,6 +443,9 @@ pub enum MathNode { Escape(Escape), /// An atom: `x`, `+`, `12`. Atom(Atom), + /// Symbol notation: `:arrow:l:` or `arrow:l`. Notations without any colons + /// are parsed as identifier expression and handled during evaluation. + Symbol(Symbol), /// A base with optional sub- and superscripts: `a_1^2`. Script(Script), /// A fraction: `x/2`. @@ -466,6 +465,7 @@ impl AstNode for MathNode { SyntaxKind::Linebreak => node.cast().map(Self::Linebreak), SyntaxKind::Escape(_) => node.cast().map(Self::Escape), SyntaxKind::Atom(_) => node.cast().map(Self::Atom), + SyntaxKind::Symbol(_) => node.cast().map(Self::Symbol), SyntaxKind::Script => node.cast().map(Self::Script), SyntaxKind::Frac => node.cast().map(Self::Frac), SyntaxKind::Align => node.cast().map(Self::Align), @@ -480,6 +480,7 @@ impl AstNode for MathNode { Self::Linebreak(v) => v.as_untyped(), Self::Escape(v) => v.as_untyped(), Self::Atom(v) => v.as_untyped(), + Self::Symbol(v) => v.as_untyped(), Self::Script(v) => v.as_untyped(), Self::Frac(v) => v.as_untyped(), Self::Align(v) => v.as_untyped(), @@ -574,6 +575,8 @@ pub enum Expr { Code(CodeBlock), /// A content block: `[*Hi* there!]`. Content(ContentBlock), + /// A math formula: `$x$`, `$ x^2 $`. + Math(Math), /// A grouped expression: `(1 + 2)`. Parenthesized(Parenthesized), /// An array: `(1, "hi", 12cm)`. @@ -622,6 +625,7 @@ impl AstNode for Expr { SyntaxKind::Ident(_) => node.cast().map(Self::Ident), SyntaxKind::CodeBlock => node.cast().map(Self::Code), SyntaxKind::ContentBlock => node.cast().map(Self::Content), + SyntaxKind::Math => node.cast().map(Self::Math), SyntaxKind::Parenthesized => node.cast().map(Self::Parenthesized), SyntaxKind::Array => node.cast().map(Self::Array), SyntaxKind::Dict => node.cast().map(Self::Dict), @@ -651,6 +655,7 @@ impl AstNode for Expr { Self::Lit(v) => v.as_untyped(), Self::Code(v) => v.as_untyped(), Self::Content(v) => v.as_untyped(), + Self::Math(v) => v.as_untyped(), Self::Ident(v) => v.as_untyped(), Self::Array(v) => v.as_untyped(), Self::Dict(v) => v.as_untyped(), diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index d4da7b3ed..3fed905fb 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -163,8 +163,6 @@ pub enum Category { ListMarker, /// A term in a description list. ListTerm, - /// A full math formula. - Math, /// The delimiters of a math formula. MathDelimiter, /// An operator with special meaning in a math formula. @@ -300,15 +298,17 @@ impl Category { SyntaxKind::EnumItem => Some(Category::ListItem), SyntaxKind::EnumNumbering(_) => Some(Category::ListMarker), SyntaxKind::DescItem => Some(Category::ListItem), - SyntaxKind::Math => Some(Category::Math), + SyntaxKind::Math => None, SyntaxKind::Atom(_) => None, SyntaxKind::Script => None, SyntaxKind::Frac => None, SyntaxKind::Align => None, SyntaxKind::Ident(_) => match parent.kind() { - SyntaxKind::Markup { .. } => Some(Category::Interpolated), - SyntaxKind::Math => Some(Category::Interpolated), + SyntaxKind::Markup { .. } + | SyntaxKind::Math + | SyntaxKind::Script + | SyntaxKind::Frac => Some(Category::Interpolated), SyntaxKind::FuncCall => Some(Category::Function), SyntaxKind::MethodCall if i > 0 => Some(Category::Function), SyntaxKind::Closure if i == 0 => Some(Category::Function), @@ -378,7 +378,6 @@ impl Category { Self::Emph => "markup.italic.typst", Self::Link => "markup.underline.link.typst", Self::Raw => "markup.raw.typst", - Self::Math => "string.other.math.typst", Self::MathDelimiter => "punctuation.definition.math.typst", Self::MathOperator => "keyword.operator.math.typst", Self::Heading => "markup.heading.typst", diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index 2ef493223..c461a5892 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -24,19 +24,4 @@ use incremental::reparse; use parser::*; #[cfg(test)] -mod tests { - use std::fmt::Debug; - - #[track_caller] - pub fn check(text: &str, found: T, expected: T) - where - T: Debug + PartialEq, - { - if found != expected { - println!("source: {text:?}"); - println!("expected: {expected:#?}"); - println!("found: {found:#?}"); - panic!("test failed"); - } - } -} +mod tests; diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index ff4a49522..3e133fe12 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -235,17 +235,9 @@ impl<'s> Parser<'s> { pub fn start_group(&mut self, kind: Group) { self.groups.push(GroupEntry { kind, prev_mode: self.tokens.mode() }); self.tokens.set_mode(match kind { - Group::Strong | Group::Emph => TokenMode::Markup, - Group::Bracket => match self.tokens.mode() { - TokenMode::Math => TokenMode::Math, - _ => TokenMode::Markup, - }, - Group::Brace | Group::Paren => match self.tokens.mode() { - TokenMode::Math => TokenMode::Math, - _ => TokenMode::Code, - }, - Group::Math => TokenMode::Math, - Group::Expr | Group::Imports => TokenMode::Code, + Group::Bracket | Group::Strong | Group::Emph => TokenMode::Markup, + Group::Math | Group::MathRow(_, _) => TokenMode::Math, + Group::Brace | Group::Paren | Group::Expr | Group::Imports => TokenMode::Code, }); match kind { @@ -255,6 +247,7 @@ impl<'s> Parser<'s> { Group::Strong => self.assert(SyntaxKind::Star), Group::Emph => self.assert(SyntaxKind::Underscore), Group::Math => self.assert(SyntaxKind::Dollar), + Group::MathRow(l, _) => self.assert(SyntaxKind::Atom(l.into())), Group::Expr => self.repeek(), Group::Imports => self.repeek(), } @@ -279,6 +272,7 @@ impl<'s> Parser<'s> { Group::Strong => Some((SyntaxKind::Star, true)), Group::Emph => Some((SyntaxKind::Underscore, true)), Group::Math => Some((SyntaxKind::Dollar, true)), + Group::MathRow(_, r) => Some((SyntaxKind::Atom(r.into()), true)), Group::Expr => Some((SyntaxKind::Semicolon, false)), Group::Imports => None, } { @@ -344,9 +338,17 @@ impl<'s> Parser<'s> { Some(SyntaxKind::RightParen) => self.inside(Group::Paren), Some(SyntaxKind::Star) => self.inside(Group::Strong), Some(SyntaxKind::Underscore) => self.inside(Group::Emph), - Some(SyntaxKind::Dollar) => self.inside(Group::Math), + Some(SyntaxKind::Dollar) => { + self.groups.last().map(|group| group.kind) == Some(Group::Math) + } Some(SyntaxKind::Semicolon) => self.inside(Group::Expr), Some(SyntaxKind::From) => self.inside(Group::Imports), + Some(SyntaxKind::Atom(s)) => match s.as_str() { + ")" => self.inside(Group::MathRow('(', ')')), + "}" => self.inside(Group::MathRow('{', '}')), + "]" => self.inside(Group::MathRow('[', ']')), + _ => false, + }, Some(SyntaxKind::Space { newlines }) => self.space_ends_group(*newlines), Some(_) => false, None => true, @@ -531,6 +533,8 @@ pub enum Group { Emph, /// A group surrounded by dollar signs: `$...$`. Math, + /// A group surrounded by math delimiters. + MathRow(char, char), /// A group ended by a semicolon or a line break: `;`, `\n`. Expr, /// A group for import items, ended by a semicolon, line break or `from`. diff --git a/src/syntax/parsing.rs b/src/syntax/parsing.rs index 59e066a66..5bd5e63bd 100644 --- a/src/syntax/parsing.rs +++ b/src/syntax/parsing.rs @@ -268,7 +268,7 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) { | SyntaxKind::Include | SyntaxKind::Break | SyntaxKind::Continue - | SyntaxKind::Return => markup_expr(p), + | SyntaxKind::Return => embedded_expr(p), // Code and content block. SyntaxKind::LeftBrace => code_block(p), @@ -359,7 +359,7 @@ fn desc_item(p: &mut Parser, at_start: bool) -> ParseResult { Ok(()) } -fn markup_expr(p: &mut Parser) { +fn embedded_expr(p: &mut Parser) { // Does the expression need termination or can content follow directly? let stmt = matches!( p.peek(), @@ -437,36 +437,63 @@ fn math_node_prec(p: &mut Parser, min_prec: usize, stop: Option) { fn math_primary(p: &mut Parser) { let Some(token) = p.peek() else { return }; match token { - // Spaces, atoms and expressions. + // Spaces and expressions. SyntaxKind::Space { .. } | SyntaxKind::Linebreak | SyntaxKind::Escape(_) - | SyntaxKind::Atom(_) - | SyntaxKind::Ident(_) => p.eat(), + | SyntaxKind::Str(_) + | SyntaxKind::Symbol(_) => p.eat(), - // Groups. - SyntaxKind::LeftParen => math_group(p, Group::Paren, '(', ')'), - SyntaxKind::LeftBracket => math_group(p, Group::Bracket, '[', ']'), - SyntaxKind::LeftBrace => math_group(p, Group::Brace, '{', '}'), + // Atoms. + SyntaxKind::Atom(s) => match s.as_str() { + "(" => math_group(p, Group::MathRow('(', ')')), + "{" => math_group(p, Group::MathRow('{', '}')), + "[" => math_group(p, Group::MathRow('[', ']')), + _ => p.eat(), + }, // Alignment indactor. SyntaxKind::Amp => math_align(p), + // Identifiers and math calls. + SyntaxKind::Ident(_) => { + let marker = p.marker(); + p.eat(); + + // Parenthesis or bracket means this is a function call. + if matches!(p.peek_direct(), Some(SyntaxKind::Atom(s)) if s == "(") { + marker.perform(p, SyntaxKind::FuncCall, math_args); + } + } + + // Hashtag + keyword / identifier. + SyntaxKind::Let + | SyntaxKind::Set + | SyntaxKind::Show + | SyntaxKind::If + | SyntaxKind::While + | SyntaxKind::For + | SyntaxKind::Import + | SyntaxKind::Include + | SyntaxKind::Break + | SyntaxKind::Continue + | SyntaxKind::Return => embedded_expr(p), + + // Code and content block. + SyntaxKind::LeftBrace => code_block(p), + SyntaxKind::LeftBracket => content_block(p), + _ => p.unexpected(), } } -fn math_group(p: &mut Parser, group: Group, l: char, r: char) { +fn math_group(p: &mut Parser, group: Group) { p.perform(SyntaxKind::Math, |p| { - let marker = p.marker(); p.start_group(group); - marker.convert(p, SyntaxKind::Atom(l.into())); while !p.eof() { math_node(p); } - let marker = p.marker(); p.end_group(); - marker.convert(p, SyntaxKind::Atom(r.into())); }) } @@ -582,6 +609,7 @@ fn primary(p: &mut Parser, atomic: bool) -> ParseResult { Some(SyntaxKind::LeftParen) => parenthesized(p, atomic), Some(SyntaxKind::LeftBrace) => Ok(code_block(p)), Some(SyntaxKind::LeftBracket) => Ok(content_block(p)), + Some(SyntaxKind::Dollar) => Ok(math(p)), // Keywords. Some(SyntaxKind::Let) => let_binding(p), @@ -902,6 +930,28 @@ fn args(p: &mut Parser) -> ParseResult { Ok(()) } +fn math_args(p: &mut Parser) { + p.start_group(Group::MathRow('(', ')')); + p.perform(SyntaxKind::Args, |p| { + let mut marker = p.marker(); + while !p.eof() { + if matches!(p.peek(), Some(SyntaxKind::Atom(s)) if s == ",") { + marker.end(p, SyntaxKind::Math); + let comma = p.marker(); + p.eat(); + comma.convert(p, SyntaxKind::Comma); + marker = p.marker(); + } else { + math_node(p); + } + } + if marker != p.marker() { + marker.end(p, SyntaxKind::Math); + } + }); + p.end_group(); +} + fn let_binding(p: &mut Parser) -> ParseResult { p.perform(SyntaxKind::LetBinding, |p| { p.assert(SyntaxKind::Let); diff --git a/src/syntax/tests.rs b/src/syntax/tests.rs new file mode 100644 index 000000000..7b5dd8706 --- /dev/null +++ b/src/syntax/tests.rs @@ -0,0 +1,483 @@ +#![allow(non_snake_case)] + +use std::num::NonZeroUsize; +use std::sync::Arc; + +use super::*; +use crate::geom::{AbsUnit, AngleUnit}; + +use ErrorPos::*; +use Option::None; +use SyntaxKind::*; +use TokenMode::{Code, Markup}; + +use std::fmt::Debug; + +#[track_caller] +pub fn check(text: &str, found: T, expected: T) +where + T: Debug + PartialEq, +{ + if found != expected { + println!("source: {text:?}"); + println!("expected: {expected:#?}"); + println!("found: {found:#?}"); + panic!("test failed"); + } +} + +fn Space(newlines: usize) -> SyntaxKind { + SyntaxKind::Space { newlines } +} + +fn Raw(text: &str, lang: Option<&str>, block: bool) -> SyntaxKind { + SyntaxKind::Raw(Arc::new(RawFields { + text: text.into(), + lang: lang.map(Into::into), + block, + })) +} + +fn Str(string: &str) -> SyntaxKind { + SyntaxKind::Str(string.into()) +} + +fn Text(string: &str) -> SyntaxKind { + SyntaxKind::Text(string.into()) +} + +fn Ident(ident: &str) -> SyntaxKind { + SyntaxKind::Ident(ident.into()) +} + +fn Error(pos: ErrorPos, message: &str) -> SyntaxKind { + SyntaxKind::Error(pos, message.into()) +} + +/// Building blocks for suffix testing. +/// +/// We extend each test case with a collection of different suffixes to make +/// sure tokens end at the correct position. These suffixes are split into +/// blocks, which can be disabled/enabled per test case. For example, when +/// testing identifiers we disable letter suffixes because these would +/// mingle with the identifiers. +/// +/// Suffix blocks: +/// - ' ': spacing +/// - 'a': letters +/// - '1': numbers +/// - '/': symbols +const BLOCKS: &str = " a1/"; + +// Suffixes described by four-tuples of: +// +// - block the suffix is part of +// - mode in which the suffix is applicable +// - the suffix string +// - the resulting suffix NodeKind +fn suffixes() -> impl Iterator, &'static str, SyntaxKind)> +{ + [ + // Whitespace suffixes. + (' ', None, " ", Space(0)), + (' ', None, "\n", Space(1)), + (' ', None, "\r", Space(1)), + (' ', None, "\r\n", Space(1)), + // Letter suffixes. + ('a', Some(Markup), "hello", Text("hello")), + ('a', Some(Markup), "💚", Text("💚")), + ('a', Some(Code), "val", Ident("val")), + ('a', Some(Code), "α", Ident("α")), + ('a', Some(Code), "_", Ident("_")), + // Number suffixes. + ('1', Some(Code), "2", Int(2)), + ('1', Some(Code), ".2", Float(0.2)), + // Symbol suffixes. + ('/', None, "[", LeftBracket), + ('/', None, "//", LineComment), + ('/', None, "/**/", BlockComment), + ('/', Some(Markup), "*", Star), + ('/', Some(Markup), r"\\", Escape('\\')), + ('/', Some(Markup), "#let", Let), + ('/', Some(Code), "(", LeftParen), + ('/', Some(Code), ":", Colon), + ('/', Some(Code), "+=", PlusEq), + ] + .into_iter() +} + +macro_rules! t { + (Both $($tts:tt)*) => { + t!(Markup $($tts)*); + t!(Code $($tts)*); + }; + ($mode:ident $([$blocks:literal])?: $text:expr => $($token:expr),*) => {{ + // Test without suffix. + t!(@$mode: $text => $($token),*); + + // Test with each applicable suffix. + for (block, mode, suffix, ref token) in suffixes() { + let text = $text; + #[allow(unused_variables)] + let blocks = BLOCKS; + $(let blocks = $blocks;)? + assert!(!blocks.contains(|c| !BLOCKS.contains(c))); + if (mode.is_none() || mode == Some($mode)) && blocks.contains(block) { + t!(@$mode: format!("{}{}", text, suffix) => $($token,)* token); + } + } + }}; + (@$mode:ident: $text:expr => $($token:expr),*) => {{ + let text = $text; + let found = Tokens::new(&text, $mode).collect::>(); + let expected = vec![$($token.clone()),*]; + check(&text, found, expected); + }}; +} + +#[test] +fn test_tokenize_brackets() { + // Test in markup. + t!(Markup: "{" => LeftBrace); + t!(Markup: "}" => RightBrace); + t!(Markup: "[" => LeftBracket); + t!(Markup: "]" => RightBracket); + t!(Markup[" /"]: "(" => Text("(")); + t!(Markup[" /"]: ")" => Text(")")); + + // Test in code. + t!(Code: "{" => LeftBrace); + t!(Code: "}" => RightBrace); + t!(Code: "[" => LeftBracket); + t!(Code: "]" => RightBracket); + t!(Code: "(" => LeftParen); + t!(Code: ")" => RightParen); +} + +#[test] +fn test_tokenize_whitespace() { + // Test basic whitespace. + t!(Both["a1/"]: "" => ); + t!(Both["a1/"]: " " => Space(0)); + t!(Both["a1/"]: " " => Space(0)); + t!(Both["a1/"]: "\t" => Space(0)); + t!(Both["a1/"]: " \t" => Space(0)); + t!(Both["a1/"]: "\u{202F}" => Space(0)); + + // Test newline counting. + t!(Both["a1/"]: "\n" => Space(1)); + t!(Both["a1/"]: "\n " => Space(1)); + t!(Both["a1/"]: " \n" => Space(1)); + t!(Both["a1/"]: " \n " => Space(1)); + t!(Both["a1/"]: "\r\n" => Space(1)); + t!(Both["a1/"]: "\r\n\r" => Space(2)); + t!(Both["a1/"]: " \n\t \n " => Space(2)); + t!(Both["a1/"]: "\n\r" => Space(2)); + t!(Both["a1/"]: " \r\r\n \x0D" => Space(3)); +} + +#[test] +fn test_tokenize_text() { + // Test basic text. + t!(Markup[" /"]: "hello" => Text("hello")); + t!(Markup[" /"]: "reha-world" => Text("reha-world")); + + // Test code symbols in text. + t!(Markup[" /"]: "a():\"b" => Text("a()"), Colon, SmartQuote { double: true }, Text("b")); + t!(Markup[" /"]: ";,|/+" => Text(";,|/+")); + t!(Markup[" /"]: "=-a" => Eq, Minus, Text("a")); + t!(Markup[" "]: "#123" => Text("#123")); + + // Test text ends. + t!(Markup[""]: "hello " => Text("hello"), Space(0)); + t!(Markup[""]: "hello~" => Text("hello"), Shorthand('\u{00A0}')); +} + +#[test] +fn test_tokenize_escape_sequences() { + // Test escapable symbols. + t!(Markup: r"\\" => Escape('\\')); + t!(Markup: r"\/" => Escape('/')); + t!(Markup: r"\[" => Escape('[')); + t!(Markup: r"\]" => Escape(']')); + t!(Markup: r"\{" => Escape('{')); + t!(Markup: r"\}" => Escape('}')); + t!(Markup: r"\*" => Escape('*')); + t!(Markup: r"\_" => Escape('_')); + t!(Markup: r"\=" => Escape('=')); + t!(Markup: r"\~" => Escape('~')); + t!(Markup: r"\'" => Escape('\'')); + t!(Markup: r#"\""# => Escape('"')); + t!(Markup: r"\`" => Escape('`')); + t!(Markup: r"\$" => Escape('$')); + t!(Markup: r"\#" => Escape('#')); + t!(Markup: r"\a" => Escape('a')); + t!(Markup: r"\u" => Escape('u')); + t!(Markup: r"\1" => Escape('1')); + + // Test basic unicode escapes. + t!(Markup: r"\u{}" => Error(Full, "invalid unicode escape sequence")); + t!(Markup: r"\u{2603}" => Escape('☃')); + t!(Markup: r"\u{P}" => Error(Full, "invalid unicode escape sequence")); + + // Test unclosed unicode escapes. + t!(Markup[" /"]: r"\u{" => Error(End, "expected closing brace")); + t!(Markup[" /"]: r"\u{1" => Error(End, "expected closing brace")); + t!(Markup[" /"]: r"\u{26A4" => Error(End, "expected closing brace")); + t!(Markup[" /"]: r"\u{1Q3P" => Error(End, "expected closing brace")); + t!(Markup: r"\u{1🏕}" => Error(End, "expected closing brace"), Text("🏕"), RightBrace); +} + +#[test] +fn test_tokenize_markup_symbols() { + // Test markup tokens. + t!(Markup[" a1"]: "*" => Star); + t!(Markup: "_" => Underscore); + t!(Markup[""]: "===" => Eq, Eq, Eq); + t!(Markup["a1/"]: "= " => Eq, Space(0)); + t!(Markup[" "]: r"\" => Linebreak); + t!(Markup: "~" => Shorthand('\u{00A0}')); + t!(Markup["a1/"]: "-?" => Shorthand('\u{00AD}')); + t!(Markup["a "]: r"a--" => Text("a"), Shorthand('\u{2013}')); + t!(Markup["a1/"]: "- " => Minus, Space(0)); + t!(Markup[" "]: "+" => Plus); + t!(Markup[" "]: "1." => EnumNumbering(NonZeroUsize::new(1).unwrap())); + t!(Markup[" "]: "1.a" => EnumNumbering(NonZeroUsize::new(1).unwrap()), Text("a")); + t!(Markup[" /"]: "a1." => Text("a1.")); +} + +#[test] +fn test_tokenize_code_symbols() { + // Test all symbols. + t!(Code: "," => Comma); + t!(Code: ";" => Semicolon); + t!(Code: ":" => Colon); + t!(Code: "+" => Plus); + t!(Code: "-" => Minus); + t!(Code[" a1"]: "*" => Star); + t!(Code[" a1"]: "/" => Slash); + t!(Code[" a/"]: "." => Dot); + t!(Code: "=" => Eq); + t!(Code: "==" => EqEq); + t!(Code: "!=" => ExclEq); + t!(Code[" /"]: "<" => Lt); + t!(Code: "<=" => LtEq); + t!(Code: ">" => Gt); + t!(Code: ">=" => GtEq); + t!(Code: "+=" => PlusEq); + t!(Code: "-=" => HyphEq); + t!(Code: "*=" => StarEq); + t!(Code: "/=" => SlashEq); + t!(Code: ".." => Dots); + t!(Code: "=>" => Arrow); + + // Test combinations. + t!(Code: "<=>" => LtEq, Gt); + t!(Code[" a/"]: "..." => Dots, Dot); + + // Test hyphen as symbol vs part of identifier. + t!(Code[" /"]: "-1" => Minus, Int(1)); + t!(Code[" /"]: "-a" => Minus, Ident("a")); + t!(Code[" /"]: "--1" => Minus, Minus, Int(1)); + t!(Code[" /"]: "--_a" => Minus, Minus, Ident("_a")); + t!(Code[" /"]: "a-b" => Ident("a-b")); + + // Test invalid. + t!(Code: r"\" => Error(Full, "not valid here")); +} + +#[test] +fn test_tokenize_keywords() { + // A list of a few (not all) keywords. + let list = [ + ("not", Not), + ("let", Let), + ("if", If), + ("else", Else), + ("for", For), + ("in", In), + ("import", Import), + ]; + + for (s, t) in list.clone() { + t!(Markup[" "]: format!("#{}", s) => t); + t!(Markup[" "]: format!("#{0}#{0}", s) => t, t); + t!(Markup[" /"]: format!("# {}", s) => Text(&format!("# {s}"))); + } + + for (s, t) in list { + t!(Code[" "]: s => t); + t!(Markup[" /"]: s => Text(s)); + } + + // Test simple identifier. + t!(Markup[" "]: "#letter" => Ident("letter")); + t!(Code[" /"]: "falser" => Ident("falser")); + t!(Code[" /"]: "None" => Ident("None")); + t!(Code[" /"]: "True" => Ident("True")); +} + +#[test] +fn test_tokenize_raw_blocks() { + // Test basic raw block. + t!(Markup: "``" => Raw("", None, false)); + t!(Markup: "`raw`" => Raw("raw", None, false)); + t!(Markup[""]: "`]" => Error(End, "expected 1 backtick")); + + // Test special symbols in raw block. + t!(Markup: "`[brackets]`" => Raw("[brackets]", None, false)); + t!(Markup[""]: r"`\`` " => Raw(r"\", None, false), Error(End, "expected 1 backtick")); + + // Test separated closing backticks. + t!(Markup: "```not `y`e`t```" => Raw("`y`e`t", Some("not"), false)); + + // Test more backticks. + t!(Markup: "``nope``" => Raw("", None, false), Text("nope"), Raw("", None, false)); + t!(Markup: "````🚀````" => Raw("", None, false)); + t!(Markup[""]: "`````👩‍🚀````noend" => Error(End, "expected 5 backticks")); + t!(Markup[""]: "````raw``````" => Raw("", Some("raw"), false), Raw("", None, false)); +} + +#[test] +fn test_tokenize_idents() { + // Test valid identifiers. + t!(Code[" /"]: "x" => Ident("x")); + t!(Code[" /"]: "value" => Ident("value")); + t!(Code[" /"]: "__main__" => Ident("__main__")); + t!(Code[" /"]: "_snake_case" => Ident("_snake_case")); + + // Test non-ascii. + t!(Code[" /"]: "α" => Ident("α")); + t!(Code[" /"]: "ម្តាយ" => Ident("ម្តាយ")); + + // Test hyphen parsed as identifier. + t!(Code[" /"]: "kebab-case" => Ident("kebab-case")); + t!(Code[" /"]: "one-10" => Ident("one-10")); +} + +#[test] +fn test_tokenize_numeric() { + let ints = [("7", 7), ("012", 12)]; + let floats = [ + (".3", 0.3), + ("0.3", 0.3), + ("3.", 3.0), + ("3.0", 3.0), + ("14.3", 14.3), + ("10e2", 1000.0), + ("10e+0", 10.0), + ("10e+1", 100.0), + ("10e-2", 0.1), + ("10.e1", 100.0), + ("10.e-1", 1.0), + (".1e1", 1.0), + ("10E2", 1000.0), + ]; + + // Test integers. + for &(s, v) in &ints { + t!(Code[" /"]: s => Int(v)); + } + + // Test floats. + for &(s, v) in &floats { + t!(Code[" /"]: s => Float(v)); + } + + // Test attached numbers. + t!(Code[" /"]: ".2.3" => Float(0.2), Float(0.3)); + t!(Code[" /"]: "1.2.3" => Float(1.2), Float(0.3)); + t!(Code[" /"]: "1e-2+3" => Float(0.01), Plus, Int(3)); + + // Test float from too large integer. + let large = i64::MAX as f64 + 1.0; + t!(Code[" /"]: large.to_string() => Float(large)); + + // Combined integers and floats. + let nums = ints.iter().map(|&(k, v)| (k, v as f64)).chain(floats); + + let suffixes: &[(&str, fn(f64) -> SyntaxKind)] = &[ + ("mm", |x| Numeric(x, Unit::Length(AbsUnit::Mm))), + ("pt", |x| Numeric(x, Unit::Length(AbsUnit::Pt))), + ("cm", |x| Numeric(x, Unit::Length(AbsUnit::Cm))), + ("in", |x| Numeric(x, Unit::Length(AbsUnit::In))), + ("rad", |x| Numeric(x, Unit::Angle(AngleUnit::Rad))), + ("deg", |x| Numeric(x, Unit::Angle(AngleUnit::Deg))), + ("em", |x| Numeric(x, Unit::Em)), + ("fr", |x| Numeric(x, Unit::Fr)), + ("%", |x| Numeric(x, Unit::Percent)), + ]; + + // Numeric types. + for &(suffix, build) in suffixes { + for (s, v) in nums.clone() { + t!(Code[" /"]: format!("{}{}", s, suffix) => build(v)); + } + } + + // Multiple dots close the number. + t!(Code[" /"]: "1..2" => Int(1), Dots, Int(2)); + t!(Code[" /"]: "1..2.3" => Int(1), Dots, Float(2.3)); + t!(Code[" /"]: "1.2..3" => Float(1.2), Dots, Int(3)); + + // Test invalid. + t!(Code[" /"]: "1foo" => Error(Full, "invalid number suffix")); +} + +#[test] +fn test_tokenize_strings() { + // Test basic strings. + t!(Code: "\"hi\"" => Str("hi")); + t!(Code: "\"hi\nthere\"" => Str("hi\nthere")); + t!(Code: "\"🌎\"" => Str("🌎")); + + // Test unterminated. + t!(Code[""]: "\"hi" => Error(End, "expected quote")); + + // Test escaped quote. + t!(Code: r#""a\"bc""# => Str("a\"bc")); + t!(Code[""]: r#""\""# => Error(End, "expected quote")); +} + +#[test] +fn test_tokenize_line_comments() { + // Test line comment with no trailing newline. + t!(Both[""]: "//" => LineComment); + + // Test line comment ends at newline. + t!(Both["a1/"]: "//bc\n" => LineComment, Space(1)); + t!(Both["a1/"]: "// bc \n" => LineComment, Space(1)); + t!(Both["a1/"]: "//bc\r\n" => LineComment, Space(1)); + + // Test nested line comments. + t!(Both["a1/"]: "//a//b\n" => LineComment, Space(1)); +} + +#[test] +fn test_tokenize_block_comments() { + // Test basic block comments. + t!(Both[""]: "/*" => BlockComment); + t!(Both: "/**/" => BlockComment); + t!(Both: "/*🏞*/" => BlockComment); + t!(Both: "/*\n*/" => BlockComment); + + // Test depth 1 and 2 nested block comments. + t!(Both: "/* /* */ */" => BlockComment); + t!(Both: "/*/*/**/*/*/" => BlockComment); + + // Test two nested, one unclosed block comments. + t!(Both[""]: "/*/*/**/*/" => BlockComment); + + // Test all combinations of up to two following slashes and stars. + t!(Both[""]: "/*" => BlockComment); + t!(Both[""]: "/*/" => BlockComment); + t!(Both[""]: "/**" => BlockComment); + t!(Both[""]: "/*//" => BlockComment); + t!(Both[""]: "/*/*" => BlockComment); + t!(Both[""]: "/**/" => BlockComment); + t!(Both[""]: "/***" => BlockComment); + + // Test unexpected terminator. + t!(Both: "/*Hi*/*/" => BlockComment, + Error(Full, "unexpected end of block comment")); +} diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index 130ad6681..571880961 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -35,14 +35,12 @@ pub enum TokenMode { impl<'s> Tokens<'s> { /// Create a new token iterator with the given mode. - #[inline] pub fn new(text: &'s str, mode: TokenMode) -> Self { Self::with_prefix("", text, mode) } /// Create a new token iterator with the given mode and a prefix to offset /// column calculations. - #[inline] pub fn with_prefix(prefix: &str, text: &'s str, mode: TokenMode) -> Self { Self { s: Scanner::new(text), @@ -53,54 +51,46 @@ impl<'s> Tokens<'s> { } /// Get the current token mode. - #[inline] pub fn mode(&self) -> TokenMode { self.mode } /// Change the token mode. - #[inline] pub fn set_mode(&mut self, mode: TokenMode) { self.mode = mode; } /// The index in the string at which the last token ends and next token /// will start. - #[inline] pub fn cursor(&self) -> usize { self.s.cursor() } /// Jump to the given index in the string. - #[inline] pub fn jump(&mut self, index: usize) { self.s.jump(index); } /// The underlying scanner. - #[inline] pub fn scanner(&self) -> Scanner<'s> { self.s } /// Whether the last token was terminated. - #[inline] pub fn terminated(&self) -> bool { self.terminated } /// The column index of a given index in the source string. - #[inline] pub fn column(&self, index: usize) -> usize { column(self.s.string(), index, self.column_offset) } } -impl<'s> Iterator for Tokens<'s> { +impl Iterator for Tokens<'_> { type Item = SyntaxKind; /// Parse the next token in the source code. - #[inline] fn next(&mut self) -> Option { let start = self.s.cursor(); let c = self.s.eat()?; @@ -124,7 +114,8 @@ impl<'s> Iterator for Tokens<'s> { } } -impl<'s> Tokens<'s> { +/// Shared. +impl Tokens<'_> { fn line_comment(&mut self) -> SyntaxKind { self.s.eat_until(is_newline); if self.s.peek().is_none() { @@ -189,8 +180,9 @@ impl<'s> Tokens<'s> { SyntaxKind::Space { newlines } } +} - #[inline] +impl Tokens<'_> { fn markup(&mut self, start: usize, c: char) -> SyntaxKind { match c { // Blocks. @@ -231,7 +223,6 @@ impl<'s> Tokens<'s> { } } - #[inline] fn text(&mut self, start: usize) -> SyntaxKind { macro_rules! table { ($(|$c:literal)*) => {{ @@ -303,7 +294,11 @@ impl<'s> Tokens<'s> { } fn hash(&mut self, start: usize) -> SyntaxKind { - if self.s.at(is_id_start) { + if self.s.eat_if('{') { + SyntaxKind::LeftBrace + } else if self.s.eat_if('[') { + SyntaxKind::LeftBracket + } else if self.s.at(is_id_start) { let read = self.s.eat_while(is_id_continue); match keyword(read) { Some(keyword) => keyword, @@ -342,8 +337,10 @@ impl<'s> Tokens<'s> { if start < end { self.s.expect(':'); SyntaxKind::Symbol(self.s.get(start..end).into()) - } else { + } else if self.mode == TokenMode::Markup { SyntaxKind::Colon + } else { + SyntaxKind::Atom(":".into()) } } @@ -426,26 +423,25 @@ impl<'s> Tokens<'s> { self.text(start) } - fn label(&mut self) -> SyntaxKind { - let label = self.s.eat_while(is_id_continue); - if self.s.eat_if('>') { - if !label.is_empty() { - SyntaxKind::Label(label.into()) - } else { - SyntaxKind::Error(ErrorPos::Full, "label cannot be empty".into()) - } - } else { - self.terminated = false; - SyntaxKind::Error(ErrorPos::End, "expected closing angle bracket".into()) - } - } - fn reference(&mut self) -> SyntaxKind { SyntaxKind::Ref(self.s.eat_while(is_id_continue).into()) } + fn in_word(&self) -> bool { + let alphanumeric = |c: Option| c.map_or(false, |c| c.is_alphanumeric()); + let prev = self.s.scout(-2); + let next = self.s.peek(); + alphanumeric(prev) && alphanumeric(next) + } +} + +/// Math. +impl Tokens<'_> { fn math(&mut self, start: usize, c: char) -> SyntaxKind { match c { + // Multi-char things. + '#' => self.hash(start), + // Escape sequences. '\\' => self.backslash(), @@ -456,18 +452,32 @@ impl<'s> Tokens<'s> { '&' => SyntaxKind::Amp, '$' => SyntaxKind::Dollar, - // Brackets. - '{' => SyntaxKind::LeftBrace, - '}' => SyntaxKind::RightBrace, - '[' => SyntaxKind::LeftBracket, - ']' => SyntaxKind::RightBracket, - '(' => SyntaxKind::LeftParen, - ')' => SyntaxKind::RightParen, + // Symbol notation. + ':' => self.colon(), - // Identifiers. + // Strings. + '"' => self.string(), + + // Identifiers and symbol notation. c if is_math_id_start(c) && self.s.at(is_math_id_continue) => { self.s.eat_while(is_math_id_continue); - SyntaxKind::Ident(self.s.from(start).into()) + + let mut symbol = false; + while self.s.eat_if(':') + && !self.s.eat_while(char::is_alphanumeric).is_empty() + { + symbol = true; + } + + if symbol { + SyntaxKind::Symbol(self.s.from(start).into()) + } else { + if self.s.scout(-1) == Some(':') { + self.s.uneat(); + } + + SyntaxKind::Ident(self.s.from(start).into()) + } } // Numbers. @@ -480,7 +490,10 @@ impl<'s> Tokens<'s> { c => SyntaxKind::Atom(c.into()), } } +} +/// Code. +impl Tokens<'_> { fn code(&mut self, start: usize, c: char) -> SyntaxKind { match c { // Blocks. @@ -493,6 +506,9 @@ impl<'s> Tokens<'s> { '(' => SyntaxKind::LeftParen, ')' => SyntaxKind::RightParen, + // Math. + '$' => SyntaxKind::Dollar, + // Labels. '<' if self.s.at(is_id_continue) => self.label(), @@ -619,14 +635,22 @@ impl<'s> Tokens<'s> { } } - fn in_word(&self) -> bool { - let alphanumeric = |c: Option| c.map_or(false, |c| c.is_alphanumeric()); - let prev = self.s.scout(-2); - let next = self.s.peek(); - alphanumeric(prev) && alphanumeric(next) + fn label(&mut self) -> SyntaxKind { + let label = self.s.eat_while(is_id_continue); + if self.s.eat_if('>') { + if !label.is_empty() { + SyntaxKind::Label(label.into()) + } else { + SyntaxKind::Error(ErrorPos::Full, "label cannot be empty".into()) + } + } else { + self.terminated = false; + SyntaxKind::Error(ErrorPos::End, "expected closing angle bracket".into()) + } } } +/// Try to parse an identifier into a keyword. fn keyword(ident: &str) -> Option { Some(match ident { "not" => SyntaxKind::Not, @@ -652,7 +676,6 @@ fn keyword(ident: &str) -> Option { /// The column index of a given index in the source string, given a column /// offset for the first line. -#[inline] fn column(string: &str, index: usize, offset: usize) -> usize { let mut apply_offset = false; let res = string[..index] @@ -729,471 +752,3 @@ fn is_math_id_start(c: char) -> bool { fn is_math_id_continue(c: char) -> bool { c.is_xid_continue() && c != '_' } - -#[cfg(test)] -#[allow(non_snake_case)] -mod tests { - use super::super::tests::check; - use super::*; - - use ErrorPos::*; - use Option::None; - use SyntaxKind::*; - use TokenMode::{Code, Markup}; - - fn Space(newlines: usize) -> SyntaxKind { - SyntaxKind::Space { newlines } - } - - fn Raw(text: &str, lang: Option<&str>, block: bool) -> SyntaxKind { - SyntaxKind::Raw(Arc::new(RawFields { - text: text.into(), - lang: lang.map(Into::into), - block, - })) - } - - fn Str(string: &str) -> SyntaxKind { - SyntaxKind::Str(string.into()) - } - - fn Text(string: &str) -> SyntaxKind { - SyntaxKind::Text(string.into()) - } - - fn Ident(ident: &str) -> SyntaxKind { - SyntaxKind::Ident(ident.into()) - } - - fn Error(pos: ErrorPos, message: &str) -> SyntaxKind { - SyntaxKind::Error(pos, message.into()) - } - - /// Building blocks for suffix testing. - /// - /// We extend each test case with a collection of different suffixes to make - /// sure tokens end at the correct position. These suffixes are split into - /// blocks, which can be disabled/enabled per test case. For example, when - /// testing identifiers we disable letter suffixes because these would - /// mingle with the identifiers. - /// - /// Suffix blocks: - /// - ' ': spacing - /// - 'a': letters - /// - '1': numbers - /// - '/': symbols - const BLOCKS: &str = " a1/"; - - // Suffixes described by four-tuples of: - // - // - block the suffix is part of - // - mode in which the suffix is applicable - // - the suffix string - // - the resulting suffix NodeKind - fn suffixes( - ) -> impl Iterator, &'static str, SyntaxKind)> { - [ - // Whitespace suffixes. - (' ', None, " ", Space(0)), - (' ', None, "\n", Space(1)), - (' ', None, "\r", Space(1)), - (' ', None, "\r\n", Space(1)), - // Letter suffixes. - ('a', Some(Markup), "hello", Text("hello")), - ('a', Some(Markup), "💚", Text("💚")), - ('a', Some(Code), "val", Ident("val")), - ('a', Some(Code), "α", Ident("α")), - ('a', Some(Code), "_", Ident("_")), - // Number suffixes. - ('1', Some(Code), "2", Int(2)), - ('1', Some(Code), ".2", Float(0.2)), - // Symbol suffixes. - ('/', None, "[", LeftBracket), - ('/', None, "//", LineComment), - ('/', None, "/**/", BlockComment), - ('/', Some(Markup), "*", Star), - ('/', Some(Markup), r"\\", Escape('\\')), - ('/', Some(Markup), "#let", Let), - ('/', Some(Code), "(", LeftParen), - ('/', Some(Code), ":", Colon), - ('/', Some(Code), "+=", PlusEq), - ] - .into_iter() - } - - macro_rules! t { - (Both $($tts:tt)*) => { - t!(Markup $($tts)*); - t!(Code $($tts)*); - }; - ($mode:ident $([$blocks:literal])?: $text:expr => $($token:expr),*) => {{ - // Test without suffix. - t!(@$mode: $text => $($token),*); - - // Test with each applicable suffix. - for (block, mode, suffix, ref token) in suffixes() { - let text = $text; - #[allow(unused_variables)] - let blocks = BLOCKS; - $(let blocks = $blocks;)? - assert!(!blocks.contains(|c| !BLOCKS.contains(c))); - if (mode.is_none() || mode == Some($mode)) && blocks.contains(block) { - t!(@$mode: format!("{}{}", text, suffix) => $($token,)* token); - } - } - }}; - (@$mode:ident: $text:expr => $($token:expr),*) => {{ - let text = $text; - let found = Tokens::new(&text, $mode).collect::>(); - let expected = vec![$($token.clone()),*]; - check(&text, found, expected); - }}; - } - - #[test] - fn test_tokenize_brackets() { - // Test in markup. - t!(Markup: "{" => LeftBrace); - t!(Markup: "}" => RightBrace); - t!(Markup: "[" => LeftBracket); - t!(Markup: "]" => RightBracket); - t!(Markup[" /"]: "(" => Text("(")); - t!(Markup[" /"]: ")" => Text(")")); - - // Test in code. - t!(Code: "{" => LeftBrace); - t!(Code: "}" => RightBrace); - t!(Code: "[" => LeftBracket); - t!(Code: "]" => RightBracket); - t!(Code: "(" => LeftParen); - t!(Code: ")" => RightParen); - } - - #[test] - fn test_tokenize_whitespace() { - // Test basic whitespace. - t!(Both["a1/"]: "" => ); - t!(Both["a1/"]: " " => Space(0)); - t!(Both["a1/"]: " " => Space(0)); - t!(Both["a1/"]: "\t" => Space(0)); - t!(Both["a1/"]: " \t" => Space(0)); - t!(Both["a1/"]: "\u{202F}" => Space(0)); - - // Test newline counting. - t!(Both["a1/"]: "\n" => Space(1)); - t!(Both["a1/"]: "\n " => Space(1)); - t!(Both["a1/"]: " \n" => Space(1)); - t!(Both["a1/"]: " \n " => Space(1)); - t!(Both["a1/"]: "\r\n" => Space(1)); - t!(Both["a1/"]: "\r\n\r" => Space(2)); - t!(Both["a1/"]: " \n\t \n " => Space(2)); - t!(Both["a1/"]: "\n\r" => Space(2)); - t!(Both["a1/"]: " \r\r\n \x0D" => Space(3)); - } - - #[test] - fn test_tokenize_text() { - // Test basic text. - t!(Markup[" /"]: "hello" => Text("hello")); - t!(Markup[" /"]: "reha-world" => Text("reha-world")); - - // Test code symbols in text. - t!(Markup[" /"]: "a():\"b" => Text("a()"), Colon, SmartQuote { double: true }, Text("b")); - t!(Markup[" /"]: ";,|/+" => Text(";,|/+")); - t!(Markup[" /"]: "=-a" => Eq, Minus, Text("a")); - t!(Markup[" "]: "#123" => Text("#123")); - - // Test text ends. - t!(Markup[""]: "hello " => Text("hello"), Space(0)); - t!(Markup[""]: "hello~" => Text("hello"), Shorthand('\u{00A0}')); - } - - #[test] - fn test_tokenize_escape_sequences() { - // Test escapable symbols. - t!(Markup: r"\\" => Escape('\\')); - t!(Markup: r"\/" => Escape('/')); - t!(Markup: r"\[" => Escape('[')); - t!(Markup: r"\]" => Escape(']')); - t!(Markup: r"\{" => Escape('{')); - t!(Markup: r"\}" => Escape('}')); - t!(Markup: r"\*" => Escape('*')); - t!(Markup: r"\_" => Escape('_')); - t!(Markup: r"\=" => Escape('=')); - t!(Markup: r"\~" => Escape('~')); - t!(Markup: r"\'" => Escape('\'')); - t!(Markup: r#"\""# => Escape('"')); - t!(Markup: r"\`" => Escape('`')); - t!(Markup: r"\$" => Escape('$')); - t!(Markup: r"\#" => Escape('#')); - t!(Markup: r"\a" => Escape('a')); - t!(Markup: r"\u" => Escape('u')); - t!(Markup: r"\1" => Escape('1')); - - // Test basic unicode escapes. - t!(Markup: r"\u{}" => Error(Full, "invalid unicode escape sequence")); - t!(Markup: r"\u{2603}" => Escape('☃')); - t!(Markup: r"\u{P}" => Error(Full, "invalid unicode escape sequence")); - - // Test unclosed unicode escapes. - t!(Markup[" /"]: r"\u{" => Error(End, "expected closing brace")); - t!(Markup[" /"]: r"\u{1" => Error(End, "expected closing brace")); - t!(Markup[" /"]: r"\u{26A4" => Error(End, "expected closing brace")); - t!(Markup[" /"]: r"\u{1Q3P" => Error(End, "expected closing brace")); - t!(Markup: r"\u{1🏕}" => Error(End, "expected closing brace"), Text("🏕"), RightBrace); - } - - #[test] - fn test_tokenize_markup_symbols() { - // Test markup tokens. - t!(Markup[" a1"]: "*" => Star); - t!(Markup: "_" => Underscore); - t!(Markup[""]: "===" => Eq, Eq, Eq); - t!(Markup["a1/"]: "= " => Eq, Space(0)); - t!(Markup[" "]: r"\" => Linebreak); - t!(Markup: "~" => Shorthand('\u{00A0}')); - t!(Markup["a1/"]: "-?" => Shorthand('\u{00AD}')); - t!(Markup["a "]: r"a--" => Text("a"), Shorthand('\u{2013}')); - t!(Markup["a1/"]: "- " => Minus, Space(0)); - t!(Markup[" "]: "+" => Plus); - t!(Markup[" "]: "1." => EnumNumbering(NonZeroUsize::new(1).unwrap())); - t!(Markup[" "]: "1.a" => EnumNumbering(NonZeroUsize::new(1).unwrap()), Text("a")); - t!(Markup[" /"]: "a1." => Text("a1.")); - } - - #[test] - fn test_tokenize_code_symbols() { - // Test all symbols. - t!(Code: "," => Comma); - t!(Code: ";" => Semicolon); - t!(Code: ":" => Colon); - t!(Code: "+" => Plus); - t!(Code: "-" => Minus); - t!(Code[" a1"]: "*" => Star); - t!(Code[" a1"]: "/" => Slash); - t!(Code[" a/"]: "." => Dot); - t!(Code: "=" => Eq); - t!(Code: "==" => EqEq); - t!(Code: "!=" => ExclEq); - t!(Code[" /"]: "<" => Lt); - t!(Code: "<=" => LtEq); - t!(Code: ">" => Gt); - t!(Code: ">=" => GtEq); - t!(Code: "+=" => PlusEq); - t!(Code: "-=" => HyphEq); - t!(Code: "*=" => StarEq); - t!(Code: "/=" => SlashEq); - t!(Code: ".." => Dots); - t!(Code: "=>" => Arrow); - - // Test combinations. - t!(Code: "<=>" => LtEq, Gt); - t!(Code[" a/"]: "..." => Dots, Dot); - - // Test hyphen as symbol vs part of identifier. - t!(Code[" /"]: "-1" => Minus, Int(1)); - t!(Code[" /"]: "-a" => Minus, Ident("a")); - t!(Code[" /"]: "--1" => Minus, Minus, Int(1)); - t!(Code[" /"]: "--_a" => Minus, Minus, Ident("_a")); - t!(Code[" /"]: "a-b" => Ident("a-b")); - - // Test invalid. - t!(Code: r"\" => Error(Full, "not valid here")); - } - - #[test] - fn test_tokenize_keywords() { - // A list of a few (not all) keywords. - let list = [ - ("not", Not), - ("let", Let), - ("if", If), - ("else", Else), - ("for", For), - ("in", In), - ("import", Import), - ]; - - for (s, t) in list.clone() { - t!(Markup[" "]: format!("#{}", s) => t); - t!(Markup[" "]: format!("#{0}#{0}", s) => t, t); - t!(Markup[" /"]: format!("# {}", s) => Text(&format!("# {s}"))); - } - - for (s, t) in list { - t!(Code[" "]: s => t); - t!(Markup[" /"]: s => Text(s)); - } - - // Test simple identifier. - t!(Markup[" "]: "#letter" => Ident("letter")); - t!(Code[" /"]: "falser" => Ident("falser")); - t!(Code[" /"]: "None" => Ident("None")); - t!(Code[" /"]: "True" => Ident("True")); - } - - #[test] - fn test_tokenize_raw_blocks() { - // Test basic raw block. - t!(Markup: "``" => Raw("", None, false)); - t!(Markup: "`raw`" => Raw("raw", None, false)); - t!(Markup[""]: "`]" => Error(End, "expected 1 backtick")); - - // Test special symbols in raw block. - t!(Markup: "`[brackets]`" => Raw("[brackets]", None, false)); - t!(Markup[""]: r"`\`` " => Raw(r"\", None, false), Error(End, "expected 1 backtick")); - - // Test separated closing backticks. - t!(Markup: "```not `y`e`t```" => Raw("`y`e`t", Some("not"), false)); - - // Test more backticks. - t!(Markup: "``nope``" => Raw("", None, false), Text("nope"), Raw("", None, false)); - t!(Markup: "````🚀````" => Raw("", None, false)); - t!(Markup[""]: "`````👩‍🚀````noend" => Error(End, "expected 5 backticks")); - t!(Markup[""]: "````raw``````" => Raw("", Some("raw"), false), Raw("", None, false)); - } - - #[test] - fn test_tokenize_idents() { - // Test valid identifiers. - t!(Code[" /"]: "x" => Ident("x")); - t!(Code[" /"]: "value" => Ident("value")); - t!(Code[" /"]: "__main__" => Ident("__main__")); - t!(Code[" /"]: "_snake_case" => Ident("_snake_case")); - - // Test non-ascii. - t!(Code[" /"]: "α" => Ident("α")); - t!(Code[" /"]: "ម្តាយ" => Ident("ម្តាយ")); - - // Test hyphen parsed as identifier. - t!(Code[" /"]: "kebab-case" => Ident("kebab-case")); - t!(Code[" /"]: "one-10" => Ident("one-10")); - } - - #[test] - fn test_tokenize_numeric() { - let ints = [("7", 7), ("012", 12)]; - let floats = [ - (".3", 0.3), - ("0.3", 0.3), - ("3.", 3.0), - ("3.0", 3.0), - ("14.3", 14.3), - ("10e2", 1000.0), - ("10e+0", 10.0), - ("10e+1", 100.0), - ("10e-2", 0.1), - ("10.e1", 100.0), - ("10.e-1", 1.0), - (".1e1", 1.0), - ("10E2", 1000.0), - ]; - - // Test integers. - for &(s, v) in &ints { - t!(Code[" /"]: s => Int(v)); - } - - // Test floats. - for &(s, v) in &floats { - t!(Code[" /"]: s => Float(v)); - } - - // Test attached numbers. - t!(Code[" /"]: ".2.3" => Float(0.2), Float(0.3)); - t!(Code[" /"]: "1.2.3" => Float(1.2), Float(0.3)); - t!(Code[" /"]: "1e-2+3" => Float(0.01), Plus, Int(3)); - - // Test float from too large integer. - let large = i64::MAX as f64 + 1.0; - t!(Code[" /"]: large.to_string() => Float(large)); - - // Combined integers and floats. - let nums = ints.iter().map(|&(k, v)| (k, v as f64)).chain(floats); - - let suffixes: &[(&str, fn(f64) -> SyntaxKind)] = &[ - ("mm", |x| Numeric(x, Unit::Length(AbsUnit::Mm))), - ("pt", |x| Numeric(x, Unit::Length(AbsUnit::Pt))), - ("cm", |x| Numeric(x, Unit::Length(AbsUnit::Cm))), - ("in", |x| Numeric(x, Unit::Length(AbsUnit::In))), - ("rad", |x| Numeric(x, Unit::Angle(AngleUnit::Rad))), - ("deg", |x| Numeric(x, Unit::Angle(AngleUnit::Deg))), - ("em", |x| Numeric(x, Unit::Em)), - ("fr", |x| Numeric(x, Unit::Fr)), - ("%", |x| Numeric(x, Unit::Percent)), - ]; - - // Numeric types. - for &(suffix, build) in suffixes { - for (s, v) in nums.clone() { - t!(Code[" /"]: format!("{}{}", s, suffix) => build(v)); - } - } - - // Multiple dots close the number. - t!(Code[" /"]: "1..2" => Int(1), Dots, Int(2)); - t!(Code[" /"]: "1..2.3" => Int(1), Dots, Float(2.3)); - t!(Code[" /"]: "1.2..3" => Float(1.2), Dots, Int(3)); - - // Test invalid. - t!(Code[" /"]: "1foo" => Error(Full, "invalid number suffix")); - } - - #[test] - fn test_tokenize_strings() { - // Test basic strings. - t!(Code: "\"hi\"" => Str("hi")); - t!(Code: "\"hi\nthere\"" => Str("hi\nthere")); - t!(Code: "\"🌎\"" => Str("🌎")); - - // Test unterminated. - t!(Code[""]: "\"hi" => Error(End, "expected quote")); - - // Test escaped quote. - t!(Code: r#""a\"bc""# => Str("a\"bc")); - t!(Code[""]: r#""\""# => Error(End, "expected quote")); - } - - #[test] - fn test_tokenize_line_comments() { - // Test line comment with no trailing newline. - t!(Both[""]: "//" => LineComment); - - // Test line comment ends at newline. - t!(Both["a1/"]: "//bc\n" => LineComment, Space(1)); - t!(Both["a1/"]: "// bc \n" => LineComment, Space(1)); - t!(Both["a1/"]: "//bc\r\n" => LineComment, Space(1)); - - // Test nested line comments. - t!(Both["a1/"]: "//a//b\n" => LineComment, Space(1)); - } - - #[test] - fn test_tokenize_block_comments() { - // Test basic block comments. - t!(Both[""]: "/*" => BlockComment); - t!(Both: "/**/" => BlockComment); - t!(Both: "/*🏞*/" => BlockComment); - t!(Both: "/*\n*/" => BlockComment); - - // Test depth 1 and 2 nested block comments. - t!(Both: "/* /* */ */" => BlockComment); - t!(Both: "/*/*/**/*/*/" => BlockComment); - - // Test two nested, one unclosed block comments. - t!(Both[""]: "/*/*/**/*/" => BlockComment); - - // Test all combinations of up to two following slashes and stars. - t!(Both[""]: "/*" => BlockComment); - t!(Both[""]: "/*/" => BlockComment); - t!(Both[""]: "/**" => BlockComment); - t!(Both[""]: "/*//" => BlockComment); - t!(Both[""]: "/*/*" => BlockComment); - t!(Both[""]: "/**/" => BlockComment); - t!(Both[""]: "/***" => BlockComment); - - // Test unexpected terminator. - t!(Both: "/*Hi*/*/" => BlockComment, - Error(Full, "unexpected end of block comment")); - } -} diff --git a/tests/ref/math/simple.png b/tests/ref/math/simple.png index 902354df134c391ad6d0c2c2ba04841149255c1a..72f9c1c6dcf20b2c521ae9850316380f17d6c9c3 100644 GIT binary patch literal 6555 zcmai(XEYqpw#P>weFhOk8zLb}h#rYP2tqL0=tPeiqxVci@15wqjov#!5G8sUy?4PG0H8L0BP{`Qo!v*f>B7|S+mBBgeL2Ecf^{0!qeU&Ra~g70SKtkKNqKScIe7sq z;r`mIwpLsb$*j~t?8(0a6fDZt;uCR!T!Fp>&*s`ay?q9W2A7EZ#WaK$#hea=Hy4uJ zhFdmMJradwfz&VF_eDaa0*I(zR8mt^MFPnrnB=g77{QPIrT=vU5d$|8eczvQa1qa@ z6kgo5=*r%R9u`jTdxWg$nL+3|K3a}}o8+%~qV$Xt?nW7g5I%2|&sE|#i8~=R@YfQ} z*&4+YZhr4jyCHjVe(fv{T+FZeNS}WJ@@QNzUVyFce87`0^VXHW8X_Y%M4S)W8p8$@ zdhf~NRJPUzc?fMWj$5Yy;kIM9RcJ3$s+(<|Gv84R6bIcQIezLz>1E#G6T)Hl{s9{Y zZ1GsTOS-dq1RJ3HO6i8hDj_=jXy!MyhxGLVCOyhIy4tME>OQ5?n9WGmhYd7i2Z~ecH5bg)}ox{YQV$!Li$ycXvLPH`+T0!>UB$ z2;Kgk9mg^@ebAh>!xQW;+uv{W?j~ACyK8FjM;UBmUNFbuwd{9|J|5qSjkIu0-uhfM zpDnN}Xj>R>6==JX2*Bu{>&OXJqr~JK*#C4AHU^gbW_Qx2X&nZ_6+uIriYNA)r7SSH zC4&N{^Z=3%l=X%VgEJ8?B;Ms>W`9#LtUDS@DA*+eY1$T8ezh-LhT2XRr!Tl-mcWK2 zeJg3@YIccx5qJhM`cGX$D&V5We3BMdwym⋙>H@FmFJHB&pue+Y}ele9@41#D-Dz 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 literal 6554 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 diff --git a/tests/ref/math/syntax.png b/tests/ref/math/syntax.png new file mode 100644 index 0000000000000000000000000000000000000000..eaf18528038ddc8b144a3e8e4f44a9c80fb9dfc8 GIT binary patch literal 53462 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: it => table( + columns: 2, + padding: 8pt, + ..it.text + .split("\n") + .map(line => (text(10pt, raw(line, lang: "typ")), eval(line) + [ ])) + .flatten() +) + +``` +Let $x in NN$ be ... +$ (1 + x/2)^2 $ +$ x arrow:l y $ +$ sum_(n=1)^mu 1 + (2pi (5 + n)) / k $ +$ { x in RR | x "is natural" and x < 10 } $ +$ sqrt(x^2) = frac(x, 1) $ +$ "profit" = "income" - "expenses" $ +$ x < #for i in range(5) [$ #i < $] y $ +$ 1 + 2 = #{1 + 2} $ +$ A sub:eq:not B $ +``` + diff --git a/tools/test-helper/extension.js b/tools/test-helper/extension.js index 253c78c71..60fbe71d3 100644 --- a/tools/test-helper/extension.js +++ b/tools/test-helper/extension.js @@ -132,15 +132,19 @@ function getWebviewContent(pngSrc, refSrc, stdout, stderr) {

Standard output

-
${stdout}
+
${escape(stdout)}

Standard error

-
${stderr}
+
${escape(stderr)}
` } +function escape(text) { + return text.replace(//g, ">"); +} + function deactivate() {} module.exports = { activate, deactivate }