Skip to main content

oak_pretty_print/document/
printer.rs

1use crate::document::Document;
2use alloc::{borrow::Cow, string::String};
3
4/// Indent style
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[cfg_attr(feature = "serde", serde(tag = "type", content = "value"))]
8pub enum IndentStyle {
9    /// Use spaces
10    Spaces(u8),
11    /// Use tabs
12    Tabs,
13}
14
15impl Default for IndentStyle {
16    fn default() -> Self {
17        IndentStyle::Spaces(4)
18    }
19}
20
21/// Line ending
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
25pub enum LineEnding {
26    /// Unix style (\n)
27    Unix,
28    /// Windows style (\r\n)
29    Windows,
30    /// Auto detect
31    Auto,
32}
33
34impl Default for LineEnding {
35    fn default() -> Self {
36        LineEnding::Auto
37    }
38}
39
40/// Printer configuration
41#[derive(Debug, Clone)]
42pub struct PrinterConfig {
43    /// Indent style
44    pub indent_style: IndentStyle,
45    /// Indent text (cached single-level indent string)
46    pub indent_text: Cow<'static, str>,
47    /// Line ending
48    pub line_ending: LineEnding,
49    /// Maximum line length
50    pub max_width: usize,
51    /// Whether to insert a final newline at the end of the file
52    pub insert_final_newline: bool,
53    /// Whether to trim trailing whitespace
54    pub trim_trailing_whitespace: bool,
55    /// Indent size (used for column calculation)
56    pub indent_size: usize,
57}
58
59impl Default for PrinterConfig {
60    fn default() -> Self {
61        let indent_style = IndentStyle::default();
62        let (indent_text, indent_size) = match indent_style {
63            IndentStyle::Spaces(count) => (" ".repeat(count as usize).into(), count as usize),
64            IndentStyle::Tabs => ("\t".into(), 4),
65        };
66
67        Self { indent_style, indent_text, line_ending: LineEnding::default(), max_width: 100, insert_final_newline: true, trim_trailing_whitespace: true, indent_size }
68    }
69}
70
71impl PrinterConfig {
72    /// Creates a new default configuration
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Gets the line ending string
78    pub fn line_ending_string(&self) -> &'static str {
79        match self.line_ending {
80            LineEnding::Unix => "\n",
81            LineEnding::Windows => "\r\n",
82            LineEnding::Auto => {
83                #[cfg(windows)]
84                return "\r\n";
85                #[cfg(not(windows))]
86                return "\n";
87            }
88        }
89    }
90
91    /// Sets the indent style
92    pub fn with_indent_style(mut self, style: IndentStyle) -> Self {
93        self.indent_style = style;
94        let (indent_text, indent_size) = match style {
95            IndentStyle::Spaces(count) => (" ".repeat(count as usize).into(), count as usize),
96            IndentStyle::Tabs => ("\t".into(), 4),
97        };
98        self.indent_text = indent_text;
99        self.indent_size = indent_size;
100        self
101    }
102
103    /// Sets the line ending
104    pub fn with_line_ending(mut self, ending: LineEnding) -> Self {
105        self.line_ending = ending;
106        self
107    }
108
109    /// Sets the maximum line length
110    pub fn with_max_width(mut self, length: usize) -> Self {
111        self.max_width = length;
112        self
113    }
114}
115
116/// Responsible for rendering a Document into a string
117pub struct Printer {
118    config: PrinterConfig,
119    output: String,
120    indent_level: usize,
121    column: usize,
122}
123
124impl Printer {
125    /// Creates a new printer with the given configuration
126    pub fn new(config: PrinterConfig) -> Self {
127        Self { config, output: String::new(), indent_level: 0, column: 0 }
128    }
129
130    /// Prints the document to a string
131    pub fn print(mut self, doc: &Document<'_>) -> String {
132        self.render(doc, false);
133        self.finalize();
134        self.output
135    }
136
137    fn finalize(&mut self) {
138        if self.config.trim_trailing_whitespace {
139            self.output = self.output.trim_end_matches([' ', '\t']).to_string()
140        }
141        if self.config.insert_final_newline && !self.output.is_empty() && !self.output.ends_with('\n') {
142            self.output.push_str(self.config.line_ending_string())
143        }
144    }
145
146    fn render(&mut self, doc: &Document<'_>, is_broken: bool) {
147        match doc {
148            Document::Nil => {}
149            Document::Text(s) => {
150                self.output.push_str(s);
151                self.column += s.len()
152            }
153            Document::Concat(docs) => {
154                for d in docs {
155                    self.render(d, is_broken)
156                }
157            }
158            Document::Group(d) => {
159                let should_break = self.will_break(d);
160                self.render(d, should_break)
161            }
162            Document::Indent(d) => {
163                self.indent_level += 1;
164                self.render(d, is_broken);
165                self.indent_level -= 1
166            }
167            Document::Line => {
168                if is_broken {
169                    self.newline()
170                }
171                else {
172                    self.output.push(' ');
173                    self.column += 1
174                }
175            }
176            Document::SoftLine => {
177                if is_broken {
178                    self.newline()
179                }
180            }
181            Document::SoftLineSpace => {
182                if is_broken {
183                    self.newline()
184                }
185                else {
186                    self.output.push(' ');
187                    self.column += 1
188                }
189            }
190            Document::HardLine => self.newline(),
191        }
192    }
193
194    fn newline(&mut self) {
195        if self.config.trim_trailing_whitespace {
196            while self.output.ends_with(' ') || self.output.ends_with('\t') {
197                let _ = self.output.pop();
198            }
199        }
200        self.output.push_str(self.config.line_ending_string());
201        self.write_indent();
202        self.column = self.indent_level * self.config.indent_size
203    }
204
205    fn write_indent(&mut self) {
206        for _ in 0..self.indent_level {
207            self.output.push_str(&self.config.indent_text)
208        }
209    }
210
211    /// Simple width prediction logic
212    fn will_break(&self, doc: &Document<'_>) -> bool {
213        let mut width = self.column;
214        self.check_width(doc, &mut width)
215    }
216
217    fn check_width(&self, doc: &Document<'_>, width: &mut usize) -> bool {
218        if *width > self.config.max_width {
219            return true;
220        }
221
222        match doc {
223            Document::Nil => false,
224            Document::Text(s) => {
225                *width += s.len();
226                *width > self.config.max_width
227            }
228            Document::Concat(docs) => {
229                for d in docs {
230                    if self.check_width(d, width) {
231                        return true;
232                    }
233                }
234                false
235            }
236            Document::Group(d) => self.check_width(d, width),
237            Document::Indent(d) => self.check_width(d, width),
238            Document::Line => {
239                *width += 1;
240                *width > self.config.max_width
241            }
242            Document::SoftLine => false,
243            Document::SoftLineSpace => {
244                *width += 1;
245                *width > self.config.max_width
246            }
247            Document::HardLine => true,
248        }
249    }
250}