diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 1a2116704..4f8961cc0 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -142,6 +142,10 @@ impl Eval for Spanned<&NodeRaw> { })); } + if self.v.block { + ctx.apply_parbreak(); + } + ctx.push(NodeStack { dirs: ctx.state.dirs, align: ctx.state.align, @@ -149,6 +153,10 @@ impl Eval for Spanned<&NodeRaw> { children, }); + if self.v.block { + ctx.apply_parbreak(); + } + ctx.state.font.families = prev; } } @@ -466,8 +474,8 @@ impl Eval for Spanned<&ExprFor> { iterate!(for (k => key, v => value) in dict.into_iter()) } - (ForPattern::KeyValue(..), Value::Str(_)) - | (ForPattern::KeyValue(..), Value::Array(_)) => { + (ForPattern::KeyValue(_, _), Value::Str(_)) + | (ForPattern::KeyValue(_, _), Value::Array(_)) => { ctx.diag(error!(self.v.pat.span, "mismatched pattern")); Value::Error } diff --git a/src/eval/value.rs b/src/eval/value.rs index 860c06348..119a2f1b0 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -110,15 +110,15 @@ impl Pretty for Value { fn pretty(&self, p: &mut Printer) { match self { Value::None => p.push_str("none"), - Value::Bool(v) => write!(p, "{}", v).unwrap(), - Value::Int(v) => p.push_str(itoa::Buffer::new().format(*v)), - Value::Float(v) => p.push_str(ryu::Buffer::new().format(*v)), - Value::Length(v) => write!(p, "{}", v).unwrap(), - Value::Angle(v) => write!(p, "{}", v).unwrap(), - Value::Relative(v) => write!(p, "{}", v).unwrap(), - Value::Linear(v) => write!(p, "{}", v).unwrap(), - Value::Color(v) => write!(p, "{}", v).unwrap(), - Value::Str(v) => write!(p, "{:?}", v).unwrap(), + Value::Bool(v) => v.pretty(p), + Value::Int(v) => v.pretty(p), + Value::Float(v) => v.pretty(p), + Value::Length(v) => v.pretty(p), + Value::Angle(v) => v.pretty(p), + Value::Relative(v) => v.pretty(p), + Value::Linear(v) => v.pretty(p), + Value::Color(v) => v.pretty(p), + Value::Str(v) => v.pretty(p), Value::Array(v) => v.pretty(p), Value::Dict(v) => v.pretty(p), Value::Template(v) => pretty_template(v, p), @@ -134,12 +134,12 @@ pub type ValueArray = Vec; impl Pretty for ValueArray { fn pretty(&self, p: &mut Printer) { - p.push_str("("); + p.push('('); p.join(self, ", ", |item, p| item.pretty(p)); if self.len() == 1 { - p.push_str(","); + p.push(','); } - p.push_str(")"); + p.push(')'); } } @@ -148,9 +148,9 @@ pub type ValueDict = BTreeMap; impl Pretty for ValueDict { fn pretty(&self, p: &mut Printer) { - p.push_str("("); + p.push('('); if self.is_empty() { - p.push_str(":"); + p.push(':'); } else { p.join(self, ", ", |(key, value), p| { p.push_str(key); @@ -158,7 +158,7 @@ impl Pretty for ValueDict { value.pretty(p); }); } - p.push_str(")"); + p.push(')'); } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 3fc7d483a..3fd2cca52 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -59,14 +59,14 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option { Token::Underscore => Node::Emph, Token::Eq => { if *at_start { - return Some(Node::Heading(heading(p))); + return Some(heading(p)); } else { - Node::Text(p.get(p.peek_span()).into()) + Node::Text(p.peek_src().into()) } } Token::Tilde => Node::Text("\u{00A0}".into()), Token::Backslash => Node::Linebreak, - Token::Raw(t) => Node::Raw(raw(p, t)), + Token::Raw(t) => raw(p, t), Token::UnicodeEscape(t) => Node::Text(unicode_escape(p, t)), // Keywords. @@ -122,7 +122,7 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option { } /// Parse a heading. -fn heading(p: &mut Parser) -> NodeHeading { +fn heading(p: &mut Parser) -> Node { // Count depth. let mut level = p.span(|p| { p.assert(&[Token::Eq]); @@ -147,16 +147,16 @@ fn heading(p: &mut Parser) -> NodeHeading { } } - NodeHeading { level, contents } + Node::Heading(NodeHeading { level, contents }) } /// Handle a raw block. -fn raw(p: &mut Parser, token: TokenRaw) -> NodeRaw { +fn raw(p: &mut Parser, token: TokenRaw) -> Node { let raw = resolve::resolve_raw(token.text, token.backticks); if !token.terminated { p.diag(error!(p.peek_span().end, "expected backtick(s)")); } - raw + Node::Raw(raw) } /// Handle a unicode escape sequence. diff --git a/src/parse/parser.rs b/src/parse/parser.rs index b77677727..986a36b09 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -243,6 +243,11 @@ impl<'s> Parser<'s> { ) } + /// Peek at the source of the next token. + pub fn peek_src(&self) -> &'s str { + self.get(self.peek_span()) + } + /// Checks whether the next token fulfills a condition. /// /// Returns `false` if there is no next token. diff --git a/src/parse/resolve.rs b/src/parse/resolve.rs index 3adbf11f4..a5e831daa 100644 --- a/src/parse/resolve.rs +++ b/src/parse/resolve.rs @@ -54,13 +54,13 @@ pub fn resolve_raw(text: &str, backticks: usize) -> NodeRaw { NodeRaw { lang: Ident::new(tag), lines, - inline: !had_newline, + block: had_newline, } } else { NodeRaw { lang: None, lines: split_lines(text), - inline: true, + block: false, } } } @@ -77,10 +77,14 @@ fn split_at_lang_tag(raw: &str) -> (&str, &str) { /// Trim raw text and splits it into lines. /// /// Returns whether at least one newline was contained in `raw`. -fn trim_and_split_raw(raw: &str) -> (Vec, bool) { - // Trims one whitespace at end and start. - let raw = raw.strip_prefix(' ').unwrap_or(raw); - let raw = raw.strip_suffix(' ').unwrap_or(raw); +fn trim_and_split_raw(mut raw: &str) -> (Vec, bool) { + // Trims one space at the start. + raw = raw.strip_prefix(' ').unwrap_or(raw); + + // Trim one space at the end if the last non-whitespace char is a backtick. + if raw.trim_end().ends_with('`') { + raw = raw.strip_suffix(' ').unwrap_or(raw); + } let mut lines = split_lines(raw); let had_newline = lines.len() > 1; @@ -167,29 +171,29 @@ mod tests { backticks: usize, lang: Option<&str>, lines: &[&str], - inline: bool, + block: bool, ) { assert_eq!(resolve_raw(raw, backticks), NodeRaw { lang: lang.map(|id| Ident(id.into())), lines: lines.iter().map(ToString::to_string).collect(), - inline, + block, }); } // Just one backtick. - test("py", 1, None, &["py"], true); - test("1\n2", 1, None, &["1", "2"], true); - test("1\r\n2", 1, None, &["1", "2"], true); + test("py", 1, None, &["py"], false); + test("1\n2", 1, None, &["1", "2"], false); + test("1\r\n2", 1, None, &["1", "2"], false); // More than one backtick with lang tag. - test("js alert()", 2, Some("js"), &["alert()"], true); - test("py quit(\n\n) ", 3, Some("py"), &["quit(", "", ")"], false); - test("♥", 2, None, &[], true); + test("js alert()", 2, Some("js"), &["alert()"], false); + test("py quit(\n\n)", 3, Some("py"), &["quit(", "", ")"], true); + test("♥", 2, None, &[], false); // Trimming of whitespace (tested more thoroughly in separate test). - test(" a", 2, None, &["a"], true); - test(" a", 2, None, &[" a"], true); - test(" \na", 2, None, &["a"], false); + test(" a", 2, None, &["a"], false); + test(" a", 2, None, &[" a"], false); + test(" \na", 2, None, &["a"], true); } #[test] @@ -203,8 +207,11 @@ mod tests { test(" hi", vec![" hi"]); test("\nhi", vec!["hi"]); test(" \n hi", vec![" hi"]); - test("hi ", vec!["hi"]); - test("hi ", vec!["hi "]); + test("hi` ", vec!["hi`"]); + test("hi` ", vec!["hi` "]); + test("hi` ", vec!["hi` "]); + test("hi ", vec!["hi "]); + test("hi ", vec!["hi "]); test("hi\n", vec!["hi"]); test("hi \n ", vec!["hi "]); test(" \n hi \n ", vec![" hi "]); diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index 405352c3a..e3550707d 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -254,22 +254,22 @@ impl<'s> Tokens<'s> { } fn math(&mut self) -> Token<'s> { - let mut inline = true; + let mut display = false; if self.s.eat_if('[') { - inline = false; + display = true; } let start = self.s.index(); let mut escaped = false; - let mut dollar = inline; + let mut dollar = !display; let terminated = loop { match self.s.eat() { Some('$') if !escaped && dollar => break true, Some(']') if !escaped => dollar = true, Some(c) => { - dollar = inline; + dollar = !display; escaped = c == '\\' && !escaped; } None => break false, @@ -277,15 +277,15 @@ impl<'s> Tokens<'s> { }; let end = self.s.index() - - match (terminated, inline) { + - match (terminated, display) { (false, _) => 0, - (true, true) => 1, - (true, false) => 2, + (true, false) => 1, + (true, true) => 2, }; Token::Math(TokenMath { formula: self.s.get(start .. end), - inline, + display, terminated, }) } @@ -470,8 +470,8 @@ mod tests { Token::Raw(TokenRaw { text, backticks, terminated }) } - const fn Math(formula: &str, inline: bool, terminated: bool) -> Token { - Token::Math(TokenMath { formula, inline, terminated }) + const fn Math(formula: &str, display: bool, terminated: bool) -> Token { + Token::Math(TokenMath { formula, display, terminated }) } const fn UnicodeEscape(sequence: &str, terminated: bool) -> Token { @@ -527,7 +527,7 @@ mod tests { ('/', None, "//", LineComment("")), ('/', None, "/**/", BlockComment("")), ('/', Some(Markup), "*", Star), - ('/', Some(Markup), "$ $", Math(" ", true, true)), + ('/', Some(Markup), "$ $", Math(" ", false, true)), ('/', Some(Markup), r"\\", Text(r"\")), ('/', Some(Markup), "#let", Let), ('/', Some(Code), "#if", If), @@ -752,21 +752,21 @@ mod tests { #[test] fn test_tokenize_math_formulas() { // Test basic formula. - t!(Markup: "$$" => Math("", true, true)); - t!(Markup: "$x$" => Math("x", true, true)); - t!(Markup: r"$\\$" => Math(r"\\", true, true)); - t!(Markup: "$[x + y]$" => Math("x + y", false, true)); - t!(Markup: r"$[\\]$" => Math(r"\\", false, true)); + t!(Markup: "$$" => Math("", false, true)); + t!(Markup: "$x$" => Math("x", false, true)); + t!(Markup: r"$\\$" => Math(r"\\", false, true)); + t!(Markup: "$[x + y]$" => Math("x + y", true, true)); + t!(Markup: r"$[\\]$" => Math(r"\\", true, true)); // Test unterminated. - t!(Markup[""]: "$x" => Math("x", true, false)); - t!(Markup[""]: "$[x" => Math("x", false, false)); - t!(Markup[""]: "$[x]\n$" => Math("x]\n$", false, false)); + t!(Markup[""]: "$x" => Math("x", false, false)); + t!(Markup[""]: "$[x" => Math("x", true, false)); + t!(Markup[""]: "$[x]\n$" => Math("x]\n$", true, false)); // Test escape sequences. - t!(Markup: r"$\$x$" => Math(r"\$x", true, true)); - t!(Markup: r"$[\\\]$]$" => Math(r"\\\]$", false, true)); - t!(Markup[""]: r"$[ ]\\$" => Math(r" ]\\$", false, false)); + t!(Markup: r"$\$x$" => Math(r"\$x", false, true)); + t!(Markup: r"$[\\\]$]$" => Math(r"\\\]$", true, true)); + t!(Markup[""]: r"$[ ]\\$" => Math(r" ]\\$", true, false)); } #[test] diff --git a/src/pretty.rs b/src/pretty.rs index f1c3cfb7d..0123e9a75 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -2,10 +2,13 @@ use std::fmt::{Arguments, Result, Write}; +use crate::color::{Color, RgbaColor}; +use crate::geom::{Angle, Length, Linear, Relative}; + /// Pretty print an item and return the resulting string. pub fn pretty(item: &T) -> String where - T: Pretty, + T: Pretty + ?Sized, { let mut p = Printer::new(); item.pretty(&mut p); @@ -29,6 +32,11 @@ impl Printer { Self { buf: String::new() } } + /// Push a character into the buffer. + pub fn push(&mut self, c: char) { + self.buf.push(c); + } + /// Push a string into the buffer. pub fn push_str(&mut self, string: &str) { self.buf.push_str(string); @@ -67,3 +75,64 @@ impl Write for Printer { Ok(()) } } + +impl Pretty for i64 { + fn pretty(&self, p: &mut Printer) { + p.push_str(itoa::Buffer::new().format(*self)); + } +} + +impl Pretty for f64 { + fn pretty(&self, p: &mut Printer) { + p.push_str(ryu::Buffer::new().format(*self)); + } +} + +impl Pretty for str { + fn pretty(&self, p: &mut Printer) { + p.push('"'); + for c in self.chars() { + match c { + '\\' => p.push_str(r"\\"), + '"' => p.push_str(r#"\""#), + '\n' => p.push_str(r"\n"), + '\r' => p.push_str(r"\r"), + '\t' => p.push_str(r"\t"), + _ => p.push(c), + } + } + p.push('"'); + } +} + +macro_rules! impl_pretty_display { + ($($type:ty),* $(,)?) => { + $(impl Pretty for $type { + fn pretty(&self, p: &mut Printer) { + write!(p, "{}", self).unwrap(); + } + })* + }; +} + +impl_pretty_display! { + bool, + Length, + Angle, + Relative, + Linear, + RgbaColor, + Color, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pretty_print_str() { + assert_eq!(pretty("\n"), r#""\n""#); + assert_eq!(pretty("\\"), r#""\\""#); + assert_eq!(pretty("\""), r#""\"""#); + } +} diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index ebe821995..a681aa32a 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -62,24 +62,28 @@ impl Pretty for Expr { fn pretty(&self, p: &mut Printer) { match self { Self::None => p.push_str("none"), - Self::Ident(v) => p.push_str(&v), - Self::Bool(v) => write!(p, "{}", v).unwrap(), - Self::Int(v) => p.push_str(itoa::Buffer::new().format(*v)), - Self::Float(v) => p.push_str(ryu::Buffer::new().format(*v)), - Self::Length(v, u) => write!(p, "{}{}", v, u).unwrap(), - Self::Angle(v, u) => write!(p, "{}{}", v, u).unwrap(), - Self::Percent(v) => write!(p, "{}%", v).unwrap(), - Self::Color(v) => write!(p, "{}", v).unwrap(), - // TODO: Debug escapes a bit more than we want (e.g. apostrophes). - // We probably need to do the escaping ourselves. - Self::Str(v) => write!(p, "{:?}", &v).unwrap(), + Self::Ident(v) => v.pretty(p), + Self::Bool(v) => v.pretty(p), + Self::Int(v) => v.pretty(p), + Self::Float(v) => v.pretty(p), + Self::Length(v, u) => { + write!(p, "{}{}", ryu::Buffer::new().format(*v), u).unwrap(); + } + Self::Angle(v, u) => { + write!(p, "{}{}", ryu::Buffer::new().format(*v), u).unwrap(); + } + Self::Percent(v) => { + write!(p, "{}%", ryu::Buffer::new().format(*v)).unwrap(); + } + Self::Color(v) => v.pretty(p), + Self::Str(v) => v.pretty(p), Self::Array(v) => v.pretty(p), Self::Dict(v) => v.pretty(p), Self::Template(v) => pretty_template(v, p), Self::Group(v) => { - p.push_str("("); + p.push('('); v.v.pretty(p); - p.push_str(")"); + p.push(')'); } Self::Block(v) => v.pretty(p), Self::Unary(v) => v.pretty(p), @@ -98,12 +102,12 @@ pub type ExprArray = SpanVec; impl Pretty for ExprArray { fn pretty(&self, p: &mut Printer) { - p.push_str("("); + p.push('('); p.join(self, ", ", |item, p| item.v.pretty(p)); if self.len() == 1 { - p.push_str(","); + p.push(','); } - p.push_str(")"); + p.push(')'); } } @@ -112,13 +116,13 @@ pub type ExprDict = Vec; impl Pretty for ExprDict { fn pretty(&self, p: &mut Printer) { - p.push_str("("); + p.push('('); if self.is_empty() { - p.push_str(":"); + p.push(':'); } else { p.join(self, ", ", |named, p| named.pretty(p)); } - p.push_str(")"); + p.push(')'); } } @@ -133,7 +137,7 @@ pub struct Named { impl Pretty for Named { fn pretty(&self, p: &mut Printer) { - p.push_str(&self.name.v); + self.name.v.pretty(p); p.push_str(": "); self.expr.v.pretty(p); } @@ -147,9 +151,9 @@ pub fn pretty_template(template: &ExprTemplate, p: &mut Printer) { if let [Spanned { v: Node::Expr(Expr::Call(call)), .. }] = template.as_slice() { pretty_func_template(call, p, false) } else { - p.push_str("["); + p.push('['); template.pretty(p); - p.push_str("]"); + p.push(']'); } } @@ -167,15 +171,15 @@ pub struct ExprBlock { impl Pretty for ExprBlock { fn pretty(&self, p: &mut Printer) { - p.push_str("{"); + p.push('{'); if self.exprs.len() > 1 { - p.push_str(" "); + p.push(' '); } p.join(&self.exprs, "; ", |expr, p| expr.v.pretty(p)); if self.exprs.len() > 1 { - p.push_str(" "); + p.push(' '); } - p.push_str("}"); + p.push('}'); } } @@ -192,7 +196,7 @@ impl Pretty for ExprUnary { fn pretty(&self, p: &mut Printer) { self.op.v.pretty(p); if self.op.v == UnOp::Not { - p.push_str(" "); + p.push(' '); } self.expr.v.pretty(p); } @@ -258,9 +262,9 @@ pub struct ExprBinary { impl Pretty for ExprBinary { fn pretty(&self, p: &mut Printer) { self.lhs.v.pretty(p); - p.push_str(" "); + p.push(' '); self.op.v.pretty(p); - p.push_str(" "); + p.push(' '); self.rhs.v.pretty(p); } } @@ -419,9 +423,9 @@ pub struct ExprCall { impl Pretty for ExprCall { fn pretty(&self, p: &mut Printer) { self.callee.v.pretty(p); - p.push_str("("); + p.push('('); self.args.v.pretty(p); - p.push_str(")"); + p.push(')'); } } @@ -444,7 +448,7 @@ pub fn pretty_func_template(call: &ExprCall, p: &mut Printer, chained: bool) { { // Previous arguments. if !head.is_empty() { - p.push_str(" "); + p.push(' '); p.join(head, ", ", |item, p| item.pretty(p)); } @@ -458,12 +462,12 @@ pub fn pretty_func_template(call: &ExprCall, p: &mut Printer, chained: bool) { template.pretty(p); } } else if !call.args.v.is_empty() { - p.push_str(" "); + p.push(' '); call.args.v.pretty(p); } // Either end of header or end of body. - p.push_str("]"); + p.push(']'); } /// The arguments to a function: `12, draw: false`. @@ -508,7 +512,7 @@ pub struct ExprLet { impl Pretty for ExprLet { fn pretty(&self, p: &mut Printer) { p.push_str("#let "); - p.push_str(&self.pat.v); + self.pat.v.pretty(p); if let Some(init) = &self.init { p.push_str(" = "); init.v.pretty(p); @@ -531,7 +535,7 @@ impl Pretty for ExprIf { fn pretty(&self, p: &mut Printer) { p.push_str("#if "); self.condition.v.pretty(p); - p.push_str(" "); + p.push(' '); self.if_body.v.pretty(p); if let Some(expr) = &self.else_body { p.push_str(" #else "); @@ -557,7 +561,7 @@ impl Pretty for ExprFor { self.pat.v.pretty(p); p.push_str(" #in "); self.iter.v.pretty(p); - p.push_str(" "); + p.push(' '); self.body.v.pretty(p); } } @@ -574,11 +578,11 @@ pub enum ForPattern { impl Pretty for ForPattern { fn pretty(&self, p: &mut Printer) { match self { - Self::Value(v) => p.push_str(&v), + Self::Value(v) => v.pretty(p), Self::KeyValue(k, v) => { - p.push_str(&k); + k.pretty(p); p.push_str(", "); - p.push_str(&v); + v.pretty(p); } } } diff --git a/src/syntax/ident.rs b/src/syntax/ident.rs index 3cb47c47d..c4cc19bc4 100644 --- a/src/syntax/ident.rs +++ b/src/syntax/ident.rs @@ -2,6 +2,8 @@ use std::ops::Deref; use unicode_xid::UnicodeXID; +use crate::pretty::{Pretty, Printer}; + /// An Unicode identifier with a few extra permissible characters. /// /// In addition to what is specified in the [Unicode Standard][uax31], we allow: @@ -28,6 +30,12 @@ impl Ident { } } +impl Pretty for Ident { + fn pretty(&self, p: &mut Printer) { + p.push_str(self.as_str()); + } +} + impl AsRef for Ident { fn as_ref(&self) -> &str { self diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index 409e8cbf2..2a8c4dbbd 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -64,9 +64,18 @@ mod tests { // Raw. roundtrip("``"); - roundtrip("`lang 1`"); - test("``` hi```", "`hi`"); - test("``` ` ```", "```"); + roundtrip("`nolang 1`"); + roundtrip("```lang 1```"); + roundtrip("```lang 1 ```"); + roundtrip("```hi line ```"); + roundtrip("```py\ndef\n```"); + roundtrip("```\n line \n```"); + roundtrip("```\n`\n```"); + roundtrip("``` ` ```"); + test("```1 ```", "``"); + test("``` 1```", "`1`"); + test("``` 1 ```", "`1 `"); + test("```` ` ````", "``` ` ```"); } #[test] @@ -77,12 +86,12 @@ mod tests { roundtrip("{true}"); roundtrip("{10}"); roundtrip("{3.14}"); - roundtrip("{10pt}"); + roundtrip("{10.0pt}"); roundtrip("{14.1deg}"); - roundtrip("{20%}"); + roundtrip("{20.0%}"); roundtrip("{#abcdef}"); roundtrip(r#"{"hi"}"#); - test(r#"{"let's go"}"#, r#"{"let\'s go"}"#); + test(r#"{"let's \" go"}"#, r#"{"let's \" go"}"#); // Arrays. roundtrip("{()}"); diff --git a/src/syntax/node.rs b/src/syntax/node.rs index f7625036e..7b6aa728f 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -26,9 +26,9 @@ pub enum Node { impl Pretty for Node { fn pretty(&self, p: &mut Printer) { match self { - Self::Strong => p.push_str("*"), - Self::Emph => p.push_str("_"), - Self::Space => p.push_str(" "), + Self::Strong => p.push('*'), + Self::Emph => p.push('_'), + Self::Space => p.push(' '), Self::Linebreak => p.push_str(r"\"), Self::Parbreak => p.push_str("\n\n"), Self::Text(text) => p.push_str(&text), @@ -46,10 +46,10 @@ impl Pretty for Node { } } -/// A section heading: `# Introduction`. +/// A section heading: `= Introduction`. #[derive(Debug, Clone, PartialEq)] pub struct NodeHeading { - /// The section depth (numer of hashtags minus 1, capped at 5). + /// The section depth (numer of equals signs minus 1, capped at 5). pub level: Spanned, /// The contents of the heading. pub contents: Tree, @@ -58,7 +58,7 @@ pub struct NodeHeading { impl Pretty for NodeHeading { fn pretty(&self, p: &mut Printer) { for _ in 0 ..= self.level.v { - p.push_str("="); + p.push('='); } self.contents.pretty(p); } @@ -67,8 +67,7 @@ impl Pretty for NodeHeading { /// A raw block with optional syntax highlighting: `` `raw` ``. /// /// Raw blocks start with 1 or 3+ backticks and end with the same number of -/// backticks. If you want to include a sequence of backticks in a raw block, -/// simply surround the block with more backticks. +/// backticks. /// /// When using at least three backticks, an optional language tag may follow /// directly after the backticks. This tag defines which language to @@ -86,7 +85,7 @@ impl Pretty for NodeHeading { /// ````typst /// ```rust println!("hello!")```; /// ```` -/// - Blocks can span multiple lines. +/// - Blocks can span multiple lines. /// ````typst /// ```rust /// loop { @@ -94,34 +93,40 @@ impl Pretty for NodeHeading { /// } /// ``` /// ```` -/// - Start with a space to omit the language tag (the space will be trimmed -/// from the output) and use more backticks to allow backticks in the raw -/// text. +/// - Start with a space to omit the language tag (the space will be trimmed +/// from the output). /// `````typst -/// ```` This contains ```backticks``` and has no leading & trailing spaces. ```` +/// ```` This has no leading space.```` +/// ````` +/// - Use more backticks to allow backticks in the raw text. +/// `````typst +/// ```` This contains ```backticks```.```` /// ````` /// -/// # Trimming -/// If we would always render the raw text between the backticks exactly as -/// given, a few things would become problematic or even impossible: -/// - Typical multiline code blocks (like in the example above) would have an -/// additional newline before and after the code. -/// - The first word of text wrapped in more than three backticks would always -/// be interpreted as a language tag which means that text without leading -/// space would be impossible. -/// - A single backtick without surrounding spaces could not exist as raw text -/// since it would be interpreted as belonging to the opening or closing -/// backticks. +/// # Trimming +/// If we would always render the raw text between the backticks exactly as +/// given, some things would become cumbersome/impossible to write: +/// - Typical multiline code blocks (like in the example above) would have an +/// additional newline before and after the code. +/// - Multi-line blocks would need to start with a space since a word would be +/// interpreted as a language tag. +/// - Text ending with a backtick would be impossible since the backtick would +/// be interpreted as belonging to the closing backticks. /// -/// To fix these problems, we trim blocks with 3+ backticks as follows: -/// - A single space or a sequence of whitespace followed by a newline at the start. -/// - A single space or a newline followed by a sequence of whitespace at the end. +/// To fix these problems, we sometimes trim a bit of space from blocks with 3+ +/// backticks: +/// - At the start, we trim a single space or a sequence of whitespace followed +/// by a newline. +/// - At the end, we trim +/// - a single space if the raw text ends with a backtick followed only by +/// whitespace, +/// - a newline followed by a sequence of whitespace. /// -/// With these rules, a single raw backtick can be produced by the sequence -/// ```` ``` ` ``` ````, ```` ``` unhighlighted text ``` ```` has no -/// surrounding spaces and multiline code blocks don't have extra empty lines. -/// Note that you can always force leading or trailing whitespace simply by -/// adding more spaces. +/// You can thus produce a single backtick without surrounding spaces with the +/// sequence ```` ``` ` ``` ````. +/// +/// Note that with these rules you can always force leading or trailing +/// whitespace simply by adding more spaces. #[derive(Debug, Clone, PartialEq)] pub struct NodeRaw { /// An optional identifier specifying the language to syntax-highlight in. @@ -129,28 +134,65 @@ pub struct NodeRaw { /// The lines of raw text, determined as the raw string between the /// backticks trimmed according to the above rules and split at newlines. pub lines: Vec, - /// Whether the element can be layouted inline. - /// - /// - When true, it will be layouted integrated within the surrounding - /// paragraph. - /// - When false, it will be separated into its own paragraph. - /// - /// Single-backtick blocks are always inline-level. Multi-backtick blocks - /// are inline-level when they contain no newlines. - pub inline: bool, + /// Whether the element is block-level, that is, it has 3+ backticks + /// and contains at least one newline. + pub block: bool, } impl Pretty for NodeRaw { fn pretty(&self, p: &mut Printer) { - p.push_str("`"); - if let Some(lang) = &self.lang { - p.push_str(&lang); - p.push_str(" "); + // Find out how many backticks we need. + let mut backticks = 1; + + // Language tag and block-level are only possible with 3+ backticks. + if self.lang.is_some() || self.block { + backticks = 3; } - // TODO: Technically, we should handle backticks in the lines by - // wrapping with more backticks, and we should add space before the - // first and/or after the last line if necessary. + + // More backticks may be required if there are lots of consecutive + // backticks in the lines. + let mut count = 0; + for line in &self.lines { + for c in line.chars() { + if c == '`' { + count += 1; + backticks = backticks.max(3).max(count + 1); + } else { + count = 0; + } + } + } + + // Starting backticks. + for _ in 0 .. backticks { + p.push('`'); + } + + // Language tag. + if let Some(lang) = &self.lang { + lang.pretty(p); + } + + // Start untrimming. + if self.block { + p.push('\n'); + } else if backticks >= 3 { + p.push(' '); + } + + // The lines. p.join(&self.lines, "\n", |line, p| p.push_str(line)); - p.push_str("`"); + + // End untrimming. + if self.block { + p.push('\n'); + } else if self.lines.last().map_or(false, |line| line.trim_end().ends_with('`')) { + p.push(' '); + } + + // Ending backticks. + for _ in 0 .. backticks { + p.push('`'); + } } } diff --git a/src/syntax/token.rs b/src/syntax/token.rs index c4b9ec8fd..5e69a350a 100644 --- a/src/syntax/token.rs +++ b/src/syntax/token.rs @@ -170,9 +170,9 @@ pub struct TokenRaw<'s> { pub struct TokenMath<'s> { /// The formula between the dollars. pub formula: &'s str, - /// Whether the formula was surrounded by one dollar (true) or two dollars - /// (false). - pub inline: bool, + /// Whether the formula is display-level, that is, it is surrounded by + /// `$[..]`. + pub display: bool, /// Whether the closing dollars were present. pub terminated: bool, } @@ -243,8 +243,8 @@ impl<'s> Token<'s> { Self::Bool(_) => "boolean", Self::Int(_) => "integer", Self::Float(_) => "float", - Self::Length(..) => "length", - Self::Angle(..) => "angle", + Self::Length(_, _) => "length", + Self::Angle(_, _) => "angle", Self::Percent(_) => "percentage", Self::Color(_) => "color", Self::Str(_) => "string", diff --git a/tests/lang/ref/raw.png b/tests/lang/ref/raw.png index 0da49c1b8..fbf027608 100644 Binary files a/tests/lang/ref/raw.png and b/tests/lang/ref/raw.png differ diff --git a/tests/lang/typ/raw.typ b/tests/lang/typ/raw.typ index 753f1a09c..a783216ea 100644 --- a/tests/lang/typ/raw.typ +++ b/tests/lang/typ/raw.typ @@ -9,6 +9,21 @@ `#let x = 1` \ `#[f 1]` +--- +// Multiline block splits paragraphs. + +First +``` +Second +``` +Third + +--- +// Lots of backticks inside. +```` +```backticks``` +```` + --- // Trimming. @@ -18,7 +33,9 @@ The keyword ```rust let```. // Trimming depends on number backticks. <``> \ <` untrimmed `> \ -<``` trimmed ```> +<``` trimmed` ```> \ +<``` trimmed ```> \ +<``` trimmed```> // Multiline trimming. ```py @@ -28,12 +45,6 @@ def hi(): print("Hi!") ``` ---- -// Lots of backticks inside. -```` -```backticks``` -```` - --- // Unterminated. // Error: 2:1-2:1 expected backtick(s) diff --git a/tests/typeset.rs b/tests/typeset.rs index 63831c229..807d55d9b 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -335,7 +335,7 @@ fn register_helpers(scope: &mut Scope, panics: Rc>>) { } value.pretty(p); }); - p.push_str(")"); + p.push(')'); Value::Str(p.finish()) }