oak_pretty_print/document/
printer.rs1use crate::document::Document;
2use alloc::{borrow::Cow, string::String};
3
4#[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 Spaces(u8),
11 Tabs,
13}
14
15impl Default for IndentStyle {
16 fn default() -> Self {
17 IndentStyle::Spaces(4)
18 }
19}
20
21#[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,
28 Windows,
30 Auto,
32}
33
34impl Default for LineEnding {
35 fn default() -> Self {
36 LineEnding::Auto
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct PrinterConfig {
43 pub indent_style: IndentStyle,
45 pub indent_text: Cow<'static, str>,
47 pub line_ending: LineEnding,
49 pub max_width: usize,
51 pub insert_final_newline: bool,
53 pub trim_trailing_whitespace: bool,
55 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 pub fn new() -> Self {
74 Self::default()
75 }
76
77 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 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 pub fn with_line_ending(mut self, ending: LineEnding) -> Self {
105 self.line_ending = ending;
106 self
107 }
108
109 pub fn with_max_width(mut self, length: usize) -> Self {
111 self.max_width = length;
112 self
113 }
114}
115
116pub struct Printer {
118 config: PrinterConfig,
119 output: String,
120 indent_level: usize,
121 column: usize,
122}
123
124impl Printer {
125 pub fn new(config: PrinterConfig) -> Self {
127 Self { config, output: String::new(), indent_level: 0, column: 0 }
128 }
129
130 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 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}