Skip to main content

eure_fmt/
doc.rs

1//! Document IR for the Eure formatter.
2//!
3//! This module defines the intermediate representation used by the formatter.
4//! The design follows Wadler's "A Prettier Printer" algorithm.
5
6/// Intermediate representation for formatting.
7///
8/// A `Doc` describes how content should be laid out, with the actual
9/// line-breaking decisions deferred to the printer.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum Doc {
12    /// Empty document
13    Nil,
14
15    /// Literal text (no line breaks allowed within)
16    Text(String),
17
18    /// A line break: becomes a single space if the enclosing group fits on one line,
19    /// otherwise becomes a newline followed by current indentation.
20    Line,
21
22    /// A soft line break: becomes empty if the enclosing group fits on one line,
23    /// otherwise becomes a newline followed by current indentation.
24    SoftLine,
25
26    /// Always a line break, regardless of whether the group fits.
27    HardLine,
28
29    /// Increase indentation for the nested document.
30    Indent(Box<Doc>),
31
32    /// Concatenate two documents.
33    Concat(Box<Doc>, Box<Doc>),
34
35    /// Try to fit the document on one line. If it doesn't fit within the
36    /// max width, break at `Line` and `SoftLine` positions.
37    Group(Box<Doc>),
38
39    /// Conditional content based on whether the enclosing group breaks.
40    /// - `flat`: used when the group fits on one line
41    /// - `broken`: used when the group breaks across lines
42    IfBreak { flat: Box<Doc>, broken: Box<Doc> },
43}
44
45impl Doc {
46    /// Create a text document.
47    pub fn text(s: impl Into<String>) -> Doc {
48        let s = s.into();
49        if s.is_empty() { Doc::Nil } else { Doc::Text(s) }
50    }
51
52    /// Create a line break (space when flat, newline when broken).
53    pub fn line() -> Doc {
54        Doc::Line
55    }
56
57    /// Create a soft line break (empty when flat, newline when broken).
58    pub fn softline() -> Doc {
59        Doc::SoftLine
60    }
61
62    /// Create a hard line break (always a newline).
63    pub fn hardline() -> Doc {
64        Doc::HardLine
65    }
66
67    /// Indent the given document.
68    pub fn indent(doc: Doc) -> Doc {
69        if matches!(doc, Doc::Nil) {
70            Doc::Nil
71        } else {
72            Doc::Indent(Box::new(doc))
73        }
74    }
75
76    /// Create a group that tries to fit on one line.
77    pub fn group(doc: Doc) -> Doc {
78        if matches!(doc, Doc::Nil) {
79            Doc::Nil
80        } else {
81            Doc::Group(Box::new(doc))
82        }
83    }
84
85    /// Concatenate two documents.
86    pub fn concat(self, other: Doc) -> Doc {
87        match (self, other) {
88            (Doc::Nil, other) => other,
89            (this, Doc::Nil) => this,
90            (this, other) => Doc::Concat(Box::new(this), Box::new(other)),
91        }
92    }
93
94    /// Conditional content based on break state.
95    pub fn if_break(flat: Doc, broken: Doc) -> Doc {
96        Doc::IfBreak {
97            flat: Box::new(flat),
98            broken: Box::new(broken),
99        }
100    }
101
102    /// Join multiple documents with a separator.
103    pub fn join(docs: impl IntoIterator<Item = Doc>, sep: Doc) -> Doc {
104        let mut result = Doc::Nil;
105        let mut first = true;
106        for doc in docs {
107            if first {
108                first = false;
109                result = doc;
110            } else {
111                result = result.concat(sep.clone()).concat(doc);
112            }
113        }
114        result
115    }
116
117    /// Concatenate multiple documents.
118    pub fn concat_all(docs: impl IntoIterator<Item = Doc>) -> Doc {
119        let mut result = Doc::Nil;
120        for doc in docs {
121            result = result.concat(doc);
122        }
123        result
124    }
125
126    /// Wrap content with prefix and suffix.
127    pub fn surround(prefix: Doc, content: Doc, suffix: Doc) -> Doc {
128        prefix.concat(content).concat(suffix)
129    }
130}
131
132/// Builder for constructing documents fluently.
133#[derive(Debug, Default)]
134pub struct DocBuilder {
135    parts: Vec<Doc>,
136}
137
138impl DocBuilder {
139    /// Create a new empty builder.
140    pub fn new() -> Self {
141        Self { parts: Vec::new() }
142    }
143
144    /// Add text.
145    pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
146        self.parts.push(Doc::text(s));
147        self
148    }
149
150    /// Add a line break.
151    pub fn line(&mut self) -> &mut Self {
152        self.parts.push(Doc::line());
153        self
154    }
155
156    /// Add a soft line break.
157    pub fn softline(&mut self) -> &mut Self {
158        self.parts.push(Doc::softline());
159        self
160    }
161
162    /// Add a hard line break.
163    pub fn hardline(&mut self) -> &mut Self {
164        self.parts.push(Doc::hardline());
165        self
166    }
167
168    /// Add a document.
169    pub fn push(&mut self, doc: Doc) -> &mut Self {
170        self.parts.push(doc);
171        self
172    }
173
174    /// Build the final document.
175    pub fn build(self) -> Doc {
176        Doc::concat_all(self.parts)
177    }
178
179    /// Build as a group.
180    pub fn build_group(self) -> Doc {
181        Doc::group(self.build())
182    }
183
184    /// Build with indentation.
185    pub fn build_indent(self) -> Doc {
186        Doc::indent(self.build())
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_text_empty() {
196        assert_eq!(Doc::text(""), Doc::Nil);
197    }
198
199    #[test]
200    fn test_text_non_empty() {
201        assert_eq!(Doc::text("hello"), Doc::Text("hello".to_string()));
202    }
203
204    #[test]
205    fn test_concat_nil() {
206        let doc = Doc::text("a").concat(Doc::Nil);
207        assert_eq!(doc, Doc::Text("a".to_string()));
208
209        let doc = Doc::Nil.concat(Doc::text("b"));
210        assert_eq!(doc, Doc::Text("b".to_string()));
211    }
212
213    #[test]
214    fn test_join() {
215        let docs = vec![Doc::text("a"), Doc::text("b"), Doc::text("c")];
216        let joined = Doc::join(docs, Doc::text(", "));
217        // Should produce: a, b, c (as nested concats)
218        assert!(matches!(joined, Doc::Concat(_, _)));
219    }
220
221    #[test]
222    fn test_builder() {
223        let doc = {
224            let mut b = DocBuilder::new();
225            b.text("hello").line().text("world");
226            b.build()
227        };
228        assert!(matches!(doc, Doc::Concat(_, _)));
229    }
230}