1use super::{kinds, AttrValue, Node};
14
15fn serial_escape(s: &str) -> String {
16 s.replace('\\', "\\\\").replace(' ', "\\ ")
17}
18
19fn push_list_attr(out: &mut Vec<(&str, String)>, name: &'static str, values: &[String]) {
20 if !values.is_empty() {
21 let joined = values
22 .iter()
23 .map(|v| serial_escape(v))
24 .collect::<Vec<_>>()
25 .join(" ");
26 out.push((name, joined));
27 }
28}
29
30fn write_node(node: &Node, depth: usize, out: &mut String) {
31 let indent = " ".repeat(depth);
32 if node.kind == kinds::TEXT {
33 if let Some(text) = &node.text {
34 for line in crate::utils::py_splitlines(text) {
42 out.push_str(&indent);
43 out.push_str(line);
44 out.push('\n');
45 }
46 }
47 return;
48 }
49
50 let mut attrs: Vec<(&str, String)> = Vec::new();
51 push_list_attr(&mut attrs, "backrefs", &node.attrs.backrefs);
52 push_list_attr(&mut attrs, "classes", &node.attrs.classes);
53 push_list_attr(&mut attrs, "dupnames", &node.attrs.dupnames);
54 push_list_attr(&mut attrs, "ids", &node.attrs.ids);
55 push_list_attr(&mut attrs, "names", &node.attrs.names);
56 for (key, value) in &node.attrs.extra {
57 let rendered = match value {
58 AttrValue::Int(i) => i.to_string(),
59 AttrValue::Str(s) => s.clone(),
60 AttrValue::List(items) => items
67 .iter()
68 .map(|v| serial_escape(v))
69 .collect::<Vec<_>>()
70 .join(" "),
71 };
72 attrs.push((key, rendered));
73 }
74 attrs.sort_by(|a, b| a.0.cmp(b.0));
75
76 out.push_str(&indent);
77 out.push('<');
78 out.push_str(node.kind);
79 for (name, value) in &attrs {
80 out.push(' ');
81 out.push_str(name);
82 out.push_str("=\"");
83 out.push_str(value);
84 out.push('"');
85 }
86 out.push_str(">\n");
87
88 for child in &node.children {
89 write_node(child, depth + 1, out);
90 }
91}
92
93pub fn pformat(node: &Node) -> String {
94 let mut out = String::new();
95 write_node(node, 0, &mut out);
96 out
97}
98
99#[cfg(test)]
100mod tests {
101 use crate::doctree::{kinds, AttrValue, Node, Span};
102
103 #[test]
104 fn pformat_section_with_attrs() {
105 let mut doc = Node::elem(kinds::DOCUMENT, Span::ZERO);
106 doc.set("source", AttrValue::Str("<snippet>".into()));
107 let mut sec = Node::elem(kinds::SECTION, Span::ZERO);
108 sec.attrs.ids.push("title".into());
109 sec.attrs.names.push("title".into());
110 let mut title = Node::elem(kinds::TITLE, Span::ZERO);
111 title.children.push(Node::text_node("Title", Span::ZERO));
112 sec.children.push(title);
113 doc.children.push(sec);
114 assert_eq!(
115 doc.pformat(),
116 "<document source=\"<snippet>\">\n <section ids=\"title\" names=\"title\">\n <title>\n Title\n"
117 );
118 }
119
120 #[test]
121 fn pformat_serial_escapes_spaces_in_list_values() {
122 let mut sec = Node::elem(kinds::SECTION, Span::ZERO);
123 sec.attrs.ids.push("my-section-title".into());
124 sec.attrs.dupnames.push("my section title!".into());
125 assert_eq!(
126 sec.pformat(),
127 "<section dupnames=\"my\\ section\\ title!\" ids=\"my-section-title\">\n"
128 );
129 }
130
131 #[test]
132 fn pformat_serial_escapes_backslashes_before_spaces() {
133 let mut t = Node::elem(kinds::TARGET, Span::ZERO);
135 t.attrs.names.push("a\\b".into());
136 assert_eq!(t.pformat(), "<target names=\"a\\\\b\">\n");
137 }
138
139 #[test]
140 fn pformat_does_not_xml_escape() {
141 let mut t = Node::elem(kinds::TARGET, Span::ZERO);
143 t.attrs.names.push("a \"quote\" <b> & c".into());
144 t.set("refuri", AttrValue::Str("https://x/?q=1&r=2".into()));
145 assert_eq!(
146 t.pformat(),
147 "<target names=\"a\\ \"quote\"\\ <b>\\ &\\ c\" refuri=\"https://x/?q=1&r=2\">\n"
148 );
149 let mut p = Node::elem(kinds::PARAGRAPH, Span::ZERO);
150 p.children
151 .push(Node::text_node("x < y & z > w", Span::ZERO));
152 assert_eq!(p.pformat(), "<paragraph>\n x < y & z > w\n");
153 }
154
155 #[test]
156 fn pformat_system_message_scalar_attrs_sorted() {
157 let mut m = Node::elem(kinds::SYSTEM_MESSAGE, Span::ZERO);
158 m.set("level", AttrValue::Int(2));
159 m.set("line", AttrValue::Int(3));
160 m.set("source", AttrValue::Str("<snippet>".into()));
161 m.set("type", AttrValue::Str("WARNING".into()));
162 let mut p = Node::elem(kinds::PARAGRAPH, Span::ZERO);
163 p.children
164 .push(Node::text_node("Title underline too short.", Span::ZERO));
165 m.children.push(p);
166 assert_eq!(
167 m.pformat(),
168 "<system_message level=\"2\" line=\"3\" source=\"<snippet>\" type=\"WARNING\">\n <paragraph>\n Title underline too short.\n"
169 );
170 }
171
172 #[test]
173 fn pformat_list_and_scalar_attrs_interleave_alphabetically() {
174 let mut m = Node::elem(kinds::SYSTEM_MESSAGE, Span::ZERO);
176 m.attrs.backrefs.push("id1".into());
177 m.set("level", AttrValue::Int(1));
178 m.set("line", AttrValue::Int(7));
179 m.set("source", AttrValue::Str("<snippet>".into()));
180 m.set("type", AttrValue::Str("INFO".into()));
181 assert_eq!(
182 m.pformat(),
183 "<system_message backrefs=\"id1\" level=\"1\" line=\"7\" source=\"<snippet>\" type=\"INFO\">\n"
184 );
185 }
186
187 #[test]
188 fn pformat_multiline_text_indents_each_line() {
189 let mut p = Node::elem(kinds::PARAGRAPH, Span::ZERO);
190 p.children.push(Node::text_node("Title\n===", Span::ZERO));
191 assert_eq!(p.pformat(), "<paragraph>\n Title\n ===\n");
192 }
193
194 #[test]
195 fn pformat_xml_space_preserve() {
196 let mut lb = Node::elem(kinds::LITERAL_BLOCK, Span::ZERO);
197 lb.set("xml:space", AttrValue::Str("preserve".into()));
198 lb.children.push(Node::text_node("code here", Span::ZERO));
199 assert_eq!(
200 lb.pformat(),
201 "<literal_block xml:space=\"preserve\">\n code here\n"
202 );
203 }
204
205 #[test]
206 fn pformat_list_attr_joins_serial_escaped_items() {
207 let mut t = Node::elem(kinds::TOCTREE, Span::ZERO);
210 t.set(
211 "entries",
212 AttrValue::List(vec!["(None, 'a')".into(), "('T x', 'b')".into()]),
213 );
214 t.set("includefiles", AttrValue::List(vec![]));
215 assert_eq!(
216 t.pformat(),
217 "<toctree entries=\"(None,\\ 'a') ('T\\ x',\\ 'b')\" includefiles=\"\">\n"
218 );
219 }
220
221 #[test]
222 fn pformat_empty_element_prints_bare_tag() {
223 let li = Node::elem(kinds::LIST_ITEM, Span::ZERO);
225 assert_eq!(li.pformat(), "<list_item>\n");
226 let t = Node::elem(kinds::TRANSITION, Span::ZERO);
227 assert_eq!(t.pformat(), "<transition>\n");
228 }
229 #[test]
243 fn pformat_text_uses_python_splitlines_and_skips_empty_text() {
244 let mut lb = Node::elem(kinds::LITERAL_BLOCK, Span::ZERO);
245 lb.set("xml:space", AttrValue::Str("preserve".into()));
246 lb.children.push(Node::text_node("a", Span::ZERO));
247 lb.children.push(Node::text_node("", Span::ZERO));
248 lb.children.push(Node::text_node("p1\u{c}p2", Span::ZERO));
249 assert_eq!(
250 lb.pformat(),
251 "<literal_block xml:space=\"preserve\">\n a\n p1\n p2\n"
252 );
253
254 let mut vt = Node::elem(kinds::LITERAL_BLOCK, Span::ZERO);
255 vt.set("xml:space", AttrValue::Str("preserve".into()));
256 vt.children
257 .push(Node::text_node("a\u{b}b\u{85}c d", Span::ZERO));
258 assert_eq!(
259 vt.pformat(),
260 "<literal_block xml:space=\"preserve\">\n a\n b\n c d\n"
261 );
262
263 let mut p = Node::elem(kinds::PARAGRAPH, Span::ZERO);
264 p.children.push(Node::text_node("x y", Span::ZERO));
265 p.children.push(Node::text_node("t\rs", Span::ZERO));
266 p.children.push(Node::text_node("k\u{1c}j", Span::ZERO));
267 assert_eq!(
268 p.pformat(),
269 "<paragraph>\n x y\n t\n s\n k\n j\n"
270 );
271 }
272
273 #[test]
279 fn pformat_text_trailing_newline_rules() {
280 let mut lb = Node::elem(kinds::LITERAL_BLOCK, Span::ZERO);
281 lb.set("xml:space", AttrValue::Str("preserve".into()));
282 lb.children.push(Node::text_node("one\ntwo\n", Span::ZERO));
283 lb.children.push(Node::text_node("\n", Span::ZERO));
284 assert_eq!(
285 lb.pformat(),
286 "<literal_block xml:space=\"preserve\">\n one\n two\n \n"
287 );
288 }
289}