summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-01-06 01:32:59 +0100
committerLaurenz <laurmaedje@gmail.com>2021-01-06 01:32:59 +0100
commit7b4d4d6002a9c3da8fafd912f3c7b2da617f19c0 (patch)
treee491f5fcf33c1032c63746003ac7bef6c3c5478f
parent2e77b1c836220766398e379ae0157736fb448874 (diff)
Pretty printing 🦋
- Syntax tree and value pretty printing - Better value evaluation (top-level strings and content are evaluated plainly, everything else is pretty printed)
-rw-r--r--src/color.rs2
-rw-r--r--src/eval/mod.rs3
-rw-r--r--src/eval/value.rs147
-rw-r--r--src/lib.rs1
-rw-r--r--src/parse/resolve.rs5
-rw-r--r--src/pretty.rs68
-rw-r--r--src/syntax/expr.rs223
-rw-r--r--src/syntax/mod.rs30
-rw-r--r--src/syntax/node.rs100
9 files changed, 533 insertions, 46 deletions
diff --git a/src/color.rs b/src/color.rs
index d74584ae..11cc5b3b 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 42a5555d..1c6e3d51 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 80c6b820..50741987 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<Value>;
+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<String, Value>;
+pub type ValueDict = BTreeMap<String, Value>;
+
+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, "<function {}>", 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<Value>, 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 15d68803..05cc0569 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 d6c6d8a4..c4afc430 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 00000000..a7482869
--- /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<T>(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<T, I, F>(&mut self, items: I, joiner: &str, mut write_item: F)
+ where
+ I: IntoIterator<Item = T>,
+ 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 cf6611f9..4916f34f 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<ExprArgs>,
}
+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<Argument>;
+impl Pretty for Vec<Argument> {
+ 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<Expr>,
}
+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<Spanned<Expr>>,
}
+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<Spanned<Expr>>,
}
+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<Expr>;
+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<Named>;
+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 9c78fbc3..22f51d82 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<Node>;
+
+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 d64d4fed..91fa72d7 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<u8>,
/// 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`");
+ }
+}