1use std::fmt::Write as _;
6
7use crate::cst::{
8 BlockScalar, ControlKind, Node, OpaqueKind, ScalarPart, ScalarParts, Span, TemplatedDocument,
9};
10
11impl TemplatedDocument<'_> {
12 #[must_use]
14 pub fn dump(&self) -> String {
15 let mut out = String::new();
16 for (index, span) in self.document_spans.iter().enumerate() {
17 let _ = writeln!(out, "document {index} {}", fmt_span(*span));
18 }
19 for node in &self.roots {
20 dump_node(node, self.source, 0, &mut out);
21 }
22 out
23 }
24}
25
26#[expect(
27 clippy::too_many_lines,
28 reason = "keeping the exhaustive node rendering in one match makes dump coverage auditable"
29)]
30fn dump_node(node: &Node, source: &str, depth: usize, out: &mut String) {
31 let pad = " ".repeat(depth);
32 match node {
33 Node::Mapping(entry) => {
34 let shape = if entry.block.is_some() {
35 "block"
36 } else if entry.opens_scope {
37 "open"
38 } else {
39 "closed"
40 };
41 let _ = write!(
42 out,
43 "{pad}entry {} {} indent={} key={}",
44 fmt_span(entry.span),
45 shape,
46 entry.indent,
47 fmt_parts_text(&entry.key, source),
48 );
49 if let Some(value) = &entry.value {
50 let _ = write!(out, " value={}", fmt_parts(value, source));
51 }
52 if let Some(block) = &entry.block {
53 let _ = write!(out, " {}", fmt_block(block));
54 }
55 out.push('\n');
56 for child in &entry.children {
57 dump_node(child, source, depth + 1, out);
58 }
59 }
60 Node::Sequence(item) => {
61 let _ = write!(
62 out,
63 "{pad}item {} indent={}",
64 fmt_span(item.span),
65 item.indent
66 );
67 if let Some(value) = &item.value {
68 let _ = write!(out, " value={}", fmt_parts(value, source));
69 }
70 if let Some(block) = &item.block {
71 let _ = write!(out, " {}", fmt_block(block));
72 }
73 out.push('\n');
74 for child in &item.children {
75 dump_node(child, source, depth + 1, out);
76 }
77 }
78 Node::Control(region) => {
79 let kind = match region.kind {
80 ControlKind::If => "if",
81 ControlKind::With => "with",
82 ControlKind::Range => "range",
83 ControlKind::Define => "define",
84 ControlKind::Block => "block",
85 };
86 let nested = if region.well_nested {
87 ""
88 } else {
89 " ill-nested"
90 };
91 let _ = writeln!(out, "{pad}control {kind} {}{nested}", fmt_span(region.span));
92 for branch in ®ion.branches {
93 let _ = writeln!(
94 out,
95 "{pad} branch {} {}",
96 fmt_span(branch.header),
97 fmt_text(branch.header, source),
98 );
99 for child in &branch.body {
100 dump_node(child, source, depth + 2, out);
101 }
102 }
103 }
104 Node::Output(output) => {
105 let _ = writeln!(
106 out,
107 "{pad}output {} {}",
108 fmt_span(output.span),
109 fmt_text(output.span, source),
110 );
111 }
112 Node::Comment(comment) => {
113 let _ = writeln!(
114 out,
115 "{pad}comment {} {}",
116 fmt_span(comment.span),
117 fmt_parts(&comment.content, source),
118 );
119 }
120 Node::Scalar(scalar) => {
121 let _ = writeln!(
122 out,
123 "{pad}scalar {} indent={} {}",
124 fmt_span(scalar.span),
125 scalar.indent,
126 fmt_parts(&scalar.content, source),
127 );
128 }
129 Node::Opaque(opaque) => {
130 let kind = match opaque.kind {
131 OpaqueKind::TemplateComment => "template-comment",
132 OpaqueKind::Assignment => "assign",
133 OpaqueKind::Break => "break",
134 OpaqueKind::Continue => "continue",
135 OpaqueKind::InlineRegion => "inline-region",
136 OpaqueKind::ActionLineText => "action-line-text",
137 OpaqueKind::ParseError => "parse-error",
138 };
139 let _ = writeln!(
140 out,
141 "{pad}opaque {kind} {} {}",
142 fmt_span(opaque.span),
143 fmt_text(opaque.span, source),
144 );
145 }
146 }
147}
148
149fn fmt_span(span: Span) -> String {
150 format!("[{}..{})", span.start, span.end)
151}
152
153fn fmt_text(span: Span, source: &str) -> String {
154 let text = source.get(span.start..span.end).unwrap_or("");
155 format!("{text:?}")
156}
157
158fn fmt_parts_text(parts: &ScalarParts, source: &str) -> String {
160 fmt_text(parts.span, source)
161}
162
163fn fmt_parts(parts: &ScalarParts, source: &str) -> String {
166 let mut rendered = String::new();
167 for part in &parts.parts {
168 match part {
169 ScalarPart::Text(span) => {
170 rendered.push_str(source.get(span.start..span.end).unwrap_or(""));
171 }
172 ScalarPart::Hole(span) => {
173 rendered.push('⟦');
174 rendered.push_str(source.get(span.start..span.end).unwrap_or(""));
175 rendered.push('⟧');
176 }
177 }
178 }
179 format!("{rendered:?}")
180}
181
182fn fmt_block(block: &BlockScalar) -> String {
183 format!(
184 "block-header={} body={} suppressed-holes={}",
185 fmt_span(block.header),
186 fmt_span(block.body),
187 block.holes.len()
188 )
189}