miden_formatting/prettier/
document.rs

1use alloc::{
2    borrow::Cow,
3    rc::Rc,
4    string::{String, ToString},
5};
6use core::fmt;
7
8#[derive(Debug, Default, Clone)]
9pub enum Document {
10    /// An empty document, rendered as an empty string
11    #[default]
12    Empty,
13    /// A line break, rendered as a single '\n' char
14    Newline,
15    /// A single unicode character.
16    ///
17    /// NOTE: Certain `char` values are normalized to other [Document] variants, e.g. `\n` becomes
18    /// a [Document::Newline], not a [Document::Char].
19    Char(char, u32),
20    /// A literal text string of width `n`
21    Text(Cow<'static, str>, u32),
22    /// A combinator which chooses the leftmost of each
23    /// choice in the given document
24    Flatten(Rc<Document>),
25    /// Increase the indentation of the given document by `n`
26    Indent(u32, Rc<Document>),
27    /// Concatenate two documents
28    Concat(Rc<Document>, Rc<Document>),
29    /// Choose the more optimal of two documents depending on
30    /// the amount of space remaining in the layout
31    Choice(Rc<Document>, Rc<Document>),
32}
33impl Document {
34    /// Returns true if this document has no content, i.e. [Document::Empty]
35    pub fn is_empty(&self) -> bool {
36        matches!(self, Self::Empty)
37    }
38
39    /// Returns true if the content of this document starts with a line break.
40    ///
41    /// This is primarily intended for use by the pretty printer itself, but may be useful to others.
42    pub fn has_leading_newline(&self) -> bool {
43        match self {
44            Self::Empty => false,
45            Self::Newline => true,
46            Self::Char('\n' | '\r', _) => true,
47            Self::Char(..) => false,
48            Self::Text(ref text, _) => text.starts_with(['\n', '\r']),
49            Self::Flatten(doc) => doc.has_leading_newline(),
50            Self::Indent(_, doc) => doc.has_leading_newline(),
51            Self::Concat(a, b) if a.is_empty() => b.has_leading_newline(),
52            Self::Concat(a, _) => a.has_leading_newline(),
53            // The choice should always have a single-line option, so we
54            // have to return false here
55            Self::Choice(..) => false,
56        }
57    }
58}
59impl From<char> for Document {
60    #[inline(always)]
61    fn from(c: char) -> Self {
62        character(c)
63    }
64}
65impl From<&'static str> for Document {
66    #[inline(always)]
67    fn from(s: &'static str) -> Self {
68        const_text(s)
69    }
70}
71impl From<String> for Document {
72    #[inline(always)]
73    fn from(s: String) -> Self {
74        text(s)
75    }
76}
77
78/// Render a line break (i.e. newline) in the output
79pub fn nl() -> Document {
80    Document::Newline
81}
82
83/// Display the given value using its [core::fmt::Display] implementation.
84///
85/// This function expects that the display format does not contain any newlines. Violating this
86/// expectation may produce incorrect output.
87pub fn display(s: impl ToString) -> Document {
88    let string = Cow::<'static, str>::Owned(s.to_string());
89    text(string)
90}
91
92/// Display the given character.
93pub fn character(c: char) -> Document {
94    match c {
95        '\n' => Document::Newline,
96        c => {
97            let width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u32;
98            Document::Char(c, width)
99        },
100    }
101}
102
103/// Display the given string exactly.
104///
105/// Like [display], this function expects the string does not contain any newlines. Violating this
106/// expectation may produce incorrect output.
107pub fn text(s: impl ToString) -> Document {
108    let string = Cow::<'static, str>::Owned(s.to_string());
109    let mut chars = string.chars();
110    match chars.next() {
111        None => Document::Empty,
112        Some(c) if chars.next().is_none() => character(c),
113        Some(_) => {
114            drop(chars);
115            let width = unicode_width::UnicodeWidthStr::width(string.as_ref()) as u32;
116            Document::Text(string, width)
117        },
118    }
119}
120
121/// Same as [text], but for static/constant strings
122pub fn const_text(s: &'static str) -> Document {
123    let mut chars = s.chars();
124    match chars.next() {
125        None => Document::Empty,
126        Some(c) if chars.next().is_none() => character(c),
127        Some(_) => {
128            drop(chars);
129            let string = Cow::Borrowed(s);
130            let width = unicode_width::UnicodeWidthStr::width(string.as_ref()) as u32;
131            Document::Text(string, width)
132        },
133    }
134}
135
136/// Create a document by splitting `input` on line breaks so ensure the invariants of [text] are upheld.
137pub fn split<S: AsRef<str>>(input: S) -> Document {
138    let input = input.as_ref();
139    input
140        .lines()
141        .map(text)
142        .reduce(|acc, doc| match acc {
143            Document::Empty => doc + nl(),
144            other => other + doc + nl(),
145        })
146        .unwrap_or(Document::Empty)
147}
148
149/// Concatenate two documents, producing a single document representing both.
150#[inline(always)]
151pub fn concat(left: Document, right: Document) -> Document {
152    left + right
153}
154
155/// Use the leftmost option of every choice in the given document.
156///
157/// If the given document upholds the expectation that none of the
158/// leftmost choices contain newlines, then this combinator has the
159/// effect of displaying all choices on one line.
160pub fn flatten(doc: Document) -> Document {
161    if doc.is_empty() {
162        return doc;
163    }
164    Document::Flatten(Rc::new(doc))
165}
166
167/// Increase the indentation level of the given document by `width`.
168///
169/// The indentation level determines the number of spaces put after newlines.
170///
171/// NOTE: Indentation is applied following newlines, therefore, the first
172/// line of a document is _not_ indented.
173pub fn indent(indent: u32, doc: Document) -> Document {
174    if doc.is_empty() {
175        return doc;
176    }
177    Document::Indent(indent, Rc::new(doc))
178}
179
180impl core::ops::Add for Document {
181    type Output = Document;
182
183    /// Concatenate the two documents
184    fn add(self: Document, other: Document) -> Self::Output {
185        if self.is_empty() {
186            return other;
187        }
188        if other.is_empty() {
189            return self;
190        }
191        Document::Concat(Rc::new(self), Rc::new(other))
192    }
193}
194
195impl core::ops::Add<char> for Document {
196    type Output = Document;
197
198    /// Concatenate the two documents
199    fn add(self: Document, other: char) -> Self::Output {
200        let other = character(other);
201        if self.is_empty() {
202            return other;
203        }
204        if other.is_empty() {
205            return self;
206        }
207        Document::Concat(Rc::new(self), Rc::new(other))
208    }
209}
210
211impl core::ops::Add<Document> for char {
212    type Output = Document;
213
214    /// Concatenate the two documents
215    fn add(self: char, other: Document) -> Self::Output {
216        let lhs = character(self);
217        if lhs.is_empty() {
218            return other;
219        }
220        if other.is_empty() {
221            return lhs;
222        }
223        Document::Concat(Rc::new(lhs), Rc::new(other))
224    }
225}
226
227impl core::ops::Add<&'static str> for Document {
228    type Output = Document;
229
230    /// Concatenate the two documents
231    fn add(self: Document, other: &'static str) -> Self::Output {
232        let other = const_text(other);
233        if self.is_empty() {
234            return other;
235        }
236        if other.is_empty() {
237            return self;
238        }
239        Document::Concat(Rc::new(self), Rc::new(other))
240    }
241}
242
243impl core::ops::Add<Document> for &'static str {
244    type Output = Document;
245
246    /// Concatenate the two documents
247    fn add(self: &'static str, other: Document) -> Self::Output {
248        let lhs = const_text(self);
249        if lhs.is_empty() {
250            return other;
251        }
252        if other.is_empty() {
253            return lhs;
254        }
255        Document::Concat(Rc::new(lhs), Rc::new(other))
256    }
257}
258
259impl core::ops::AddAssign for Document {
260    /// Append `rhs` to `self`
261    fn add_assign(&mut self, rhs: Document) {
262        if rhs.is_empty() {
263            return;
264        }
265        if self.is_empty() {
266            *self = rhs;
267            return;
268        }
269        let lhs = core::mem::take(self);
270        *self = Document::Concat(Rc::new(lhs), Rc::new(rhs));
271    }
272}
273
274impl core::ops::AddAssign<char> for Document {
275    /// Append `rhs` to `self`
276    fn add_assign(&mut self, rhs: char) {
277        let rhs = character(rhs);
278        if rhs.is_empty() {
279            return;
280        }
281        if self.is_empty() {
282            *self = rhs;
283            return;
284        }
285        let lhs = core::mem::take(self);
286        *self = Document::Concat(Rc::new(lhs), Rc::new(rhs));
287    }
288}
289
290impl core::ops::AddAssign<&'static str> for Document {
291    /// Append `rhs` to `self`
292    fn add_assign(&mut self, rhs: &'static str) {
293        let rhs = const_text(rhs);
294        if rhs.is_empty() {
295            return;
296        }
297        if self.is_empty() {
298            *self = rhs;
299            return;
300        }
301        let lhs = core::mem::take(self);
302        *self = Document::Concat(Rc::new(lhs), Rc::new(rhs));
303    }
304}
305
306impl core::ops::BitOr for Document {
307    type Output = Document;
308
309    /// If inside a `flat`, _or_ the first line of the left document fits within
310    /// the required width, then display the left document. Otherwise, display
311    /// the right document.
312    fn bitor(self: Document, other: Document) -> Self::Output {
313        if self.is_empty() {
314            return other;
315        }
316        if other.is_empty() {
317            return self;
318        }
319        Document::Choice(Rc::new(self), Rc::new(other))
320    }
321}
322
323impl fmt::Display for Document {
324    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
325        use core::fmt::Write;
326        match self {
327            Self::Empty => Ok(()),
328            Self::Newline => f.write_char('\n'),
329            Self::Char(c, _) => f.write_char(*c),
330            doc => {
331                let width = f.width().unwrap_or(80);
332                super::print::pretty_print(doc, width, f)
333            },
334        }
335    }
336}