sphinx_ultra/doctree/
pformat.rs1use 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 text.split('\n') {
35 out.push_str(&indent);
36 out.push_str(line);
37 out.push('\n');
38 }
39 }
40 return;
41 }
42
43 let mut attrs: Vec<(&str, String)> = Vec::new();
44 push_list_attr(&mut attrs, "backrefs", &node.attrs.backrefs);
45 push_list_attr(&mut attrs, "classes", &node.attrs.classes);
46 push_list_attr(&mut attrs, "dupnames", &node.attrs.dupnames);
47 push_list_attr(&mut attrs, "ids", &node.attrs.ids);
48 push_list_attr(&mut attrs, "names", &node.attrs.names);
49 for (key, value) in &node.attrs.extra {
50 let rendered = match value {
51 AttrValue::Int(i) => i.to_string(),
52 AttrValue::Str(s) => s.clone(),
53 };
54 attrs.push((key, rendered));
55 }
56 attrs.sort_by(|a, b| a.0.cmp(b.0));
57
58 out.push_str(&indent);
59 out.push('<');
60 out.push_str(node.kind);
61 for (name, value) in &attrs {
62 out.push(' ');
63 out.push_str(name);
64 out.push_str("=\"");
65 out.push_str(value);
66 out.push('"');
67 }
68 out.push_str(">\n");
69
70 for child in &node.children {
71 write_node(child, depth + 1, out);
72 }
73}
74
75pub fn pformat(node: &Node) -> String {
76 let mut out = String::new();
77 write_node(node, 0, &mut out);
78 out
79}
80
81#[cfg(test)]
82mod tests {
83 use crate::doctree::{kinds, AttrValue, Node, Span};
84
85 #[test]
86 fn pformat_section_with_attrs() {
87 let mut doc = Node::elem(kinds::DOCUMENT, Span::ZERO);
88 doc.set("source", AttrValue::Str("<snippet>".into()));
89 let mut sec = Node::elem(kinds::SECTION, Span::ZERO);
90 sec.attrs.ids.push("title".into());
91 sec.attrs.names.push("title".into());
92 let mut title = Node::elem(kinds::TITLE, Span::ZERO);
93 title.children.push(Node::text_node("Title", Span::ZERO));
94 sec.children.push(title);
95 doc.children.push(sec);
96 assert_eq!(
97 doc.pformat(),
98 "<document source=\"<snippet>\">\n <section ids=\"title\" names=\"title\">\n <title>\n Title\n"
99 );
100 }
101
102 #[test]
103 fn pformat_serial_escapes_spaces_in_list_values() {
104 let mut sec = Node::elem(kinds::SECTION, Span::ZERO);
105 sec.attrs.ids.push("my-section-title".into());
106 sec.attrs.dupnames.push("my section title!".into());
107 assert_eq!(
108 sec.pformat(),
109 "<section dupnames=\"my\\ section\\ title!\" ids=\"my-section-title\">\n"
110 );
111 }
112
113 #[test]
114 fn pformat_serial_escapes_backslashes_before_spaces() {
115 let mut t = Node::elem(kinds::TARGET, Span::ZERO);
117 t.attrs.names.push("a\\b".into());
118 assert_eq!(t.pformat(), "<target names=\"a\\\\b\">\n");
119 }
120
121 #[test]
122 fn pformat_does_not_xml_escape() {
123 let mut t = Node::elem(kinds::TARGET, Span::ZERO);
125 t.attrs.names.push("a \"quote\" <b> & c".into());
126 t.set("refuri", AttrValue::Str("https://x/?q=1&r=2".into()));
127 assert_eq!(
128 t.pformat(),
129 "<target names=\"a\\ \"quote\"\\ <b>\\ &\\ c\" refuri=\"https://x/?q=1&r=2\">\n"
130 );
131 let mut p = Node::elem(kinds::PARAGRAPH, Span::ZERO);
132 p.children
133 .push(Node::text_node("x < y & z > w", Span::ZERO));
134 assert_eq!(p.pformat(), "<paragraph>\n x < y & z > w\n");
135 }
136
137 #[test]
138 fn pformat_system_message_scalar_attrs_sorted() {
139 let mut m = Node::elem(kinds::SYSTEM_MESSAGE, Span::ZERO);
140 m.set("level", AttrValue::Int(2));
141 m.set("line", AttrValue::Int(3));
142 m.set("source", AttrValue::Str("<snippet>".into()));
143 m.set("type", AttrValue::Str("WARNING".into()));
144 let mut p = Node::elem(kinds::PARAGRAPH, Span::ZERO);
145 p.children
146 .push(Node::text_node("Title underline too short.", Span::ZERO));
147 m.children.push(p);
148 assert_eq!(
149 m.pformat(),
150 "<system_message level=\"2\" line=\"3\" source=\"<snippet>\" type=\"WARNING\">\n <paragraph>\n Title underline too short.\n"
151 );
152 }
153
154 #[test]
155 fn pformat_list_and_scalar_attrs_interleave_alphabetically() {
156 let mut m = Node::elem(kinds::SYSTEM_MESSAGE, Span::ZERO);
158 m.attrs.backrefs.push("id1".into());
159 m.set("level", AttrValue::Int(1));
160 m.set("line", AttrValue::Int(7));
161 m.set("source", AttrValue::Str("<snippet>".into()));
162 m.set("type", AttrValue::Str("INFO".into()));
163 assert_eq!(
164 m.pformat(),
165 "<system_message backrefs=\"id1\" level=\"1\" line=\"7\" source=\"<snippet>\" type=\"INFO\">\n"
166 );
167 }
168
169 #[test]
170 fn pformat_multiline_text_indents_each_line() {
171 let mut p = Node::elem(kinds::PARAGRAPH, Span::ZERO);
172 p.children.push(Node::text_node("Title\n===", Span::ZERO));
173 assert_eq!(p.pformat(), "<paragraph>\n Title\n ===\n");
174 }
175
176 #[test]
177 fn pformat_xml_space_preserve() {
178 let mut lb = Node::elem(kinds::LITERAL_BLOCK, Span::ZERO);
179 lb.set("xml:space", AttrValue::Str("preserve".into()));
180 lb.children.push(Node::text_node("code here", Span::ZERO));
181 assert_eq!(
182 lb.pformat(),
183 "<literal_block xml:space=\"preserve\">\n code here\n"
184 );
185 }
186
187 #[test]
188 fn pformat_empty_element_prints_bare_tag() {
189 let li = Node::elem(kinds::LIST_ITEM, Span::ZERO);
191 assert_eq!(li.pformat(), "<list_item>\n");
192 let t = Node::elem(kinds::TRANSITION, Span::ZERO);
193 assert_eq!(t.pformat(), "<transition>\n");
194 }
195}