mago_formatter/document/
mod.rs

1use std::fmt;
2
3use crate::document::group::GroupIdentifier;
4
5pub mod group;
6
7#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
8pub enum Document<'a> {
9    String(&'a str),
10    Array(Vec<Document<'a>>),
11    /// Increase the level of indentation.
12    Indent(Vec<Document<'a>>),
13    IndentIfBreak(IndentIfBreak<'a>),
14    /// Mark a group of items which the printer should try to fit on one line.
15    /// This is the basic command to tell the printer when to break.
16    /// Groups are usually nested, and the printer will try to fit everything on one line,
17    /// but if it doesn't fit it will break the outermost group first and try again.
18    /// It will continue breaking groups until everything fits (or there are no more groups to break).
19    Group(Group<'a>),
20    /// Specify a line break.
21    /// If an expression fits on one line, the line break will be replaced with a space.
22    /// Line breaks always indent the next line with the current level of indentation.
23    Line(Line),
24    /// This is used to implement trailing comments.
25    /// It's not practical to constantly check where the line ends to avoid accidentally printing some code at the end of a comment.
26    /// `lineSuffix` buffers docs passed to it and flushes them before any new line.
27    LineSuffix(Vec<Document<'a>>),
28    LineSuffixBoundary,
29    /// Print something if the current `group` or the current element of `fill` breaks and something else if it doesn't.
30    IfBreak(IfBreak<'a>),
31    /// This is an alternative type of group which behaves like text layout:
32    /// it's going to add a break whenever the next element doesn't fit in the line anymore.
33    /// The difference with `group` is that it's not going to break all the separators, just the ones that are at the end of lines.
34    Fill(Fill<'a>),
35    /// Include this anywhere to force all parent groups to break.
36    BreakParent,
37    Align(Align<'a>),
38    /// Trim all newlines from the end of the document.
39    Trim(Trim),
40    /// Do not perform any trimming before printing the next document.
41    DoNotTrim,
42}
43
44#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
45pub struct Align<'a> {
46    pub alignment: &'a str,
47    pub contents: Vec<Document<'a>>,
48}
49
50#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
51pub enum Trim {
52    /// Trims trailing whitespace characters (spaces and tabs) from the end of the document.
53    Whitespace,
54    /// Removes all newline characters from the end of the document.
55    Newlines,
56}
57
58#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)]
59pub struct Line {
60    pub hard: bool,
61    pub soft: bool,
62    pub literal: bool,
63}
64
65#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
66pub struct Group<'a> {
67    pub contents: Vec<Document<'a>>,
68    pub should_break: bool,
69    pub expanded_states: Option<Vec<Document<'a>>>,
70    pub id: Option<GroupIdentifier>,
71}
72
73#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
74pub struct IndentIfBreak<'a> {
75    pub contents: Vec<Document<'a>>,
76    pub group_id: Option<GroupIdentifier>,
77}
78
79#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
80pub struct Fill<'a> {
81    pub parts: Vec<Document<'a>>,
82}
83
84#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
85pub struct IfBreak<'a> {
86    pub break_contents: Box<Document<'a>>,
87    pub flat_content: Box<Document<'a>>,
88    pub group_id: Option<GroupIdentifier>,
89}
90
91#[derive(Clone, Copy)]
92pub enum Separator {
93    #[allow(unused)]
94    SoftLine,
95    HardLine,
96    LiteralLine,
97    CommaLine, // [",", line]
98    Space,
99}
100
101impl Line {
102    /// Specify a line break.
103    /// The difference from line is that if the expression fits on one line, it will be replaced with nothing.
104    pub fn soft() -> Self {
105        Self { soft: true, ..Self::default() }
106    }
107
108    /// Specify a line break that is **always** included in the output,
109    /// no matter if the expression fits on one line or not.
110    pub fn hard() -> Self {
111        Self { hard: true, ..Self::default() }
112    }
113
114    pub fn literal() -> Self {
115        Self { hard: true, literal: true, ..Default::default() }
116    }
117}
118
119impl<'a> Group<'a> {
120    pub fn new(contents: Vec<Document<'a>>) -> Self {
121        Self { contents, should_break: false, id: None, expanded_states: None }
122    }
123
124    pub fn conditional(contents: Vec<Document<'a>>, expanded_states: Vec<Document<'a>>) -> Self {
125        Self { contents, should_break: false, id: None, expanded_states: Some(expanded_states) }
126    }
127
128    pub fn with_break(mut self, yes: bool) -> Self {
129        self.should_break = yes;
130        self
131    }
132
133    pub fn with_id(mut self, id: GroupIdentifier) -> Self {
134        self.id = Some(id);
135        self
136    }
137}
138
139impl<'a> IndentIfBreak<'a> {
140    pub fn new(contents: Vec<Document<'a>>) -> Self {
141        Self { contents, group_id: None }
142    }
143
144    pub fn with_id(mut self, id: GroupIdentifier) -> Self {
145        self.group_id = Some(id);
146        self
147    }
148}
149
150impl<'a> Fill<'a> {
151    pub fn drain_out_pair(&mut self) -> (Option<Document<'a>>, Option<Document<'a>>) {
152        let content = if !self.parts.is_empty() { Some(self.parts.remove(0)) } else { None };
153        let whitespace = if !self.parts.is_empty() { Some(self.parts.remove(0)) } else { None };
154
155        (content, whitespace)
156    }
157
158    pub fn dequeue(&mut self) -> Option<Document<'a>> {
159        if !self.parts.is_empty() { Some(self.parts.remove(0)) } else { None }
160    }
161
162    pub fn enqueue(&mut self, doc: Document<'a>) {
163        self.parts.insert(0, doc);
164    }
165
166    pub fn parts(&self) -> &[Document<'a>] {
167        &self.parts
168    }
169}
170
171impl<'a> IfBreak<'a> {
172    pub fn new(break_contents: Document<'a>, flat_content: Document<'a>) -> Self {
173        Self { break_contents: Box::new(break_contents), flat_content: Box::new(flat_content), group_id: None }
174    }
175
176    pub fn then(break_contents: Document<'a>) -> Self {
177        Self { break_contents: Box::new(break_contents), flat_content: Box::new(Document::empty()), group_id: None }
178    }
179
180    pub fn with_id(mut self, id: GroupIdentifier) -> Self {
181        self.group_id = Some(id);
182        self
183    }
184}
185
186impl<'a> Document<'a> {
187    #[inline]
188    pub fn empty() -> Document<'a> {
189        Document::String("")
190    }
191
192    #[inline]
193    pub fn space() -> Document<'a> {
194        Document::String(" ")
195    }
196
197    pub fn can_break(&self) -> bool {
198        self.any(|doc| matches!(doc, Document::Line(_)))
199    }
200
201    pub fn any<F>(&self, predicate: F) -> bool
202    where
203        F: Fn(&Document<'a>) -> bool,
204    {
205        if predicate(self) {
206            return true;
207        }
208
209        match self {
210            Document::Array(docs) | Document::LineSuffix(docs) | Document::Indent(docs) => docs.iter().any(predicate),
211            Document::IndentIfBreak(IndentIfBreak { contents, .. }) | Document::Group(Group { contents, .. }) => {
212                contents.iter().any(predicate)
213            }
214            Document::IfBreak(IfBreak { break_contents, flat_content, .. }) => {
215                predicate(break_contents) || predicate(flat_content)
216            }
217            Document::Fill(fill) => fill.parts.iter().any(predicate),
218            _ => false,
219        }
220    }
221
222    pub fn join(documents: Vec<Document<'a>>, separator: Separator) -> Vec<Document<'a>> {
223        let mut parts = vec![];
224        for (i, document) in documents.into_iter().enumerate() {
225            if i != 0 {
226                parts.push(match separator {
227                    Separator::Space => Document::String(" "),
228                    Separator::SoftLine => Document::Line(Line::soft()),
229                    Separator::HardLine => Document::Line(Line::hard()),
230                    Separator::LiteralLine => {
231                        Document::Array(vec![Document::Line(Line::literal()), Document::BreakParent])
232                    }
233                    Separator::CommaLine => {
234                        Document::Array(vec![Document::String(","), Document::Line(Line::default())])
235                    }
236                });
237            }
238
239            parts.push(document);
240        }
241        parts
242    }
243}
244
245impl fmt::Display for Document<'_> {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        match self {
248            Document::String(s) => write!(f, "{s:?}"),
249            Document::Array(docs) => {
250                let mut printed: Vec<String> = docs.iter().map(|d| d.to_string()).collect();
251
252                if printed.len() == 1 {
253                    write!(f, "{}", printed.pop().unwrap())
254                } else {
255                    write!(f, "[{}]", printed.join(", "))
256                }
257            }
258            Document::Indent(docs) => {
259                write!(f, "indent({})", Document::Array(docs.clone()))
260            }
261            Document::IndentIfBreak(IndentIfBreak { contents, group_id }) => {
262                let mut options = vec![];
263                if let Some(id) = group_id {
264                    options.push(format!("groupId: {id}"));
265                }
266                let options_str =
267                    if options.is_empty() { String::new() } else { format!(", {{ {} }}", options.join(", ")) };
268                write!(f, "indentIfBreak({}{})", Document::Array(contents.clone()), options_str)
269            }
270            Document::Group(Group { contents, should_break, expanded_states, id }) => {
271                let mut options = vec![];
272                if *should_break {
273                    options.push("shouldBreak: true".to_string());
274                }
275                if let Some(id) = id {
276                    options.push(format!("id: {id}"));
277                }
278                let expanded_states_str = if let Some(states) = expanded_states {
279                    format!(
280                        "conditionalGroup([{}]",
281                        states.iter().map(|s| s.to_string()).collect::<Vec<_>>().join(", ")
282                    )
283                } else {
284                    String::new()
285                };
286                let options_str =
287                    if options.is_empty() { String::new() } else { format!(", {{ {} }}", options.join(", ")) };
288
289                if expanded_states_str.is_empty() {
290                    write!(f, "group({}{})", Document::Array(contents.clone()), options_str)
291                } else {
292                    write!(f, "{}, {}{})", expanded_states_str, Document::Array(contents.clone()), options_str,)
293                }
294            }
295            Document::Line(line) => {
296                if line.literal {
297                    write!(f, "literalLine")
298                } else if line.hard {
299                    write!(f, "hardline")
300                } else if line.soft {
301                    write!(f, "softline")
302                } else {
303                    write!(f, "line")
304                }
305            }
306            Document::LineSuffix(docs) => {
307                write!(f, "lineSuffix({})", Document::Array(docs.clone()))
308            }
309            Document::LineSuffixBoundary => write!(f, "lineSuffixBoundary"),
310            Document::IfBreak(IfBreak { break_contents, flat_content, group_id }) => {
311                let mut options = vec![];
312                if let Some(id) = group_id {
313                    options.push(format!("groupId: {id}"));
314                }
315                let options_str =
316                    if options.is_empty() { String::new() } else { format!(", {{ {} }}", options.join(", ")) };
317
318                write!(f, "ifBreak({break_contents}, {flat_content}{options_str})")
319            }
320            Document::Fill(Fill { parts }) => {
321                write!(f, "fill([{}])", parts.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", "))
322            }
323            Document::BreakParent => write!(f, "breakParent"),
324            Document::Align(Align { alignment, contents }) => {
325                write!(f, "dedentToRoot(align({:?}, {}))", alignment, Document::Array(contents.clone()))
326            }
327            Document::Trim(trim) => match trim {
328                Trim::Whitespace => write!(f, "trim"),
329                Trim::Newlines => write!(f, "trimNewlines"),
330            },
331            Document::DoNotTrim => write!(f, "doNotTrim"),
332        }
333    }
334}