Tidy up raw blocks 🧹

- Better trimming (only trim at the end if necessary)
- Fixed block-level layouting
- Improved pretty printing
- Flip inline variable to block
- Flip inline variable to display for math formulas
This commit is contained in:
Laurenz 2021-02-03 21:30:36 +01:00
parent 6fcef9973b
commit d86a5e8a1f
15 changed files with 337 additions and 174 deletions

View File

@ -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
}

View File

@ -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<Value>;
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<String, Value>;
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(')');
}
}

View File

@ -59,14 +59,14 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option<Node> {
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<Node> {
}
/// 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.

View File

@ -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.

View File

@ -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<String>, 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<String>, 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 "]);

View File

@ -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]

View File

@ -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<T>(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#""\"""#);
}
}

View File

@ -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<Expr>;
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<Named>;
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);
}
}
}

View File

@ -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<str> for Ident {
fn as_ref(&self) -> &str {
self

View File

@ -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("{()}");

View File

@ -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<u8>,
/// 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<String>,
/// 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('`');
}
}
}

View File

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -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)

View File

@ -335,7 +335,7 @@ fn register_helpers(scope: &mut Scope, panics: Rc<RefCell<Vec<Panic>>>) {
}
value.pretty(p);
});
p.push_str(")");
p.push(')');
Value::Str(p.finish())
}