tiny_pretty/
print.rs

1use crate::{
2    options::{LineBreak, PrintOptions},
3    Doc, IndentKind,
4};
5
6#[derive(Clone, Copy)]
7enum Mode {
8    Flat,
9    Break,
10}
11
12type Action<'a> = (usize, Mode, &'a Doc<'a>);
13
14/// Pretty print a doc.
15///
16/// ## Panics
17///
18/// Panics if `options.tab_size` is `0`.
19pub fn print(doc: &Doc, options: &PrintOptions) -> String {
20    assert!(options.tab_size > 0);
21
22    let mut printer = Printer::new(options);
23    let mut out = String::with_capacity(1024);
24    printer.print_to((0, Mode::Break, doc), &mut out);
25    out
26}
27
28struct Printer<'a> {
29    options: &'a PrintOptions,
30    cols: usize,
31}
32
33impl<'a> Printer<'a> {
34    fn new(options: &'a PrintOptions) -> Self {
35        Self { options, cols: 0 }
36    }
37
38    fn print_to(&mut self, init_action: Action<'a>, out: &mut String) -> bool {
39        let line_break = match self.options.line_break {
40            LineBreak::Lf => "\n",
41            LineBreak::Crlf => "\r\n",
42        };
43
44        let mut actions = Vec::with_capacity(128);
45        actions.push(init_action);
46
47        let mut fits = true;
48
49        while let Some((indent, mode, doc)) = actions.pop() {
50            match doc {
51                Doc::Nil => {}
52                Doc::Alt(doc_flat, doc_break) => match mode {
53                    Mode::Flat => actions.push((indent, mode, doc_flat)),
54                    Mode::Break => actions.push((indent, mode, doc_break)),
55                },
56                Doc::Union(attempt, alternate) => {
57                    let original_cols = self.cols;
58
59                    let mut buf = String::new();
60                    if self.print_to((indent, mode, attempt), &mut buf) {
61                        // SAFETY: Both are `String`s.
62                        unsafe {
63                            out.as_mut_vec().append(buf.as_mut_vec());
64                        }
65                    } else {
66                        self.cols = original_cols;
67                        actions.push((indent, mode, alternate));
68                    }
69                }
70                Doc::Nest(offset, doc) => {
71                    actions.push((indent + offset, mode, doc));
72                }
73                Doc::Text(text) => {
74                    self.cols += measure_text_width(text);
75                    out.push_str(text);
76                    fits &= self.cols <= self.options.width;
77                }
78                Doc::NewLine => {
79                    self.cols = indent;
80                    out.push_str(line_break);
81                    match self.options.indent_kind {
82                        IndentKind::Space => {
83                            out.push_str(&" ".repeat(indent));
84                        }
85                        IndentKind::Tab => {
86                            out.push_str(&"\t".repeat(indent / self.options.tab_size));
87                            out.push_str(&" ".repeat(indent % self.options.tab_size));
88                        }
89                    }
90                    fits &= self.cols <= self.options.width;
91                }
92                Doc::EmptyLine => {
93                    out.push_str(line_break);
94                }
95                Doc::Break(spaces, offset) => {
96                    match mode {
97                        Mode::Flat => {
98                            self.cols += spaces;
99                            out.push_str(&" ".repeat(*spaces));
100                        }
101                        Mode::Break => {
102                            self.cols = indent + offset;
103                            out.push_str(line_break);
104                            match self.options.indent_kind {
105                                IndentKind::Space => {
106                                    out.push_str(&" ".repeat(self.cols));
107                                }
108                                IndentKind::Tab => {
109                                    out.push_str(&"\t".repeat(self.cols / self.options.tab_size));
110                                    out.push_str(&" ".repeat(self.cols % self.options.tab_size));
111                                }
112                            }
113                        }
114                    };
115                    fits &= self.cols <= self.options.width;
116                }
117                Doc::Group(docs) => match mode {
118                    Mode::Flat => {
119                        actions.extend(docs.iter().map(|doc| (indent, Mode::Flat, doc)).rev());
120                    }
121                    Mode::Break => {
122                        let fitting_actions = docs
123                            .iter()
124                            .map(|doc| (indent, Mode::Flat, doc))
125                            .rev()
126                            .collect();
127                        let mode = if fitting(
128                            fitting_actions,
129                            actions.iter().rev(),
130                            self.cols,
131                            self.options.width,
132                        ) {
133                            Mode::Flat
134                        } else {
135                            Mode::Break
136                        };
137                        actions.extend(docs.iter().map(|doc| (indent, mode, doc)).rev());
138                    }
139                },
140                Doc::List(docs) => {
141                    actions.extend(docs.iter().map(|doc| (indent, mode, doc)).rev());
142                }
143            }
144        }
145
146        fits
147    }
148}
149
150/// Check if a group can be placed on single line.
151///
152/// There's no magic here:
153/// it just simply attempts to put the whole group and the rest actions into current line.
154/// After that, if current column is still less than width limitation,
155/// we can feel sure that this group can be put on current line without line breaks.
156fn fitting<'a>(
157    mut actions: Vec<Action<'a>>,
158    mut best_actions: impl Iterator<Item = &'a Action<'a>>,
159    mut cols: usize,
160    width: usize,
161) -> bool {
162    while let Some((indent, mode, doc)) = actions.pop().or_else(|| best_actions.next().copied()) {
163        match doc {
164            Doc::Nil => {}
165            Doc::Alt(doc_flat, doc_break) => match mode {
166                Mode::Flat => actions.push((indent, mode, doc_flat)),
167                Mode::Break => actions.push((indent, mode, doc_break)),
168            },
169            Doc::Union(attempt, alternate) => match mode {
170                Mode::Flat => actions.push((indent, mode, attempt)),
171                Mode::Break => actions.push((indent, mode, alternate)),
172            },
173            Doc::Nest(offset, doc) => {
174                actions.push((indent + offset, mode, doc));
175            }
176            Doc::Text(text) => {
177                cols += measure_text_width(text);
178            }
179            Doc::Break(spaces, _) => match mode {
180                Mode::Flat => cols += spaces,
181                Mode::Break => return true,
182            },
183            Doc::NewLine => {
184                // https://github.com/Marwes/pretty.rs/blob/83021205d557d77731d404cd40b37b105ab762c7/src/render.rs#L381
185                return matches!(mode, Mode::Break);
186            }
187            Doc::EmptyLine => {}
188            Doc::Group(docs) | Doc::List(docs) => {
189                actions.extend(docs.iter().map(|doc| (indent, mode, doc)).rev());
190            }
191        }
192        if cols > width {
193            return false;
194        }
195    }
196    true
197}
198
199#[cfg(not(feature = "unicode-width"))]
200fn measure_text_width(text: &str) -> usize {
201    text.len()
202}
203
204#[cfg(feature = "unicode-width")]
205fn measure_text_width(text: &str) -> usize {
206    use unicode_width::UnicodeWidthStr;
207    text.width()
208}