From 38c5c362419c5eee7a4fdc0b43d3a9dfb339a6d2 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 8 Nov 2021 12:13:32 +0100 Subject: [PATCH] Final touches --- Cargo.toml | 3 +- benches/oneshot.rs | 4 +-- src/eval/capture.rs | 1 + src/eval/mod.rs | 64 +++++++++++++++++--------------------- src/eval/walk.rs | 17 ++++++++++ src/parse/mod.rs | 28 ++++++++--------- src/parse/parser.rs | 30 +++++++++--------- src/parse/resolve.rs | 12 +++---- src/parse/tokens.rs | 52 +++++++++++++++---------------- src/source.rs | 1 + src/syntax/ast.rs | 27 +++++++++------- src/syntax/mod.rs | 43 +++---------------------- src/syntax/pretty.rs | 64 +++++++++++++++++++++++--------------- tests/ref/markup/math.png | Bin 0 -> 2448 bytes tests/typ/code/array.typ | 2 +- tests/typ/code/call.typ | 2 +- tests/typ/code/dict.typ | 2 +- tests/typ/markup/math.typ | 12 +++++++ 18 files changed, 183 insertions(+), 181 deletions(-) create mode 100644 tests/ref/markup/math.png create mode 100644 tests/typ/markup/math.typ diff --git a/Cargo.toml b/Cargo.toml index 6a5b72b99..c7fa703c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,10 @@ authors = ["The Typst Project Developers"] edition = "2018" [features] -default = ["cli", "fs", "layout-cache", "parse-cache"] +default = ["cli", "fs", "layout-cache"] cli = ["anyhow", "codespan-reporting", "fs", "pico-args", "same-file"] fs = ["dirs", "memmap2", "same-file", "walkdir"] layout-cache = ["rand"] -parse-cache = [] [profile.dev] # Faster compilation diff --git a/benches/oneshot.rs b/benches/oneshot.rs index 63f201ac5..bb385688f 100644 --- a/benches/oneshot.rs +++ b/benches/oneshot.rs @@ -44,11 +44,11 @@ fn bench_scan(iai: &mut Iai) { } fn bench_tokenize(iai: &mut Iai) { - iai.run(|| Tokens::new(black_box(&SRC), black_box(TokenMode::Markup)).count()); + iai.run(|| Tokens::new(black_box(SRC), black_box(TokenMode::Markup)).count()); } fn bench_parse(iai: &mut Iai) { - iai.run(|| parse(&SRC)); + iai.run(|| parse(SRC)); } fn bench_eval(iai: &mut Iai) { diff --git a/src/eval/capture.rs b/src/eval/capture.rs index 4e24bc908..e47831dfd 100644 --- a/src/eval/capture.rs +++ b/src/eval/capture.rs @@ -172,5 +172,6 @@ mod tests { // Scoping. test("{ let x = 1; { let y = 2; y }; x + y }", &["y"]); + test("[#let x = 1]#x", &["x"]); } } diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 540b58b98..fda2184e6 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -398,11 +398,11 @@ impl Eval for CallArgs { value: Spanned::new(expr.eval(ctx)?, expr.span()), }); } - CallArg::Named(x) => { + CallArg::Named(named) => { items.push(Arg { span, - name: Some(x.name().take().into()), - value: Spanned::new(x.expr().eval(ctx)?, x.expr().span()), + name: Some(named.name().take().into()), + value: Spanned::new(named.expr().eval(ctx)?, named.expr().span()), }); } CallArg::Spread(expr) => match expr.eval(ctx)? { @@ -511,8 +511,8 @@ impl Eval for WithExpr { type Output = Value; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let wrapped = - self.callee().eval(ctx)?.cast::().at(self.callee().span())?; + let callee = self.callee(); + let wrapped = callee.eval(ctx)?.cast::().at(callee.span())?; let applied = self.args().eval(ctx)?; let name = wrapped.name().cloned(); @@ -529,7 +529,7 @@ impl Eval for LetExpr { type Output = Value; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let value = match &self.init() { + let value = match self.init() { Some(expr) => expr.eval(ctx)?, None => Value::None, }; @@ -542,15 +542,10 @@ impl Eval for IfExpr { type Output = Value; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let condition = self - .condition() - .eval(ctx)? - .cast::() - .at(self.condition().span())?; - - if condition { + let condition = self.condition(); + if condition.eval(ctx)?.cast::().at(condition.span())? { self.if_body().eval(ctx) - } else if let Some(else_body) = &self.else_body() { + } else if let Some(else_body) = self.else_body() { else_body.eval(ctx) } else { Ok(Value::None) @@ -564,14 +559,11 @@ impl Eval for WhileExpr { fn eval(&self, ctx: &mut EvalContext) -> TypResult { let mut output = Value::None; - while self - .condition() - .eval(ctx)? - .cast::() - .at(self.condition().span())? - { - let value = self.body().eval(ctx)?; - output = ops::join(output, value).at(self.body().span())?; + let condition = self.condition(); + while condition.eval(ctx)?.cast::().at(condition.span())? { + let body = self.body(); + let value = body.eval(ctx)?; + output = ops::join(output, value).at(body.span())?; } Ok(output) @@ -597,7 +589,7 @@ impl Eval for ForExpr { } ctx.scopes.exit(); - Ok(output) + return Ok(output); }}; } @@ -607,18 +599,20 @@ impl Eval for ForExpr { let value = pattern.value().take(); match (key, value, iter) { - (None, v, Value::Str(string)) => iter!(for (v => value) in string.iter()), + (None, v, Value::Str(string)) => { + iter!(for (v => value) in string.iter()); + } (None, v, Value::Array(array)) => { - iter!(for (v => value) in array.into_iter()) + iter!(for (v => value) in array.into_iter()); } (Some(i), v, Value::Array(array)) => { - iter!(for (i => idx, v => value) in array.into_iter().enumerate()) + iter!(for (i => idx, v => value) in array.into_iter().enumerate()); } (None, v, Value::Dict(dict)) => { - iter!(for (v => value) in dict.into_iter().map(|p| p.1)) + iter!(for (v => value) in dict.into_iter().map(|p| p.1)); } (Some(k), v, Value::Dict(dict)) => { - iter!(for (k => key, v => value) in dict.into_iter()) + iter!(for (k => key, v => value) in dict.into_iter()); } (_, _, Value::Str(_)) => { bail!(pattern.span(), "mismatched pattern"); @@ -634,9 +628,9 @@ impl Eval for ImportExpr { type Output = Value; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let path = self.path().eval(ctx)?.cast::().at(self.path().span())?; - - let file = ctx.import(&path, self.path().span())?; + let path = self.path(); + let resolved = path.eval(ctx)?.cast::().at(path.span())?; + let file = ctx.import(&resolved, path.span())?; let module = &ctx.modules[&file]; match self.imports() { @@ -664,12 +658,10 @@ impl Eval for IncludeExpr { type Output = Value; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let path_node = self.path(); - let path = path_node.eval(ctx)?.cast::().at(path_node.span())?; - - let file = ctx.import(&path, path_node.span())?; + let path = self.path(); + let resolved = path.eval(ctx)?.cast::().at(path.span())?; + let file = ctx.import(&resolved, path.span())?; let module = &ctx.modules[&file]; - Ok(Value::Template(module.template.clone())) } } diff --git a/src/eval/walk.rs b/src/eval/walk.rs index 1656929b8..aab32f406 100644 --- a/src/eval/walk.rs +++ b/src/eval/walk.rs @@ -33,6 +33,7 @@ impl Walk for MarkupNode { Self::Emph => ctx.template.modify(|s| s.text_mut().emph.flip()), Self::Text(text) => ctx.template.text(text), Self::Raw(raw) => raw.walk(ctx)?, + Self::Math(math) => math.walk(ctx)?, Self::Heading(heading) => heading.walk(ctx)?, Self::List(list) => list.walk(ctx)?, Self::Enum(enum_) => enum_.walk(ctx)?, @@ -67,6 +68,22 @@ impl Walk for RawNode { } } +impl Walk for MathNode { + fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { + if self.display { + ctx.template.parbreak(); + } + + ctx.template.monospace(self.formula.trim()); + + if self.display { + ctx.template.parbreak(); + } + + Ok(()) + } +} + impl Walk for HeadingNode { fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { let level = self.level(); diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 78e4f896d..f9c0049f0 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -16,8 +16,8 @@ use crate::syntax::ast::{Associativity, BinOp, UnOp}; use crate::syntax::{ErrorPos, Green, GreenNode, NodeKind}; /// Parse a source file. -pub fn parse(source: &str) -> Rc { - let mut p = Parser::new(source); +pub fn parse(src: &str) -> Rc { + let mut p = Parser::new(src); markup(&mut p); match p.finish().into_iter().next() { Some(Green::Node(node)) => node, @@ -93,16 +93,17 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) { | NodeKind::Strong | NodeKind::Linebreak | NodeKind::Raw(_) + | NodeKind::Math(_) | NodeKind::UnicodeEscape(_) => { p.eat(); } NodeKind::Eq if *at_start => heading(p), - NodeKind::ListBullet if *at_start => list_node(p), + NodeKind::Minus if *at_start => list_node(p), NodeKind::EnumNumbering(_) if *at_start => enum_node(p), // Line-based markup that is not currently at the start of the line. - NodeKind::Eq | NodeKind::ListBullet | NodeKind::EnumNumbering(_) => { + NodeKind::Eq | NodeKind::Minus | NodeKind::EnumNumbering(_) => { p.convert(NodeKind::Text(p.peek_src().into())); } @@ -149,7 +150,7 @@ fn heading(p: &mut Parser) { /// Parse a single list item. fn list_node(p: &mut Parser) { p.perform(NodeKind::List, |p| { - p.eat_assert(&NodeKind::ListBullet); + p.eat_assert(&NodeKind::Minus); let column = p.column(p.prev_end()); markup_indented(p, column); }); @@ -193,10 +194,7 @@ fn expr_prec(p: &mut Parser, atomic: bool, min_prec: usize) -> ParseResult { loop { // Exclamation mark, parenthesis or bracket means this is a function // call. - if matches!( - p.peek_direct(), - Some(NodeKind::LeftParen | NodeKind::LeftBracket) - ) { + if let Some(NodeKind::LeftParen | NodeKind::LeftBracket) = p.peek_direct() { call(p, marker)?; continue; } @@ -241,7 +239,6 @@ fn primary(p: &mut Parser, atomic: bool) -> ParseResult { match p.peek() { // Things that start with an identifier. Some(NodeKind::Ident(_)) => { - // Start closure params. let marker = p.marker(); p.eat(); @@ -364,9 +361,10 @@ enum CollectionKind { /// Returns the length of the collection and whether the literal contained any /// commas. fn collection(p: &mut Parser) -> (CollectionKind, usize) { - let mut items = 0; let mut kind = CollectionKind::Positional; + let mut items = 0; let mut can_group = true; + let mut error = false; let mut missing_coma: Option = None; while !p.eof() { @@ -393,12 +391,14 @@ fn collection(p: &mut Parser) -> (CollectionKind, usize) { if p.eat_if(&NodeKind::Comma) { can_group = false; } else { - missing_coma = Some(p.marker()); + missing_coma = Some(p.trivia_start()); } + } else { + error = true; } } - if can_group && items == 1 { + if error || (can_group && items == 1) { kind = CollectionKind::Group; } @@ -467,7 +467,7 @@ fn params(p: &mut Parser, marker: Marker) { NodeKind::Named | NodeKind::Comma | NodeKind::Ident(_) => Ok(()), NodeKind::Spread if matches!( - x.children().last().map(|x| x.kind()), + x.children().last().map(|child| child.kind()), Some(&NodeKind::Ident(_)) ) => { diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 5ebc2c17e..1c4c2a5c1 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -52,6 +52,17 @@ impl<'s> Parser<'s> { Marker(self.children.len()) } + /// Create a markup right before the trailing trivia. + pub fn trivia_start(&self) -> Marker { + let count = self + .children + .iter() + .rev() + .take_while(|node| self.is_trivia(node.kind())) + .count(); + Marker(self.children.len() - count) + } + /// Perform a subparse that wraps its result in a node with the given kind. pub fn perform(&mut self, kind: NodeKind, f: F) -> T where @@ -66,7 +77,7 @@ impl<'s> Parser<'s> { // Trailing trivia should not be wrapped into the new node. let idx = self.children.len(); self.children.push(Green::default()); - self.children.extend(children.drain(until ..)); + self.children.extend(children.drain(until.0 ..)); self.children[idx] = GreenNode::with_children(kind, children).into(); } else { self.children.push(GreenNode::with_children(kind, children).into()); @@ -238,7 +249,7 @@ impl<'s> Parser<'s> { // Rescan the peeked token if the mode changed. if rescan { if group_mode == TokenMode::Code { - self.children.truncate(self.trivia_start()); + self.children.truncate(self.trivia_start().0); } self.tokens.jump(self.prev_end()); @@ -290,17 +301,6 @@ impl<'s> Parser<'s> { } } - /// Find the index in the children list where trailing trivia starts. - fn trivia_start(&self) -> usize { - self.children.len() - - self - .children - .iter() - .rev() - .take_while(|node| self.is_trivia(node.kind())) - .count() - } - /// Whether the active group must end at a newline. fn stop_at_newline(&self) -> bool { matches!( @@ -350,7 +350,7 @@ impl Parser<'_> { /// Add an error that the `thing` was expected at the end of the last /// non-trivia token. pub fn expected_at(&mut self, thing: &str) { - Marker(self.trivia_start()).expected(self, thing); + self.trivia_start().expected(self, thing); } } @@ -374,7 +374,7 @@ impl Marker { /// with the given `kind`. pub fn end(self, p: &mut Parser, kind: NodeKind) { let until = p.trivia_start(); - let children = p.children.drain(self.0 .. until).collect(); + let children = p.children.drain(self.0 .. until.0).collect(); p.children .insert(self.0, GreenNode::with_children(kind, children).into()); } diff --git a/src/parse/resolve.rs b/src/parse/resolve.rs index 6719f41df..e15ae339d 100644 --- a/src/parse/resolve.rs +++ b/src/parse/resolve.rs @@ -1,5 +1,5 @@ use super::{is_ident, is_newline, Scanner}; -use crate::syntax::RawData; +use crate::syntax::ast::RawNode; use crate::util::EcoString; /// Resolve all escape sequences in a string. @@ -46,21 +46,19 @@ pub fn resolve_hex(sequence: &str) -> Option { } /// Resolve the language tag and trims the raw text. -pub fn resolve_raw(column: usize, backticks: u8, text: &str) -> RawData { +pub fn resolve_raw(column: usize, backticks: usize, text: &str) -> RawNode { if backticks > 1 { let (tag, inner) = split_at_lang_tag(text); let (text, block) = trim_and_split_raw(column, inner); - RawData { + RawNode { lang: is_ident(tag).then(|| tag.into()), text: text.into(), - backticks, block, } } else { - RawData { + RawNode { lang: None, text: split_lines(text).join("\n").into(), - backticks, block: false, } } @@ -181,7 +179,7 @@ mod tests { #[track_caller] fn test( column: usize, - backticks: u8, + backticks: usize, raw: &str, lang: Option<&str>, text: &str, diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index 1523cd643..96dfd9d15 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -5,7 +5,8 @@ use super::{ Scanner, }; use crate::geom::{AngularUnit, LengthUnit}; -use crate::syntax::*; +use crate::syntax::ast::{MathNode, RawNode}; +use crate::syntax::{ErrorPos, NodeKind}; use crate::util::EcoString; /// An iterator over the tokens of a string of source code. @@ -26,8 +27,8 @@ pub enum TokenMode { impl<'s> Tokens<'s> { /// Create a new token iterator with the given mode. #[inline] - pub fn new(source: &'s str, mode: TokenMode) -> Self { - Self { s: Scanner::new(source), mode } + pub fn new(src: &'s str, mode: TokenMode) -> Self { + Self { s: Scanner::new(src), mode } } /// Get the current token mode. @@ -254,7 +255,7 @@ impl<'s> Tokens<'s> { } } c if c.is_whitespace() => NodeKind::Linebreak, - _ => NodeKind::Text("\\".into()), + _ => NodeKind::Text('\\'.into()), }, None => NodeKind::Linebreak, } @@ -281,7 +282,7 @@ impl<'s> Tokens<'s> { NodeKind::EnDash } } else if self.s.check_or(true, char::is_whitespace) { - NodeKind::ListBullet + NodeKind::Minus } else { NodeKind::Text("-".into()) } @@ -310,16 +311,15 @@ impl<'s> Tokens<'s> { let column = self.s.column(self.s.index() - 1); let mut backticks = 1; - while self.s.eat_if('`') && backticks < u8::MAX { + while self.s.eat_if('`') { backticks += 1; } // Special case for empty inline block. if backticks == 2 { - return NodeKind::Raw(Rc::new(RawData { + return NodeKind::Raw(Rc::new(RawNode { text: EcoString::new(), lang: None, - backticks: 1, block: false, })); } @@ -389,7 +389,7 @@ impl<'s> Tokens<'s> { }; if terminated { - NodeKind::Math(Rc::new(MathData { + NodeKind::Math(Rc::new(MathNode { formula: self.s.get(start .. end).into(), display, })) @@ -429,9 +429,7 @@ impl<'s> Tokens<'s> { // Read the exponent. if self.s.eat_if('e') || self.s.eat_if('E') { - if !self.s.eat_if('+') { - self.s.eat_if('-'); - } + let _ = self.s.eat_if('+') || self.s.eat_if('-'); self.s.eat_while(|c| c.is_ascii_digit()); } @@ -483,6 +481,7 @@ impl<'s> Tokens<'s> { false } })); + if self.s.eat_if('"') { NodeKind::Str(string) } else { @@ -567,17 +566,16 @@ mod tests { NodeKind::Error(pos, message.into()) } - fn Raw(text: &str, lang: Option<&str>, backticks_left: u8, block: bool) -> NodeKind { - NodeKind::Raw(Rc::new(RawData { + fn Raw(text: &str, lang: Option<&str>, block: bool) -> NodeKind { + NodeKind::Raw(Rc::new(RawNode { text: text.into(), lang: lang.map(Into::into), - backticks: backticks_left, block, })) } fn Math(formula: &str, display: bool) -> NodeKind { - NodeKind::Math(Rc::new(MathData { formula: formula.into(), display })) + NodeKind::Math(Rc::new(MathNode { formula: formula.into(), display })) } fn Str(string: &str) -> NodeKind { @@ -655,13 +653,13 @@ mod tests { ]; // Test with each applicable suffix. - for (block, mode, suffix, token) in suffixes { + for &(block, mode, suffix, ref token) in suffixes { let src = $src; #[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) { + if (mode.is_none() || mode == Some($mode)) && blocks.contains(block) { t!(@$mode: format!("{}{}", src, suffix) => $($token,)* token); } } @@ -790,7 +788,7 @@ mod tests { t!(Markup: "~" => NonBreakingSpace); t!(Markup[" "]: r"\" => Linebreak); t!(Markup["a "]: r"a--" => Text("a"), EnDash); - t!(Markup["a1/"]: "- " => ListBullet, Space(0)); + t!(Markup["a1/"]: "- " => Minus, Space(0)); t!(Markup[" "]: "." => EnumNumbering(None)); t!(Markup[" "]: "1." => EnumNumbering(Some(1))); t!(Markup[" "]: "1.a" => Text("1."), Text("a")); @@ -867,22 +865,22 @@ mod tests { #[test] fn test_tokenize_raw_blocks() { // Test basic raw block. - t!(Markup: "``" => Raw("", None, 1, false)); - t!(Markup: "`raw`" => Raw("raw", None, 1, false)); + 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, 1, false)); - t!(Markup[""]: r"`\`` " => Raw(r"\", None, 1, false), Error(End, "expected 1 backtick")); + 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"), 3, false)); + t!(Markup: "```not `y`e`t```" => Raw("`y`e`t", Some("not"), false)); // Test more backticks. - t!(Markup: "``nope``" => Raw("", None, 1, false), Text("nope"), Raw("", None, 1, false)); - t!(Markup: "````🚀````" => Raw("", None, 4, false)); + 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"), 4, false), Raw("", None, 1, false)); + t!(Markup[""]: "````raw``````" => Raw("", Some("raw"), false), Raw("", None, false)); } #[test] diff --git a/src/source.rs b/src/source.rs index 713380c58..85f48491a 100644 --- a/src/source.rs +++ b/src/source.rs @@ -144,6 +144,7 @@ impl SourceFile { } } + /// The file's abstract syntax tree. pub fn ast(&self) -> TypResult { let red = RedNode::from_root(self.root.clone(), self.id); let errors = red.errors(); diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 067bd6daf..288c749a0 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -68,11 +68,8 @@ impl Markup { NodeKind::EnDash => Some(MarkupNode::Text("\u{2013}".into())), NodeKind::EmDash => Some(MarkupNode::Text("\u{2014}".into())), NodeKind::NonBreakingSpace => Some(MarkupNode::Text("\u{00A0}".into())), - NodeKind::Raw(raw) => Some(MarkupNode::Raw(RawNode { - block: raw.block, - lang: raw.lang.clone(), - text: raw.text.clone(), - })), + NodeKind::Math(math) => Some(MarkupNode::Math(math.as_ref().clone())), + NodeKind::Raw(raw) => Some(MarkupNode::Raw(raw.as_ref().clone())), NodeKind::Heading => node.cast().map(MarkupNode::Heading), NodeKind::List => node.cast().map(MarkupNode::List), NodeKind::Enum => node.cast().map(MarkupNode::Enum), @@ -98,6 +95,8 @@ pub enum MarkupNode { Text(EcoString), /// A raw block with optional syntax highlighting: `` `...` ``. Raw(RawNode), + /// A math formula: `$a^2 = b^2 + c^2$`. + Math(MathNode), /// A section heading: `= Introduction`. Heading(HeadingNode), /// An item in an unordered list: `- ...`. @@ -121,6 +120,16 @@ pub struct RawNode { pub block: bool, } +/// A math formula: `$a^2 + b^2 = c^2$`. +#[derive(Debug, Clone, PartialEq)] +pub struct MathNode { + /// The formula between the dollars / brackets. + pub formula: EcoString, + /// Whether the formula is display-level, that is, it is surrounded by + /// `$[..]$`. + pub display: bool, +} + node! { /// A section heading: `= Introduction`. HeadingNode: Heading @@ -133,12 +142,8 @@ impl HeadingNode { } /// The section depth (numer of equals signs). - pub fn level(&self) -> u8 { - self.0 - .children() - .filter(|n| n.kind() == &NodeKind::Eq) - .count() - .min(u8::MAX.into()) as u8 + pub fn level(&self) -> usize { + self.0.children().filter(|n| n.kind() == &NodeKind::Eq).count() } } diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index 0660d57b9..ca6ed2430 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -10,7 +10,7 @@ use std::rc::Rc; pub use pretty::*; pub use span::*; -use self::ast::TypedNode; +use self::ast::{MathNode, RawNode, TypedNode}; use crate::diag::Error; use crate::geom::{AngularUnit, LengthUnit}; use crate::source::SourceId; @@ -178,7 +178,7 @@ impl From for Green { /// A owned wrapper for a green node with span information. /// -/// Owned variant of [`RedRef`]. Can be [cast](Self::cast) to an AST nodes. +/// Owned variant of [`RedRef`]. Can be [cast](Self::cast) to an AST node. #[derive(Clone, PartialEq)] pub struct RedNode { id: SourceId, @@ -192,15 +192,6 @@ impl RedNode { Self { id, offset: 0, green: root.into() } } - /// Create a new red node from a node kind and a span. - pub fn from_data(kind: NodeKind, span: Span) -> Self { - Self { - id: span.source, - offset: span.start, - green: Green::Token(GreenData { kind, len: span.len() }), - } - } - /// Convert to a borrowed representation. pub fn as_ref(&self) -> RedRef<'_> { RedRef { @@ -527,13 +518,11 @@ pub enum NodeKind { EnumNumbering(Option), /// An item in an unordered list: `- ...`. List, - /// The bullet character of an item in an unordered list: `-`. - ListBullet, /// An arbitrary number of backticks followed by inner contents, terminated /// with the same number of backticks: `` `...` ``. - Raw(Rc), + Raw(Rc), /// Dollar signs surrounding inner contents. - Math(Rc), + Math(Rc), /// An identifier: `center`. Ident(EcoString), /// A boolean: `true`, `false`. @@ -613,29 +602,6 @@ pub enum NodeKind { Unknown(EcoString), } -/// Payload of a raw block node. -#[derive(Debug, Clone, PartialEq)] -pub struct RawData { - /// The raw text in the block. - pub text: EcoString, - /// The programming language of the raw text. - pub lang: Option, - /// The number of opening backticks. - pub backticks: u8, - /// Whether to display this as a block. - pub block: bool, -} - -/// Payload of a math formula node. -#[derive(Debug, Clone, PartialEq)] -pub struct MathData { - /// The formula between the dollars / brackets. - pub formula: EcoString, - /// Whether the formula is display-level, that is, it is surrounded by - /// `$[..]$`. - pub display: bool, -} - /// Where in a node an error should be annotated. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ErrorPos { @@ -730,7 +696,6 @@ impl NodeKind { Self::Enum => "enumeration item", Self::EnumNumbering(_) => "enumeration item numbering", Self::List => "list item", - Self::ListBullet => "list bullet", Self::Raw(_) => "raw block", Self::Math(_) => "math formula", Self::Ident(_) => "identifier", diff --git a/src/syntax/pretty.rs b/src/syntax/pretty.rs index 9e4510b62..c453fb563 100644 --- a/src/syntax/pretty.rs +++ b/src/syntax/pretty.rs @@ -63,7 +63,6 @@ impl Printer { write_item(item, self); count += 1; } - count } @@ -99,6 +98,7 @@ impl Pretty for MarkupNode { Self::Emph => p.push('_'), Self::Text(text) => p.push_str(text), Self::Raw(raw) => raw.pretty(p), + Self::Math(math) => math.pretty(p), Self::Heading(heading) => heading.pretty(p), Self::List(list) => list.pretty(p), Self::Enum(enum_) => enum_.pretty(p), @@ -168,6 +168,20 @@ impl Pretty for RawNode { } } +impl Pretty for MathNode { + fn pretty(&self, p: &mut Printer) { + p.push('$'); + if self.display { + p.push('['); + } + p.push_str(&self.formula); + if self.display { + p.push(']'); + } + p.push('$'); + } +} + impl Pretty for HeadingNode { fn pretty(&self, p: &mut Printer) { for _ in 0 .. self.level() { @@ -253,12 +267,9 @@ impl Pretty for ArrayExpr { impl Pretty for DictExpr { fn pretty(&self, p: &mut Printer) { p.push('('); - - let mut items = self.items().peekable(); - if items.peek().is_none() { + let len = p.join(self.items(), ", ", |named, p| named.pretty(p)); + if len == 0 { p.push(':'); - } else { - p.join(items, ", ", |named, p| named.pretty(p)); } p.push(')'); } @@ -291,13 +302,11 @@ impl Pretty for GroupExpr { impl Pretty for BlockExpr { fn pretty(&self, p: &mut Printer) { p.push('{'); - - let exprs: Vec<_> = self.exprs().collect(); - if exprs.len() > 1 { + if self.exprs().count() > 1 { p.push(' '); } - p.join(&exprs, "; ", |expr, p| expr.pretty(p)); - if exprs.len() > 1 { + let len = p.join(self.exprs(), "; ", |expr, p| expr.pretty(p)); + if len > 1 { p.push(' '); } p.push('}'); @@ -348,17 +357,17 @@ impl Pretty for CallExpr { }; let args: Vec<_> = self.args().items().collect(); - - if let Some(Expr::Template(template)) = args - .last() - .and_then(|x| if let CallArg::Pos(arg) = x { Some(arg) } else { None }) - { - if args.len() > 1 { - write_args(&args[0 .. args.len() - 1]); + match args.as_slice() { + // This can be moved behind the arguments. + // + // Example: Transforms "#v(a, [b])" => "#v(a)[b]". + [head @ .., CallArg::Pos(Expr::Template(template))] => { + if !head.is_empty() { + write_args(head); + } + template.pretty(p); } - template.pretty(p); - } else { - write_args(&args); + items => write_args(items), } } } @@ -423,12 +432,12 @@ impl Pretty for LetExpr { fn pretty(&self, p: &mut Printer) { p.push_str("let "); self.binding().pretty(p); - if let Some(Expr::Closure(closure)) = &self.init() { + if let Some(Expr::Closure(closure)) = self.init() { p.push('('); p.join(closure.params(), ", ", |item, p| item.pretty(p)); p.push_str(") = "); closure.body().pretty(p); - } else if let Some(init) = &self.init() { + } else if let Some(init) = self.init() { p.push_str(" = "); init.pretty(p); } @@ -441,7 +450,7 @@ impl Pretty for IfExpr { self.condition().pretty(p); p.push(' '); self.if_body().pretty(p); - if let Some(expr) = &self.else_body() { + if let Some(expr) = self.else_body() { p.push_str(" else "); expr.pretty(p); } @@ -525,7 +534,7 @@ mod tests { #[track_caller] fn test_parse(src: &str, expected: &str) { let source = SourceFile::detached(src); - let ast: Markup = source.ast().unwrap(); + let ast = source.ast().unwrap(); let found = pretty(&ast); if found != expected { println!("tree: {:#?}", ast); @@ -563,6 +572,11 @@ mod tests { test_parse("``` 1```", "`1`"); test_parse("``` 1 ```", "`1 `"); test_parse("```` ` ````", "``` ` ```"); + + // Math node. + roundtrip("$$"); + roundtrip("$a+b$"); + roundtrip("$[ a^2 + b^2 = c^2 ]$"); } #[test] diff --git a/tests/ref/markup/math.png b/tests/ref/markup/math.png new file mode 100644 index 0000000000000000000000000000000000000000..426f3dbfb88fc52c5f4a2cb688131b56055371a5 GIT binary patch literal 2448 zcmaJ@c{CIX7avSxh~7+6NDPBY=A~((u`kao2`@{sWS1?&H(4H8!lWl+j5TReF+@a~ ztVN1vd5vX8S)MUdk-;EhXpH&#&UfBB-}~cz_uO;OJ@=e@?zzkF++W7e_E>pY6sC`rd~)>islgn5O2{S!>tsUD?Z|ExG^ zt1sPqb0EDOc9fA1+WiS;Y^m!QggvVB2i#Mbg#PR~?LET7j{KOMKAu;8=cD<*k|qU$ z0u*9C$a$^w<+>=oE|Moa%2*zW$FslfJjWCZ%?>4v4ElP!tAR3fu?D%Hlv^+cG&jZ( zdOv2Q=k_UFXq2ur4XRC6(O;Fz@9kl6^rn+w@6n79oz}BF)k=Q+phd=#8uu}=%$qKw zz^Z;+q!|F`Ia?AtEkJiGLQ_)Y`1>W&qpE|YtTUX$K}tweWKF*+%i^iEUj;!jM4!wW zC%+3D0;L-uu$l+^%D79=nBht%jW?NvcGsI{(ij^hr;5XO zH@in%h>6JTbex9gkDqEk_-cd}IMRv*jLtwwUoHKe6d2K<@|NDA_uWrZh3h4Wz`IlM zX4NB%ohfi|E&bg@lrHit?G3L6Tt3i0Jb}s$r~rlWL9f2oIjK{7>lw|(?U&!=hGEayF;i?S|1Ce`t%5Ec11QFUdNvfB!rI;wN_^h1a1@LvCB1!MuYU# z2uv9hQ1VPA8-2}yEGJ*q^N^VmGHh^}5HG#!6QQ4ZX-u`ZcA&v)IbSNU+5C{FOgo%41{@9lg!y$&=nNi4P^wCRa94pKH6!^*d-H2;3>E3r@? zpd|#zb%i(YBn(y1Ckm~}4PTok+z?@x7w_r)~5Pgd86Ek6!T z+AWC()g|rPk@MwAUiorhZ90{#yEU6xSGS;7v%vp|3}IBHaf)DLPS=G?4cZw0v($tB zJyJlC2Lxx7&#b<>*vRA>YDP6&zq+6Miv2~ruj}Ng`#Siv3^`Y^F@!c|T3ET%pg6=H z#xU80pnNUqfDWmYhmvOuXLHytud}p=fRv9NQ~D;+8FGVcfj(lz;hy~J!<+wvW*E3D z6RP!>?5Gka_5MDJ|IJ(}g4(;BF>NY3J89BZ82jS9K&}h2HqaV4zv4!TM7kcg0$(|8 zg;Pid`^L#Ls+z6e1_|rig=?}TZK)Nr)I?OE(ANG&vwW*dy1>oCJi|L3qG0QHIVwJ) zw4Lfu?wdh-d4D?k}>63vb(b1%=z$`d^VrJ^~4(Un+el8M? z*iuSJo_``xsLT_}0Hv_*n-W^s3rOc7_rMT-wYIWfHLJSC;oKdF-x=bqu3S*wi0-As z>L=$^U0`eJ0l!9^qZ!(#Y^EDTxrBC7RC5>hx|9SLs&bbx7oN_Rlcy^?;!a0KE0pTw8DBTk*Y0f z>7a=)1#`IY3f@}#z0)9dbMovPaY0-97g{#<+09B%+{O{>-V7k?HtF{4C~ZvUz-+OLkjGIY$t9cpIx#Fg9oBCM3FyJ>9jAGsvgo(Y zh;(!?N8M&lx7KCRBC)=73aohMZslw7C~k_rsXd7kqZ*`X~YH`;i@0Q zR@P1wj=rK?UR#;lXs@=rK>e`DiaAntl7@x)<<0o29ScI)Uj8k=&z+VFQk$Z>p32hK zT7Qbni&#5Scj`Kqo``t9YiFlpc(b$l9BGD}2ZL&g8W*LiHri%-|NmZB_5lAP cLE+m!&^W9H?V%2-|4=Qwjr~t`Rz9i!26O;~?f?J) literal 0 HcmV?d00001 diff --git a/tests/typ/code/array.typ b/tests/typ/code/array.typ index 44b8b5979..df37dd454 100644 --- a/tests/typ/code/array.typ +++ b/tests/typ/code/array.typ @@ -72,7 +72,7 @@ {(,1)} // Missing expression makes named pair incomplete, making this an empty array. -// Error: 3-5 expected expression, found named pair +// Error: 5 expected expression {(a:)} // Named pair after this is already identified as an array. diff --git a/tests/typ/code/call.typ b/tests/typ/code/call.typ index 95d75595d..2c16af1cf 100644 --- a/tests/typ/code/call.typ +++ b/tests/typ/code/call.typ @@ -72,7 +72,7 @@ // Error: 10-12 expected expression, found end of block comment #func(a:1*/) -// Error: 9 expected comma +// Error: 8 expected comma #func(1 2) // Error: 7-8 expected identifier diff --git a/tests/typ/code/dict.typ b/tests/typ/code/dict.typ index 757759aca..b369b8b65 100644 --- a/tests/typ/code/dict.typ +++ b/tests/typ/code/dict.typ @@ -42,7 +42,7 @@ // Identified as dictionary due to initial colon. // Error: 4-5 expected named pair, found expression -// Error: 6 expected comma +// Error: 5 expected comma // Error: 12-16 expected identifier // Error: 17-18 expected expression, found colon {(:1 b:"", true::)} diff --git a/tests/typ/markup/math.typ b/tests/typ/markup/math.typ new file mode 100644 index 000000000..cad01d107 --- /dev/null +++ b/tests/typ/markup/math.typ @@ -0,0 +1,12 @@ +// Test math formulas. + +--- +The sum of $a$ and $b$ is $a + b$. + +--- +We will show that: +$[ a^2 + b^2 = c^2 ]$ + +--- +// Error: 2:1 expected closing bracket and dollar sign +$[a