zerodds_lang_render/
writer.rs1use alloc::string::{String, ToString};
10
11#[derive(Debug, Clone)]
13pub struct Writer {
14 out: String,
15 depth: usize,
16 unit: &'static str,
17 line_started: bool,
19}
20
21impl Default for Writer {
22 fn default() -> Self {
23 Self::new(" ")
24 }
25}
26
27impl Writer {
28 #[must_use]
30 pub fn new(unit: &'static str) -> Self {
31 Self {
32 out: String::new(),
33 depth: 0,
34 unit,
35 line_started: false,
36 }
37 }
38
39 #[must_use]
42 pub fn spaces4() -> Self {
43 Self::new(" ")
44 }
45
46 pub fn indent(&mut self) {
48 self.depth = self.depth.saturating_add(1);
49 }
50
51 pub fn dedent(&mut self) {
53 self.depth = self.depth.saturating_sub(1);
54 }
55
56 #[must_use]
58 pub const fn depth(&self) -> usize {
59 self.depth
60 }
61
62 pub fn push(&mut self, text: &str) {
64 if !self.line_started && !text.is_empty() {
65 for _ in 0..self.depth {
66 self.out.push_str(self.unit);
67 }
68 self.line_started = true;
69 }
70 self.out.push_str(text);
71 }
72
73 pub fn line(&mut self, text: &str) {
75 self.push(text);
76 self.newline();
77 }
78
79 pub fn newline(&mut self) {
85 self.out.push('\n');
86 self.line_started = false;
87 }
88
89 pub fn finish_line(&mut self) {
94 if self.line_started {
95 self.newline();
96 }
97 }
98
99 pub fn blank_line(&mut self) {
104 self.finish_line();
105 if self.out.is_empty() || self.out.ends_with("\n\n") {
106 return;
107 }
108 self.out.push('\n');
109 }
110
111 #[must_use]
113 pub fn is_empty(&self) -> bool {
114 self.out.is_empty()
115 }
116
117 #[must_use]
119 pub fn as_str(&self) -> &str {
120 &self.out
121 }
122
123 #[must_use]
125 pub fn finish(mut self) -> String {
126 self.finish_line();
127 self.out
128 }
129}
130
131pub fn verbatim_lines(writer: &mut Writer, text: &str) {
139 for line in text.lines() {
140 writer.line(line);
141 }
142}
143
144pub fn comment_lines(writer: &mut Writer, marker: &str, lines: &[String]) {
150 for line in lines {
151 if line.is_empty() {
152 writer.line(marker.trim_end());
153 } else {
154 let mut text = marker.to_string();
155 text.push_str(line);
156 writer.line(&text);
157 }
158 }
159}
160
161#[must_use]
167pub fn escape_string(text: &str) -> String {
168 let mut out = String::new();
169 for ch in text.chars() {
170 match ch {
171 '"' => out.push_str("\\\""),
172 '\\' => out.push_str("\\\\"),
173 '\n' => out.push_str("\\n"),
174 '\r' => out.push_str("\\r"),
175 '\t' => out.push_str("\\t"),
176 '\0' => out.push_str("\\0"),
177 other => out.push(other),
178 }
179 }
180 out
181}
182
183#[must_use]
185pub fn escape_char(ch: char) -> String {
186 match ch {
187 '\'' => "\\'".to_string(),
188 '\\' => "\\\\".to_string(),
189 '\n' => "\\n".to_string(),
190 '\r' => "\\r".to_string(),
191 '\t' => "\\t".to_string(),
192 '\0' => "\\0".to_string(),
193 other => other.to_string(),
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
200 use super::*;
201
202 #[test]
203 fn indentation_applies_once_per_line_not_per_write() {
204 let mut w = Writer::spaces4();
205 w.indent();
206 w.push("a");
207 w.push("b");
208 w.newline();
209 assert_eq!(w.finish(), " ab\n");
210 }
211
212 #[test]
213 fn a_blank_line_carries_no_trailing_whitespace() {
214 let mut w = Writer::spaces4();
217 w.indent();
218 w.line("a");
219 w.newline();
220 w.line("b");
221 assert_eq!(w.finish(), " a\n\n b\n");
222 }
223
224 #[test]
225 fn a_blank_line_does_not_double_up() {
226 let mut w = Writer::spaces4();
227 w.line("a");
228 w.blank_line();
229 w.blank_line();
230 w.line("b");
231 assert_eq!(w.finish(), "a\n\nb\n");
232 }
233
234 #[test]
235 fn a_blank_line_at_the_start_writes_nothing() {
236 let mut w = Writer::spaces4();
237 w.blank_line();
238 w.line("a");
239 assert_eq!(w.finish(), "a\n");
240 }
241
242 #[test]
243 fn dedent_below_zero_stays_at_zero() {
244 let mut w = Writer::spaces4();
247 w.dedent();
248 w.dedent();
249 assert_eq!(w.depth(), 0);
250 w.line("a");
251 assert_eq!(w.finish(), "a\n");
252 }
253
254 #[test]
255 fn string_escapes_cover_the_ones_a_generated_literal_meets() {
256 assert_eq!(escape_string("a\"b"), "a\\\"b");
257 assert_eq!(escape_string("a\\b"), "a\\\\b");
258 assert_eq!(escape_string("a\nb"), "a\\nb");
259 assert_eq!(escape_string("plain"), "plain");
260 }
261
262 #[test]
263 fn a_comment_block_marks_every_line() {
264 let mut w = Writer::spaces4();
265 comment_lines(
266 &mut w,
267 "/// ",
268 &["first".to_string(), String::new(), "third".to_string()],
269 );
270 assert_eq!(w.finish(), "/// first\n///\n/// third\n");
271 }
272}