From 7b4d4d6002a9c3da8fafd912f3c7b2da617f19c0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 6 Jan 2021 01:32:59 +0100 Subject: [PATCH] =?UTF-8?q?Pretty=20printing=20=F0=9F=A6=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Syntax tree and value pretty printing - Better value evaluation (top-level strings and content are evaluated plainly, everything else is pretty printed) --- src/color.rs | 2 + src/eval/mod.rs | 3 +- src/eval/value.rs | 147 +++++++++++++++++++--------- src/lib.rs | 1 + src/parse/resolve.rs | 5 + src/pretty.rs | 68 +++++++++++++ src/syntax/expr.rs | 223 +++++++++++++++++++++++++++++++++++++++++++ src/syntax/mod.rs | 30 +++++- src/syntax/node.rs | 100 ++++++++++++++++++- 9 files changed, 533 insertions(+), 46 deletions(-) create mode 100644 src/pretty.rs diff --git a/src/color.rs b/src/color.rs index d74584ae7..11cc5b3b7 100644 --- a/src/color.rs +++ b/src/color.rs @@ -126,6 +126,7 @@ mod tests { #[test] fn test_parse_color_strings() { + #[track_caller] fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); } @@ -139,6 +140,7 @@ mod tests { #[test] fn test_parse_invalid_colors() { + #[track_caller] fn test(hex: &str) { assert_eq!(RgbaColor::from_str(hex), Err(ParseRgbaError)); } diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 42a5555d5..1c6e3d51f 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -169,7 +169,7 @@ impl Eval for Spanned<&Lit> { fn eval(self, ctx: &mut EvalContext) -> Self::Output { match *self.v { - Lit::Ident(ref v) => match ctx.state.scope.get(v.as_str()) { + Lit::Ident(ref v) => match ctx.state.scope.get(&v) { Some(value) => value.clone(), None => { ctx.diag(error!(self.span, "unknown variable")); @@ -286,6 +286,7 @@ fn add(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value { // Complex data types to themselves. (Str(a), Str(b)) => Str(a + &b), + (Array(a), Array(b)) => Array(concat(a, b)), (Dict(a), Dict(b)) => Dict(concat(a, b)), (Content(a), Content(b)) => Content(concat(a, b)), diff --git a/src/eval/value.rs b/src/eval/value.rs index 80c6b8200..507419879 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -1,5 +1,5 @@ use std::any::Any; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::fmt::{self, Debug, Display, Formatter}; use std::ops::Deref; use std::rc::Rc; @@ -7,10 +7,11 @@ use std::rc::Rc; use super::{Args, Eval, EvalContext}; use crate::color::Color; use crate::geom::{Length, Linear, Relative}; -use crate::syntax::{Spanned, Tree, WithSpan}; +use crate::pretty::{pretty, Pretty, Printer}; +use crate::syntax::{pretty_content_expr, Spanned, Tree, WithSpan}; /// A computational value. -#[derive(Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum Value { /// The value that indicates the absence of a meaningful value. None, @@ -82,21 +83,9 @@ impl Eval for &Value { fn eval(self, ctx: &mut EvalContext) -> Self::Output { ctx.push(ctx.make_text_node(match self { Value::None => return, - Value::Bool(v) => v.to_string(), - Value::Int(v) => v.to_string(), - Value::Float(v) => v.to_string(), - Value::Length(v) => v.to_string(), - Value::Relative(v) => v.to_string(), - Value::Linear(v) => v.to_string(), - Value::Color(v) => v.to_string(), - Value::Str(v) => v.clone(), - // TODO: Find good representation for composite types. - Value::Array(_v) => "(array)".into(), - Value::Dict(_v) => "(dictionary)".into(), + Value::Str(s) => s.clone(), Value::Content(tree) => return tree.eval(ctx), - Value::Func(v) => v.to_string(), - Value::Any(v) => v.to_string(), - Value::Error => "(error)".into(), + other => pretty(other), })); } } @@ -107,24 +96,24 @@ impl Default for Value { } } -impl Debug for Value { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { +impl Pretty for Value { + fn pretty(&self, p: &mut Printer) { match self { - Self::None => f.pad("None"), - Self::Bool(v) => Debug::fmt(v, f), - Self::Int(v) => Debug::fmt(v, f), - Self::Float(v) => Debug::fmt(v, f), - Self::Length(v) => Debug::fmt(v, f), - Self::Relative(v) => Debug::fmt(v, f), - Self::Linear(v) => Debug::fmt(v, f), - Self::Color(v) => Debug::fmt(v, f), - Self::Str(v) => Debug::fmt(v, f), - Self::Array(v) => Debug::fmt(v, f), - Self::Dict(v) => Debug::fmt(v, f), - Self::Content(v) => Debug::fmt(v, f), - Self::Func(v) => Debug::fmt(v, f), - Self::Any(v) => Debug::fmt(v, f), - Self::Error => f.pad("Error"), + Value::None => p.push_str("none"), + Value::Bool(v) => write!(p, "{}", v).unwrap(), + Value::Int(v) => write!(p, "{}", v).unwrap(), + Value::Float(v) => write!(p, "{}", v).unwrap(), + Value::Length(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::Array(array) => array.pretty(p), + Value::Dict(dict) => dict.pretty(p), + Value::Content(content) => pretty_content_expr(content, p), + Value::Func(v) => v.pretty(p), + Value::Any(v) => v.pretty(p), + Value::Error => p.push_str("(error)"), } } } @@ -132,8 +121,35 @@ impl Debug for Value { /// An array value: `(1, "hi", 12cm)`. pub type ValueArray = Vec; +impl Pretty for ValueArray { + fn pretty(&self, p: &mut Printer) { + p.push_str("("); + p.join(self, ", ", |item, p| item.pretty(p)); + if self.len() == 1 { + p.push_str(","); + } + p.push_str(")"); + } +} + /// A dictionary value: `(color: #f79143, pattern: dashed)`. -pub type ValueDict = HashMap; +pub type ValueDict = BTreeMap; + +impl Pretty for ValueDict { + fn pretty(&self, p: &mut Printer) { + p.push_str("("); + if self.is_empty() { + p.push_str(":"); + } else { + p.join(self, ", ", |(key, value), p| { + p.push_str(key); + p.push_str(": "); + value.pretty(p); + }); + } + p.push_str(")"); + } +} /// A content value: `{*Hi* there}`. pub type ValueContent = Tree; @@ -169,14 +185,15 @@ impl Deref for ValueFunc { } } -impl Display for ValueFunc { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "", self.name) +impl Pretty for ValueFunc { + fn pretty(&self, p: &mut Printer) { + write!(p, "(function {})", self.name).unwrap(); } } + impl Debug for ValueFunc { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Display::fmt(self, f) + f.debug_struct("ValueFunc").field("name", &self.name).finish() } } @@ -229,15 +246,15 @@ impl PartialEq for ValueAny { } } -impl Display for ValueAny { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Display::fmt(&self.0, f) +impl Pretty for ValueAny { + fn pretty(&self, p: &mut Printer) { + write!(p, "{}", self.0).unwrap(); } } impl Debug for ValueAny { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Debug::fmt(&self.0, f) + f.debug_tuple("ValueAny").field(&self.0).finish() } } @@ -465,3 +482,47 @@ macro_rules! impl_type { } }; } + +#[cfg(test)] +mod tests { + use super::*; + use crate::color::RgbaColor; + use crate::pretty::pretty; + use crate::syntax::Node; + + #[track_caller] + fn test_pretty(value: impl Into, exp: &str) { + assert_eq!(pretty(&value.into()), exp); + } + + #[test] + fn test_pretty_print_values() { + test_pretty(Value::None, "none"); + test_pretty(false, "false"); + test_pretty(12.4, "12.4"); + test_pretty(Length::ZERO, "0pt"); + test_pretty(Relative::ONE, "100%"); + test_pretty(Relative::new(0.3) + Length::cm(2.0), "30% + 2cm"); + test_pretty(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "#010101"); + test_pretty("hello", r#""hello""#); + test_pretty(vec![Spanned::zero(Node::Strong)], "{*}"); + test_pretty(ValueFunc::new("nil", |_, _| Value::None), "(function nil)"); + test_pretty(ValueAny::new(1), "1"); + test_pretty(Value::Error, "(error)"); + } + + #[test] + fn test_pretty_print_collections() { + // Array. + test_pretty(Value::Array(vec![]), "()"); + test_pretty(vec![Value::None], "(none,)"); + test_pretty(vec![Value::Int(1), Value::Int(2)], "(1, 2)"); + + // Dictionary. + let mut dict = BTreeMap::new(); + dict.insert("one".into(), Value::Int(1)); + dict.insert("two".into(), Value::Int(2)); + test_pretty(BTreeMap::new(), "(:)"); + test_pretty(dict, "(one: 1, two: 2)"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 15d688034..05cc0569b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ pub mod library; pub mod paper; pub mod parse; pub mod prelude; +pub mod pretty; pub mod shaping; pub mod syntax; diff --git a/src/parse/resolve.rs b/src/parse/resolve.rs index d6c6d8a42..c4afc430c 100644 --- a/src/parse/resolve.rs +++ b/src/parse/resolve.rs @@ -124,6 +124,7 @@ mod tests { #[test] fn test_resolve_strings() { + #[track_caller] fn test(string: &str, expected: &str) { assert_eq!(resolve_string(string), expected.to_string()); } @@ -144,6 +145,7 @@ mod tests { #[test] fn test_split_at_lang_tag() { + #[track_caller] fn test(text: &str, lang: &str, inner: &str) { assert_eq!(split_at_lang_tag(text), (lang, inner)); } @@ -158,6 +160,7 @@ mod tests { #[test] fn test_resolve_raw() { + #[track_caller] fn test( raw: &str, backticks: usize, @@ -190,6 +193,7 @@ mod tests { #[test] fn test_trim_raw() { + #[track_caller] fn test(text: &str, expected: Vec<&str>) { assert_eq!(trim_and_split_raw(text).0, expected); } @@ -207,6 +211,7 @@ mod tests { #[test] fn test_split_lines() { + #[track_caller] fn test(text: &str, expected: Vec<&str>) { assert_eq!(split_lines(text), expected); } diff --git a/src/pretty.rs b/src/pretty.rs new file mode 100644 index 000000000..a7482869b --- /dev/null +++ b/src/pretty.rs @@ -0,0 +1,68 @@ +//! Pretty printing. + +use std::fmt::{Arguments, Result, Write}; + +/// Pretty print an item and return the resulting string. +pub fn pretty(item: &T) -> String +where + T: Pretty, +{ + let mut p = Printer::new(); + item.pretty(&mut p); + p.finish() +} + +/// Pretty printing. +pub trait Pretty { + /// Pretty print this item into the given printer. + fn pretty(&self, p: &mut Printer); +} + +/// A buffer into which items are printed. +pub struct Printer { + buf: String, +} + +impl Printer { + /// Create a new pretty printer. + pub fn new() -> Self { + Self { buf: String::new() } + } + + /// Push a string into the buffer. + pub fn push_str(&mut self, string: &str) { + self.buf.push_str(string); + } + + /// Write formatted items into the buffer. + pub fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result { + Write::write_fmt(self, fmt) + } + + /// Write a comma-separated list of items. + pub fn join(&mut self, items: I, joiner: &str, mut write_item: F) + where + I: IntoIterator, + F: FnMut(T, &mut Self), + { + let mut iter = items.into_iter(); + if let Some(first) = iter.next() { + write_item(first, self); + } + for item in iter { + self.push_str(joiner); + write_item(item, self); + } + } + + /// Finish pretty printing and return the underlying buffer. + pub fn finish(self) -> String { + self.buf + } +} + +impl Write for Printer { + fn write_str(&mut self, s: &str) -> Result { + Ok(self.push_str(s)) + } +} diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index cf6611f93..4916f34f6 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -21,6 +21,35 @@ pub enum Expr { Content(ExprContent), } +impl Pretty for Expr { + fn pretty(&self, p: &mut Printer) { + match self { + Self::Lit(lit) => lit.pretty(p), + Self::Call(call) => call.pretty(p), + Self::Unary(unary) => unary.pretty(p), + Self::Binary(binary) => binary.pretty(p), + Self::Array(array) => array.pretty(p), + Self::Dict(dict) => dict.pretty(p), + Self::Content(content) => pretty_content_expr(content, p), + } + } +} + +/// Pretty print content in an expression context. +pub fn pretty_content_expr(tree: &Tree, p: &mut Printer) { + if let [Spanned { v: Node::Expr(Expr::Call(call)), .. }] = tree.as_slice() { + // Remove unncessary braces from content expression containing just a + // single function call. + // + // Example: Transforms "{(call: {[f]})}" => "{(call: [f])}" + pretty_bracket_call(call, p, false); + } else { + p.push_str("{"); + tree.pretty(p); + p.push_str("}"); + } +} + /// An invocation of a function: `[foo ...]`, `foo(...)`. #[derive(Debug, Clone, PartialEq)] pub struct ExprCall { @@ -30,12 +59,68 @@ pub struct ExprCall { pub args: Spanned, } +impl Pretty for ExprCall { + fn pretty(&self, p: &mut Printer) { + p.push_str(&self.name.v); + p.push_str("("); + self.args.v.pretty(p); + p.push_str(")"); + } +} + +/// Pretty print a bracketed function call, with body or chaining when possible. +pub fn pretty_bracket_call(call: &ExprCall, p: &mut Printer, chained: bool) { + if chained { + p.push_str(" | "); + } else { + p.push_str("["); + } + + // Function name. + p.push_str(&call.name.v); + + // Find out whether this can be written as body or chain. + // + // Example: Transforms "[v {Hi}]" => "[v][Hi]". + if let [head @ .., Argument::Pos(Spanned { v: Expr::Content(content), .. })] = + call.args.v.as_slice() + { + // Previous arguments. + if !head.is_empty() { + p.push_str(" "); + p.join(head, ", ", |item, p| item.pretty(p)); + } + + // Find out whether this can written as a chain. + // + // Example: Transforms "[v][[f]]" => "[v | f]". + if let [Spanned { v: Node::Expr(Expr::Call(call)), .. }] = content.as_slice() { + return pretty_bracket_call(call, p, true); + } else { + p.push_str("]["); + content.pretty(p); + } + } else if !call.args.v.is_empty() { + p.push_str(" "); + call.args.v.pretty(p); + } + + // Either end of header or end of body. + p.push_str("]"); +} + /// The arguments to a function: `12, draw: false`. /// /// In case of a bracketed invocation with a body, the body is _not_ /// included in the span for the sake of clearer error messages. pub type ExprArgs = Vec; +impl Pretty for Vec { + fn pretty(&self, p: &mut Printer) { + p.join(self, ", ", |item, p| item.pretty(p)); + } +} + /// An argument to a function call: `12` or `draw: false`. #[derive(Debug, Clone, PartialEq)] pub enum Argument { @@ -45,6 +130,15 @@ pub enum Argument { Named(Named), } +impl Pretty for Argument { + fn pretty(&self, p: &mut Printer) { + match self { + Self::Pos(expr) => expr.v.pretty(p), + Self::Named(named) => named.pretty(p), + } + } +} + /// A pair of a name and an expression: `pattern: dashed`. #[derive(Debug, Clone, PartialEq)] pub struct Named { @@ -54,6 +148,14 @@ pub struct Named { pub expr: Spanned, } +impl Pretty for Named { + fn pretty(&self, p: &mut Printer) { + p.push_str(&self.name.v); + p.push_str(": "); + self.expr.v.pretty(p); + } +} + /// A unary operation: `-x`. #[derive(Debug, Clone, PartialEq)] pub struct ExprUnary { @@ -63,6 +165,13 @@ pub struct ExprUnary { pub expr: Box>, } +impl Pretty for ExprUnary { + fn pretty(&self, p: &mut Printer) { + self.op.v.pretty(p); + self.expr.v.pretty(p); + } +} + /// A unary operator. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum UnOp { @@ -70,6 +179,14 @@ pub enum UnOp { Neg, } +impl Pretty for UnOp { + fn pretty(&self, p: &mut Printer) { + p.push_str(match self { + Self::Neg => "-", + }); + } +} + /// A binary operation: `a + b`, `a / b`. #[derive(Debug, Clone, PartialEq)] pub struct ExprBinary { @@ -81,6 +198,16 @@ pub struct ExprBinary { pub rhs: Box>, } +impl Pretty for ExprBinary { + fn pretty(&self, p: &mut Printer) { + self.lhs.v.pretty(p); + p.push_str(" "); + self.op.v.pretty(p); + p.push_str(" "); + self.rhs.v.pretty(p); + } +} + /// A binary operator. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum BinOp { @@ -94,12 +221,46 @@ pub enum BinOp { Div, } +impl Pretty for BinOp { + fn pretty(&self, p: &mut Printer) { + p.push_str(match self { + Self::Add => "+", + Self::Sub => "-", + Self::Mul => "*", + Self::Div => "/", + }); + } +} + /// An array expression: `(1, "hi", 12cm)`. pub type ExprArray = SpanVec; +impl Pretty for ExprArray { + fn pretty(&self, p: &mut Printer) { + p.push_str("("); + p.join(self, ", ", |item, p| item.v.pretty(p)); + if self.len() == 1 { + p.push_str(","); + } + p.push_str(")"); + } +} + /// A dictionary expression: `(color: #f79143, pattern: dashed)`. pub type ExprDict = Vec; +impl Pretty for ExprDict { + fn pretty(&self, p: &mut Printer) { + p.push_str("("); + if self.is_empty() { + p.push_str(":"); + } else { + p.join(self, ", ", |named, p| named.pretty(p)); + } + p.push_str(")"); + } +} + /// A content expression: `{*Hello* there!}`. pub type ExprContent = Tree; @@ -128,3 +289,65 @@ pub enum Lit { /// A string literal: `"hello!"`. Str(String), } + +impl Pretty for Lit { + fn pretty(&self, p: &mut Printer) { + match self { + Self::Ident(v) => p.push_str(&v), + Self::None => p.push_str("none"), + Self::Bool(v) => write!(p, "{}", v).unwrap(), + Self::Int(v) => write!(p, "{}", v).unwrap(), + Self::Float(v) => write!(p, "{}", v).unwrap(), + Self::Length(v, u) => write!(p, "{}{}", v, u).unwrap(), + Self::Percent(v) => write!(p, "{}%", v).unwrap(), + Self::Color(v) => write!(p, "{}", v).unwrap(), + Self::Str(s) => write!(p, "{:?}", &s).unwrap(), + } + } +} + +#[cfg(test)] +mod tests { + use super::super::tests::test_pretty; + + #[test] + fn test_pretty_print_chaining() { + // All equivalent. + test_pretty("[v [f]]", "[v | f]"); + test_pretty("[v {[f]}]", "[v | f]"); + test_pretty("[v][[f]]", "[v | f]"); + test_pretty("[v | f]", "[v | f]"); + } + + #[test] + fn test_pretty_print_expressions() { + // Unary and binary operations. + test_pretty("{1 +}", "{1}"); + test_pretty("{1 + func(-2)}", "{1 + func(-2)}"); + + // Array. + test_pretty("(-5,)", "(-5,)"); + test_pretty("(1, 2, 3)", "(1, 2, 3)"); + + // Dictionary. + test_pretty("{(:)}", "{(:)}"); + test_pretty("{(percent: 5%)}", "{(percent: 5%)}"); + + // Content expression without unncessary braces. + test_pretty("[v [f], 1]", "[v [f], 1]"); + test_pretty("(func: {[f]})", "(func: [f])"); + } + + #[test] + fn test_pretty_print_literals() { + test_pretty("{none}", "{none}"); + test_pretty("{true}", "{true}"); + test_pretty("{25}", "{25}"); + test_pretty("{2.50}", "{2.5}"); + test_pretty("{1e2}", "{100}"); + test_pretty("{12pt}", "{12pt}"); + test_pretty("{50%}", "{50%}"); + test_pretty("{#fff}", "{#ffffff}"); + test_pretty(r#"{"hi\n"}"#, r#"{"hi\n"}"#); + } +} diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index 9c78fbc39..22f51d826 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -12,5 +12,33 @@ pub use node::*; pub use span::*; pub use token::*; -/// A collection of nodes which form a tree together with their children. +use crate::pretty::{Pretty, Printer}; + +/// The abstract syntax tree. pub type Tree = SpanVec; + +impl Pretty for Tree { + fn pretty(&self, p: &mut Printer) { + for node in self { + node.v.pretty(p); + } + } +} + +#[cfg(test)] +mod tests { + use crate::parse::parse; + use crate::pretty::pretty; + + #[track_caller] + pub fn test_pretty(src: &str, exp: &str) { + let tree = parse(src).output; + let found = pretty(&tree); + if exp != found { + println!("tree: {:#?}", tree); + println!("expected: {}", exp); + println!("found: {}", found); + panic!("test failed"); + } + } +} diff --git a/src/syntax/node.rs b/src/syntax/node.rs index d64d4fed0..91fa72d7c 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -27,15 +27,62 @@ pub enum Node { Expr(Expr), } +impl Pretty for Node { + fn pretty(&self, p: &mut Printer) { + match self { + Self::Text(text) => p.push_str(&text), + Self::Space => p.push_str(" "), + Self::Linebreak => p.push_str(r"\"), + Self::Parbreak => p.push_str("\n\n"), + Self::Strong => p.push_str("*"), + Self::Emph => p.push_str("_"), + Self::Heading(heading) => heading.pretty(p), + Self::Raw(raw) => raw.pretty(p), + Self::Expr(expr) => pretty_expr_node(expr, p), + } + } +} + +/// Pretty print an expression in a node context. +pub fn pretty_expr_node(expr: &Expr, p: &mut Printer) { + match expr { + // Prefer bracket calls over expression blocks with just a single paren + // call. + // + // Example: Transforms "{v()}" => "[v]". + Expr::Call(call) => pretty_bracket_call(call, p, false), + + // Remove unncessary nesting of content and expression blocks. + // + // Example: Transforms "{{Hi}}" => "Hi". + Expr::Content(content) => content.pretty(p), + + _ => { + p.push_str("{"); + expr.pretty(p); + p.push_str("}"); + } + } +} + /// A section heading: `# Introduction`. #[derive(Debug, Clone, PartialEq)] pub struct NodeHeading { - /// The section depth (numer of hashtags minus 1). + /// The section depth (numer of hashtags minus 1, capped at 5). pub level: Spanned, /// The contents of the heading. pub contents: Tree, } +impl Pretty for NodeHeading { + fn pretty(&self, p: &mut Printer) { + for _ in 0 ..= self.level.v { + p.push_str("#"); + } + self.contents.pretty(p); + } +} + /// A raw block with optional syntax highlighting: `` `raw` ``. /// /// Raw blocks start with an arbitrary number of backticks and end with the same @@ -114,3 +161,54 @@ pub struct NodeRaw { /// are inline-level when they contain no newlines. pub inline: 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(" "); + } + // TODO: Technically, we should handle backticks in the lines + // by wrapping with more backticks and possibly adding space + // before the first or after the last line. + p.join(&self.lines, "\n", |line, p| p.push_str(line)); + p.push_str("`"); + } +} + +#[cfg(test)] +mod tests { + use super::super::tests::test_pretty; + + #[test] + fn test_pretty_print_removes_nesting() { + // Even levels of nesting do not matter. + test_pretty("{{Hi}}", "Hi"); + test_pretty("{{{{Hi}}}}", "Hi"); + } + + #[test] + fn test_pretty_print_prefers_bracket_calls() { + // All reduces to a simple bracket call. + test_pretty("{v()}", "[v]"); + test_pretty("[v]", "[v]"); + test_pretty("{[v]}", "[v]"); + test_pretty("{{[v]}}", "[v]"); + } + + #[test] + fn test_pretty_print_nodes() { + // Basic text and markup. + test_pretty(r"*Hi_\", r"*Hi_\"); + + // Whitespace. + test_pretty(" ", " "); + test_pretty("\n\n\n", "\n\n"); + + // Heading and raw. + test_pretty("# Ok", "# Ok"); + test_pretty("``\none\ntwo\n``", "`one\ntwo`"); + test_pretty("`lang one\ntwo`", "`lang one\ntwo`"); + } +}