pretty_simple/lib.rs
1// Copyright 2025 Cameron Swords
2// SPDX-License-Identifier: Apache-2.0
3
4use std::rc::Rc;
5
6use once_cell::unsync::Lazy;
7
8mod tests;
9
10// -------------------------------------------------------------------------------------------------
11// Main Trait
12// -------------------------------------------------------------------------------------------------
13
14pub trait ToDoc {
15 /// Convert the type into a `Doc`.
16 fn to_doc(&self) -> Doc;
17 fn render(&self, width: i16) -> String {
18 self.to_doc().render(width)
19 }
20}
21
22// -------------------------------------------------------------------------------------------------
23// Helpers
24// -------------------------------------------------------------------------------------------------
25
26/// Convert an iterator of items to a `Doc` by rendering each item with `ToDoc` and
27/// interspersing `separator` between them.
28///
29/// Returns [`Doc::nil()`] if the iterator is empty.
30///
31/// # Example
32/// ```rust
33///use pretty_simple::*;
34///
35/// #[derive(Debug)]
36/// struct Item(&'static str);
37///
38/// impl ToDoc for Item {
39/// fn to_doc(&self) -> Doc { Doc::text(self.0) }
40/// }
41///
42/// let xs = [Item("a"), Item("b"), Item("c")];
43///
44/// let doc = to_list(xs.iter(), Doc::text(","));
45///
46/// assert_eq!(doc.render(80), "a,b,c");
47/// ```
48pub fn to_list<'a, T>(docs: impl IntoIterator<Item = &'a T>, separator: Doc) -> Doc
49where
50 T: ToDoc + 'a,
51{
52 let mut iter = docs.into_iter();
53 if let Some(first) = iter.next() {
54 let mut output = first.to_doc();
55 for next in iter {
56 output = output.concat(separator.clone()).concat(next.to_doc());
57 }
58 output
59 } else {
60 Doc::nil()
61 }
62}
63
64// -------------------------------------------------------------------------------------------------
65// Documents
66// -------------------------------------------------------------------------------------------------
67
68pub struct Doc(Rc<DocInner>);
69
70type DocFn = Rc<dyn Fn(i16) -> Doc + 'static>;
71
72enum DocInner {
73 Empty,
74 Text(String),
75 Line, // soft line break
76 Concat(Doc, Doc),
77 Nest(i16, Doc),
78 Alt(Doc, Doc),
79 Nesting(DocFn),
80 Column(DocFn),
81}
82
83// This is a bit of an absue of notation, but it will make our lives a touch simpler.
84impl DocInner {
85 fn into_doc(self) -> Doc {
86 Doc(Rc::new(self))
87 }
88}
89
90impl Clone for Doc {
91 fn clone(&self) -> Self {
92 Doc(Rc::clone(&self.0))
93 }
94}
95// -----------------------------------------------
96// Thread Locals
97// -----------------------------------------------
98
99thread_local! {
100 static NIL_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Empty));
101 static SPACE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text(" ".to_string())));
102 static COMMA_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text(",".to_string())));
103 static LINE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Line));
104 static SOFTLINE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Alt(Doc::space(), Doc::line())));
105 static SOFTLINE_EMPTY_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Alt(Doc::nil(), Doc::line())));
106 static LPAREN_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("(".to_string())));
107 static RPAREN_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text(")".to_string())));
108 static LANGLE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("<".to_string())));
109 static RANGLE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text(">".to_string())));
110 static LBRACKET_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("[".to_string())));
111 static RBRACKET_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("]".to_string())));
112 static LBRACE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("{".to_string())));
113 static RBRACE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("}".to_string())));
114}
115
116impl Doc {
117 // -------------------------------------------
118 // Core Constructors
119 // -------------------------------------------
120
121 /// The empty document.
122 ///
123 /// Renders to nothing and acts as the identity element for [`Doc::concat`].
124 pub fn nil() -> Doc {
125 NIL_INNER.with(|lazy| Doc(Rc::clone(lazy)))
126 }
127
128 /// A single ASCII space as a document (`" "`).
129 pub fn space() -> Doc {
130 SPACE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
131 }
132
133 /// A single ASCII comma as a document (`","`).
134 pub fn comma() -> Doc {
135 COMMA_INNER.with(|lazy| Doc(Rc::clone(lazy)))
136 }
137
138 /// A hard line break.
139 ///
140 /// When rendered, this always breaks the line and sets the cursor to the current
141 /// indentation level tracked by nesting/indentation combinators.
142 pub fn line() -> Doc {
143 LINE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
144 }
145
146 /// A soft line break that becomes a space if the layout fits the given width,
147 /// or a newline otherwise.
148 ///
149 /// This is equivalent to `Alt(space, line)` in Wadler/Leijen pretty‑printing.
150 pub fn softline() -> Doc {
151 SOFTLINE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
152 }
153
154 /// A soft line break that becomes empty if the layout fits, or a newline
155 /// otherwise.
156 ///
157 /// Useful for optional separators (e.g., trailing commas off).
158 pub fn softline_empty() -> Doc {
159 SOFTLINE_EMPTY_INNER.with(|lazy| Doc(Rc::clone(lazy)))
160 }
161
162 /// Construct a document from raw text.
163 ///
164 /// The string is inserted verbatim; it will not contain line breaks unless
165 /// they are present in the string itself (which generally should be avoided
166 /// in pretty‑printing docs).
167 pub fn text<S: Into<String>>(str: S) -> Doc {
168 DocInner::Text(str.into()).into_doc()
169 }
170
171 /// Concatenate two documents without inserting any separator.
172 pub fn concat(self, other: Doc) -> Doc {
173 DocInner::Concat(self, other).into_doc()
174 }
175
176 /// Increase the nesting (indentation) level for all lines that follow a newline
177 /// within the given document by `depth` columns.
178 pub fn nest(self, depth: i16) -> Doc {
179 DocInner::Nest(depth, self).into_doc()
180 }
181
182 // `<+>` from Haskell
183 //
184 // Concatenates the two documents with a space between them.
185 pub fn concat_space(self, other: Doc) -> Doc {
186 self.concat(Doc::space()).concat(other)
187 }
188
189 /// Creates an `alt` set, preferring the first one if it fits and devolving to the second if it
190 /// does not.
191 pub fn alt(self, other: Doc) -> Doc {
192 DocInner::Alt(self, other).into_doc()
193 }
194
195 /// Try to render `self` on a single line by first flattening all soft breaks;
196 /// if that does not fit within the current width, fall back to the original
197 /// (multi‑line) layout.
198 ///
199 /// This is the standard `group` combinator from pretty‑printing literature.
200 pub fn group(self) -> Doc {
201 match &*self.0 {
202 DocInner::Alt(_, _) => self,
203 _ => DocInner::Alt(self.clone().flatten(), self).into_doc(),
204 }
205 }
206
207 fn flatten(self) -> Doc {
208 match &*self.0 {
209 DocInner::Empty | DocInner::Text(_) => self,
210 DocInner::Line => Doc::space(),
211 DocInner::Concat(x, y) => {
212 DocInner::Concat(x.clone().flatten(), y.clone().flatten()).into_doc()
213 }
214 DocInner::Nest(_, inner) => inner.clone().flatten(),
215 DocInner::Alt(flat, _) => flat.clone().flatten(),
216 DocInner::Column(f) => {
217 let f = Rc::clone(f);
218 let f = Rc::new(move |i| f(i).flatten());
219 Doc(Rc::new(DocInner::Column(f)))
220 }
221 DocInner::Nesting(f) => {
222 let f = Rc::clone(f);
223 let f = Rc::new(move |i| f(i).flatten());
224 Doc(Rc::new(DocInner::Nesting(f)))
225 }
226 }
227 }
228
229 /// Create a document whose contents are computed from the **current output column**.
230 ///
231 /// The closure receives the current cursor column (0‑based) and returns the
232 /// document to splice in at that point. The closure is stored as a `'static`
233 /// callable via `Rc`, so capture owned data in it.
234 ///
235 /// See also [`Doc::nesting`].
236 pub fn column<F>(f: F) -> Doc
237 where
238 F: Fn(i16) -> Doc + 'static,
239 {
240 let f: DocFn = Rc::new(f);
241 DocInner::Column(f).into_doc()
242 }
243
244 /// Create a document whose contents are computed from the **current nesting level**.
245 ///
246 /// The closure receives the current indentation level (the `i` tracked by the
247 /// renderer) and returns the document to splice in. Use this to align content
248 /// relative to the current indent.
249 ///
250 /// See also [`Doc::column`].
251 pub fn nesting<F>(f: F) -> Doc
252 where
253 F: Fn(i16) -> Doc + 'static,
254 {
255 let f: DocFn = Rc::new(f);
256 DocInner::Nesting(f).into_doc()
257 }
258
259 // -------------------------------------------
260 // Helpers
261 // -------------------------------------------
262
263 /// Fold an iterator of documents by repeatedly combining adjacent items with
264 /// `concat_f`.
265 ///
266 /// This is a generalized form of [`hcat`](Self::hcat), [`hsep`](Self::hsep),
267 /// and [`vsep`](Self::vsep). Returns [`Doc::nil()`] for an empty iterator.
268 pub fn concat_with<F>(docs: impl IntoIterator<Item = Doc>, concat_f: F) -> Doc
269 where
270 F: Fn(Doc, Doc) -> Doc,
271 {
272 let mut iter = docs.into_iter();
273 if let Some(first) = iter.next() {
274 let mut output = first;
275 for next in iter {
276 output = concat_f(output, next);
277 }
278 output
279 } else {
280 Doc::nil()
281 }
282 }
283
284 /// A convenience for “hanging” indentation: `self.nest(i).align()`.
285 ///
286 /// Subsequent lines align under the first character after an `i`‑space indent.
287 pub fn hang(self, i: i16) -> Doc {
288 self.nest(i).align()
289 }
290
291 /// Indent `self` by `i` spaces, and use a hanging layout so subsequent lines
292 /// align under the first non‑space character.
293 ///
294 /// Equivalent to `Doc::spaces(i).concat(self).hang(i)`.
295 pub fn indent(self, i: i16) -> Doc {
296 Doc::spaces(i).concat(self).hang(i)
297 }
298
299 /// Align subsequent lines to the current column.
300 ///
301 /// Useful for layouts like:
302 /// ```text
303 /// key: value that
304 /// wraps across lines
305 /// ```
306 /// Internally implemented via [`Doc::column`] and [`Doc::nesting`].
307 pub fn align(self) -> Doc {
308 // Move an owned clone into the closures so they’re 'static.
309 Doc::column({
310 let base = self.clone();
311 move |k| {
312 let base2 = base.clone();
313 Doc::nesting(move |i| base2.clone().nest(k - i))
314 }
315 })
316 }
317
318 /// Produce `i` spaces as a document (`" ".repeat(i)`), with fast paths for 0 and 1.
319 pub fn spaces(i: i16) -> Doc {
320 match i {
321 0 => Doc::nil(),
322 1 => Doc::space(),
323 n => Doc::text(" ".repeat(n as usize)),
324 }
325 }
326
327 /// Horizontally separate an iterator of documents with single spaces.
328 ///
329 /// Equivalent to interspersing [`Doc::space()`] and concatenating.
330 pub fn hsep(docs: impl IntoIterator<Item = Doc>) -> Doc {
331 Doc::concat_with(docs, |x, y| x.concat_space(y))
332 }
333
334 /// Vertically separate an iterator of documents with hard newlines.
335 ///
336 /// Equivalent to interspersing [`Doc::line()`] and concatenating.
337 pub fn vsep(docs: impl IntoIterator<Item = Doc>) -> Doc {
338 Doc::concat_with(docs, |x, y| x.concat(Doc::line()).concat(y))
339 }
340
341 // Tries laying the elements out with spaces, or vertically if they do not fit.
342 pub fn sep(docs: impl IntoIterator<Item = Doc>) -> Doc {
343 Doc::vsep(docs).group()
344 }
345
346 /// Concatenate an iterator of documents without separators (left‑associative).
347 pub fn hcat(docs: impl IntoIterator<Item = Doc>) -> Doc {
348 Doc::concat_with(docs, |x, y| x.concat(y))
349 }
350
351 /// Concatenate `docs`, inserting `separator` between each adjacent pair.
352 ///
353 /// Returns [`Doc::nil()`] if `docs` is empty.
354 pub fn intersperse(docs: impl IntoIterator<Item = Doc>, separator: Doc) -> Doc {
355 let mut iter = docs.into_iter();
356 if let Some(first) = iter.next() {
357 let mut output = first;
358 for next in iter {
359 output = output.concat(separator.clone()).concat(next);
360 }
361 output
362 } else {
363 Doc::nil()
364 }
365 }
366
367 /// Surround `self` with `(` and `)` (parentheses).
368 pub fn parens(self) -> Doc {
369 Self::lparen().concat(self).concat(Self::rparen())
370 }
371
372 /// Surround `self` with `<` and `>` (angle brackets).
373 pub fn angles(self) -> Doc {
374 Self::langle().concat(self).concat(Self::rangle())
375 }
376
377 /// Surround `self` with `[` and `]` (square brackets).
378 pub fn brackets(self) -> Doc {
379 Self::lbracket().concat(self).concat(Self::rbracket())
380 }
381
382 /// Surround `self` with `{` and `}` (curly braces).
383 pub fn braces(self) -> Doc {
384 Self::lbrace().concat(self).concat(Self::rbrace())
385 }
386
387 /// Render `self` as a typical block:
388 ///
389 /// ```text
390 /// {start}
391 /// {self (indented, grouped)}
392 /// {end}
393 /// ```
394 ///
395 /// Uses a 4‑space indent and inserts newlines before and after the block body.
396 pub fn block(self, start: Doc, end: Doc) -> Doc {
397 start
398 .concat(Doc::line())
399 .concat(self.indent(4).group())
400 .concat(Doc::line())
401 .concat(end)
402 }
403
404 /// Fill a la Wadler
405 /// This
406 pub fn fill(xs: &[Doc]) -> Doc {
407 Self::fill_core(xs, 0, false)
408 }
409
410 /// `head_flat` means: treat xs[i] as already flattened (because caller passed `flatten y : zs`)
411 fn fill_core(xs: &[Doc], i: usize, head_flat: bool) -> Doc {
412 if i >= xs.len() {
413 return Doc::nil();
414 }
415 let n = xs.len() - i;
416 if n == 1 {
417 let mut d = xs[i].clone();
418 if head_flat {
419 d = d.flatten();
420 }
421 return d;
422 }
423
424 // We have at least two: x = xs[i], y = xs[i+1]
425 let x = xs[i].clone();
426 let y_is_head = i + 1; // head of the recursive tail
427
428 // Left branch: (flatten x <+> fill (flatten y : zs))
429 // If the current head is already flattened, don't double-flatten.
430 let x_flat = if head_flat {
431 x.clone()
432 } else {
433 x.clone().flatten()
434 };
435 let left = x_flat
436 .concat(Doc::space())
437 // Next level's head (y) must be treated as already flattened
438 .concat(Self::fill_core(xs, y_is_head, true));
439
440 // Right branch: (x </> fill (y : zs))
441 // If head_flat is true, x is already flattened; use it as-is.
442 let x_for_right = if head_flat { x } else { xs[i].clone() };
443 let right = x_for_right
444 .concat(Doc::line())
445 .concat(Self::fill_core(xs, y_is_head, false));
446
447 left.alt(right)
448 }
449
450 // -------------------------------------------
451 // Constant Constructors
452 // -------------------------------------------
453
454 /// The `<` document.
455 pub fn lparen() -> Doc {
456 LPAREN_INNER.with(|lazy| Doc(Rc::clone(lazy)))
457 }
458
459 /// The `>` document.
460 pub fn rparen() -> Doc {
461 RPAREN_INNER.with(|lazy| Doc(Rc::clone(lazy)))
462 }
463
464 /// The `<` document.
465 pub fn langle() -> Doc {
466 LANGLE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
467 }
468
469 /// The `>` document.
470 pub fn rangle() -> Doc {
471 RANGLE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
472 }
473
474 /// The `[` document.
475 pub fn lbracket() -> Doc {
476 LBRACKET_INNER.with(|lazy| Doc(Rc::clone(lazy)))
477 }
478
479 /// The `]` document.
480 pub fn rbracket() -> Doc {
481 RBRACKET_INNER.with(|lazy| Doc(Rc::clone(lazy)))
482 }
483
484 /// The `{` document.
485 pub fn lbrace() -> Doc {
486 LBRACE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
487 }
488
489 /// The `}` document.
490 pub fn rbrace() -> Doc {
491 RBRACE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
492 }
493
494 // -------------------------------------------
495 // Rendering
496 // -------------------------------------------
497
498 /// Render the document to a `String` using the given maximum line `width`.
499 ///
500 /// Soft breaks choose between space/newline based on whether the flattened
501 /// alternative fits within the remaining width; hard breaks always break.
502 /// The algorithm is a variant of Wadler/Leijen pretty‑printing.
503 pub fn render(self, width: i16) -> String {
504 let rendered = self.best(width);
505 let output = rendered.render();
506 // std::mem::forget(rendered);
507 output.unwrap()
508 }
509
510 fn best(self, width: i16) -> Render {
511 use DocInner as DI;
512
513 enum Cons {
514 Cell { head: (i16, Doc), tail: Rc<Cons> },
515 Nil,
516 }
517
518 fn cons(head: (i16, Doc), tail: Rc<Cons>) -> Rc<Cons> {
519 Rc::new(Cons::Cell { head, tail })
520 }
521
522 // A non-allocating, non-recursive "does it fit?" that peeks ahead.
523 // Returns false if we'd exceed `remaining` or hit a hard Line.
524 fn fits(mut remaining: i16, mut cursor: i16, mut docs: Rc<Cons>) -> bool {
525 while let Cons::Cell {
526 head: (i, doc),
527 tail,
528 } = &*docs
529 {
530 match &*doc.0 {
531 DI::Line => return true,
532 DI::Empty => {
533 docs = tail.clone();
534 }
535 DI::Text(s) => {
536 let s_len = s.len() as i16;
537 if s_len > remaining {
538 return false;
539 };
540 remaining -= s_len;
541 let Some(new_cursor) = cursor.checked_add(s_len) else {
542 return false;
543 };
544 cursor = new_cursor;
545 docs = tail.clone();
546 }
547 DI::Concat(x, y) => {
548 docs = cons((*i, x.clone()), cons((*i, y.clone()), tail.clone()));
549 }
550 DI::Nest(j, inner) => {
551 docs = cons((i + j, inner.clone()), tail.clone());
552 }
553 DI::Alt(flat, _doc2) => {
554 docs = cons((*i, flat.clone()), tail.clone());
555 }
556 DI::Column(f) => {
557 docs = cons((*i, f(cursor)), tail.clone());
558 }
559 DI::Nesting(f) => {
560 docs = cons((*i, f(*i)), tail.clone());
561 }
562 }
563 }
564 true
565 }
566
567 let mut docs = cons((0, self), Rc::new(Cons::Nil));
568 let mut cursor = 0i16;
569 let mut out: Vec<RenderPart> = vec![];
570
571 while let Cons::Cell { head, tail } = &*docs {
572 let (indent, doc) = head;
573 match &*doc.0 {
574 DI::Empty => {
575 docs = tail.clone();
576 }
577 DI::Text(s) => {
578 out.push(RenderPart::Text(s.to_string()));
579 cursor = cursor.saturating_add(s.len() as i16);
580 docs = tail.clone();
581 }
582 DI::Concat(x, y) => {
583 docs = cons(
584 (*indent, x.clone()),
585 cons((*indent, y.clone()), tail.clone()),
586 );
587 }
588 DI::Nest(j, inner) => {
589 docs = cons((indent + j, inner.clone()), tail.clone());
590 }
591 DI::Line => {
592 out.push(RenderPart::Line(*indent));
593 cursor = *indent;
594 docs = tail.clone();
595 }
596 DI::Alt(flat, alt) => {
597 let flat = cons((*indent, flat.clone()), tail.clone());
598 if fits(width, cursor, flat.clone()) {
599 docs = flat;
600 } else {
601 docs = cons((*indent, alt.clone()), tail.clone());
602 }
603 }
604 DI::Column(f) => {
605 docs = cons((*indent, f(cursor)), tail.clone());
606 }
607 DI::Nesting(f) => {
608 docs = cons((*indent, f(*indent)), tail.clone());
609 }
610 }
611 }
612
613 Render(out)
614 }
615}
616
617// -------------------------------------------------------------------------------------------------
618// Rendering
619// -------------------------------------------------------------------------------------------------
620
621enum RenderPart {
622 Line(i16),
623 Text(String),
624}
625
626struct Render(Vec<RenderPart>);
627
628impl Render {
629 fn render(&self) -> Result<String, std::fmt::Error> {
630 use std::fmt::Write;
631 let renders = &self.0;
632 let mut output = String::new();
633 for render in renders.iter() {
634 match render {
635 RenderPart::Line(i) => {
636 writeln!(&mut output)?;
637 for _n in 0..*i {
638 write!(&mut output, " ")?;
639 }
640 }
641 RenderPart::Text(s) => {
642 write!(&mut output, "{}", s)?;
643 }
644 }
645 }
646 Ok(output)
647 }
648}