1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use crate::ast::*;
use crate::printer::{inline::ToDocInline, ToDoc};
use pretty::{Arena, DocAllocator, DocBuilder};
use std::rc::Rc;
impl<'a> ToDoc<'a> for Vec<Block> {
fn to_doc(
&self,
config: Rc<crate::printer::config::Config>,
arena: &'a Arena<'a>,
) -> DocBuilder<'a, Arena<'a>, ()> {
let refs: Vec<_> = self.iter().collect();
refs.to_doc(config, arena)
}
}
impl<'a> ToDoc<'a> for Vec<&Block> {
fn to_doc(
&self,
config: Rc<crate::printer::config::Config>,
arena: &'a Arena<'a>,
) -> DocBuilder<'a, Arena<'a>, ()> {
let mut acc = arena.nil();
for (i, block) in self.iter().enumerate() {
if i > 0 {
// first block should not have an empty line before it
acc = acc.append(arena.hardline());
if matches!(block, Block::List(_)) {
if config.empty_line_before_list {
// empty line before list block
acc = acc.append(arena.hardline());
}
} else {
acc = acc.append(arena.hardline());
}
}
acc = acc.append(block.to_doc(config.clone(), arena))
}
acc
}
}
/// Block-level nodes
impl<'a> ToDoc<'a> for Block {
fn to_doc(
&self,
config: Rc<crate::printer::config::Config>,
arena: &'a Arena<'a>,
) -> DocBuilder<'a, Arena<'a>, ()> {
match self {
Block::Paragraph(inlines) => inlines.to_doc_inline(true, arena),
Block::Heading(v) => v.to_doc(config, arena),
Block::ThematicBreak => arena.text("---"),
Block::BlockQuote(inner) => {
crate::printer::blockquote::blockquote_to_doc(config, arena, inner)
}
Block::List(v) => v.to_doc(config, arena),
Block::CodeBlock(CodeBlock { kind, literal }) => {
match kind {
CodeBlockKind::Fenced { info } => {
let info = info.as_deref().unwrap_or("");
// Use hardline() between lines so nest() indentation applies correctly
// when the code block is inside a list or other nested structure.
// We use split('\n') instead of lines() to preserve trailing newlines.
let mut doc = arena.text(format!("```{info}"));
// Handle code block content.
// For non-empty content, we use split('\n') instead of lines() to preserve
// trailing newlines. Each line gets a hardline() before it so that nest()
// indentation applies correctly when inside lists or other nested structures.
// IMPORTANT: For blank lines (empty or whitespace-only), we only add
// hardline() without any text, so that nest() doesn't compound whitespace
// on repeated format passes. This ensures idempotent formatting.
if !literal.is_empty() {
let lines: Vec<&str> = literal.split('\n').collect();
for line in lines {
doc = doc.append(arena.hardline());
// Only add text for lines with non-whitespace content.
// This prevents whitespace from compounding on each format pass.
let trimmed = line.trim_start();
if !trimmed.is_empty() {
doc = doc.append(arena.text(line.to_string()));
}
}
}
// Closing fence must be on its own line
doc.append(arena.hardline()).append(arena.text("```"))
}
CodeBlockKind::Indented => {
// Each line indented with 4 spaces
let indented = literal
.lines()
.map(|l| format!(" {l}"))
.collect::<Vec<_>>()
.join("\n");
arena.text(indented)
}
}
}
Block::HtmlBlock(html) => arena.text(html.clone()),
Block::Definition(def) => arena
.text("[")
.append(def.label.to_doc_inline(true, arena))
.append(arena.text("]: "))
.append(arena.text(format!(
"{}{}",
def.destination,
def.title
.as_ref()
.map(|t| format!(" \"{t}\""))
.unwrap_or_default()
))),
Block::Empty => arena.nil(),
Block::Table(v) => v.to_doc(config, arena),
Block::FootnoteDefinition(def) => arena
.text(format!("[^{}]: ", def.label))
.append(def.blocks.to_doc(config, arena)),
Block::GitHubAlert(alert) => {
crate::printer::github_alert::github_alert_to_doc(alert, config, arena)
}
}
}
}