oxedyne/fe2o3/fe2o3_text/src/fmt/doc.rs
5.3 KiB, 3 runs
created by r1870400018:11624, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Layout algebra for code formatting. |
| 2 | //! |
| 3 | //! Based on Wadler's "A Prettier Printer" (2003) with extensions for |
| 4 | //! column alignment. The algebra has a small number of constructors |
| 5 | //! that can express any formatting pattern: |
| 6 | //! |
| 7 | //! - `Text` — literal text, never broken. |
| 8 | //! - `Line` — a potential line break (rendered as a space when the |
| 9 | //! enclosing `Group` fits on one line, a newline otherwise). |
| 10 | //! - `HardLine` — an unconditional line break. |
| 11 | //! - `Nest` — increase indentation for the nested document. |
| 12 | //! - `Group` — try to fit the contents on one line; break if too wide. |
| 13 | //! - `Concat` — sequential composition. |
| 14 | //! - `Align` — align continuation lines to the current column. |
| 15 | //! - `IfBreak` — choose between two documents depending on whether |
| 16 | //! the enclosing group was broken. |
| 17 | //! |
| 18 | |
| 19 | /// A layout document. Constructed with the free functions below, |
| 20 | /// then rendered to a string by the renderer. |
| 21 | #[derive(Clone, Debug)] |
| 22 | pub enum Doc { |
| 23 | /// Nothing. |
| 24 | Empty, |
| 25 | /// Literal text (must not contain newlines). |
| 26 | Text(String), |
| 27 | /// A potential line break. Rendered as a single space when the |
| 28 | /// enclosing `Group` fits, or a newline + indentation otherwise. |
| 29 | Line, |
| 30 | /// An unconditional line break. |
| 31 | HardLine, |
| 32 | /// Increase indentation by `n` spaces for the nested document. |
| 33 | Nest(u16, Box<Doc>), |
| 34 | /// Try to fit everything on one line. If it doesn't fit within |
| 35 | /// the remaining width, break at every `Line` inside. |
| 36 | Group(Box<Doc>), |
| 37 | /// Sequential composition. |
| 38 | Concat(Vec<Doc>), |
| 39 | /// Align continuation lines to the current column position. |
| 40 | Align(Box<Doc>), |
| 41 | /// Emit `flat` when the enclosing group is flat (fits on one |
| 42 | /// line), `broken` when the group was broken across lines. |
| 43 | IfBreak { |
| 44 | flat: Box<Doc>, |
| 45 | broken: Box<Doc>, |
| 46 | }, |
| 47 | } |
| 48 | |
| 49 | // ── Constructors ───────────────────────────────────────────────── |
| 50 | |
| 51 | /// Empty document. |
| 52 | pub fn empty() -> Doc { Doc::Empty } |
| 53 | |
| 54 | /// Literal text (must not contain newlines). |
| 55 | pub fn text<S: Into<String>>(s: S) -> Doc { Doc::Text(s.into()) } |
| 56 | |
| 57 | /// A potential line break (space when flat, newline when broken). |
| 58 | pub fn line() -> Doc { Doc::Line } |
| 59 | |
| 60 | /// An unconditional line break. |
| 61 | pub fn hardline() -> Doc { Doc::HardLine } |
| 62 | |
| 63 | /// A soft line break: nothing when the group is flat, a newline |
| 64 | /// when the group breaks. Use inside brackets: `(softline body softline)`. |
| 65 | pub fn softline() -> Doc { |
| 66 | Doc::IfBreak { |
| 67 | flat: Box::new(Doc::Empty), |
| 68 | broken: Box::new(Doc::Line), |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | /// Increase indentation for the nested document. |
| 73 | pub fn nest(indent: u16, doc: Doc) -> Doc { |
| 74 | Doc::Nest(indent, Box::new(doc)) |
| 75 | } |
| 76 | |
| 77 | /// Try to fit the document on one line. |
| 78 | pub fn group(doc: Doc) -> Doc { |
| 79 | Doc::Group(Box::new(doc)) |
| 80 | } |
| 81 | |
| 82 | /// Align continuation lines to the current column. |
| 83 | pub fn align(doc: Doc) -> Doc { |
| 84 | Doc::Align(Box::new(doc)) |
| 85 | } |
| 86 | |
| 87 | /// Emit different documents depending on whether the enclosing |
| 88 | /// group was broken. |
| 89 | pub fn if_break(flat: Doc, broken: Doc) -> Doc { |
| 90 | Doc::IfBreak { |
| 91 | flat: Box::new(flat), |
| 92 | broken: Box::new(broken), |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | /// Concatenate a sequence of documents. |
| 97 | pub fn concat(docs: Vec<Doc>) -> Doc { |
| 98 | // Flatten nested concats and remove empties. |
| 99 | let mut flat = Vec::new(); |
| 100 | for d in docs { |
| 101 | match d { |
| 102 | Doc::Empty => {} |
| 103 | Doc::Concat(inner) => flat.extend(inner), |
| 104 | other => flat.push(other), |
| 105 | } |
| 106 | } |
| 107 | match flat.len() { |
| 108 | 0 => Doc::Empty, |
| 109 | 1 => flat.into_iter().next().unwrap_or(Doc::Empty), |
| 110 | _ => Doc::Concat(flat), |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | /// Concatenate two documents. |
| 115 | pub fn cat(a: Doc, b: Doc) -> Doc { |
| 116 | concat(vec![a, b]) |
| 117 | } |
| 118 | |
| 119 | // ── Convenience combinators ────────────────────────────────────── |
| 120 | |
| 121 | /// Join documents with a separator between each pair. |
| 122 | pub fn join(sep: Doc, docs: Vec<Doc>) -> Doc { |
| 123 | let mut parts = Vec::with_capacity(docs.len() * 2); |
| 124 | let mut first = true; |
| 125 | for d in docs { |
| 126 | if !first { |
| 127 | parts.push(sep.clone()); |
| 128 | } |
| 129 | parts.push(d); |
| 130 | first = false; |
| 131 | } |
| 132 | concat(parts) |
| 133 | } |
| 134 | |
| 135 | /// Join documents with `line()` between each pair (soft breaks). |
| 136 | pub fn join_lines(docs: Vec<Doc>) -> Doc { |
| 137 | join(line(), docs) |
| 138 | } |
| 139 | |
| 140 | /// Text followed by a space. |
| 141 | pub fn texts<S: Into<String>>(s: S) -> Doc { |
| 142 | cat(text(s), text(" ")) |
| 143 | } |
| 144 | |
| 145 | /// Surround a document with left and right text, indenting the body. |
| 146 | /// Typically used for bracketed constructs: `surround("(", ")", 4, body)`. |
| 147 | pub fn surround( |
| 148 | left: &str, |
| 149 | right: &str, |
| 150 | indent: u16, |
| 151 | body: Doc, |
| 152 | ) -> Doc { |
| 153 | group(concat(vec![ |
| 154 | text(left), |
| 155 | nest(indent, concat(vec![line(), body])), |
| 156 | line(), |
| 157 | text(right), |
| 158 | ])) |
| 159 | } |
| 160 | |
| 161 | /// Like `surround` but with a hardline before the closing bracket, |
| 162 | /// producing the "tall" layout even when the group fits. |
| 163 | pub fn surround_hard( |
| 164 | left: &str, |
| 165 | right: &str, |
| 166 | indent: u16, |
| 167 | body: Doc, |
| 168 | ) -> Doc { |
| 169 | concat(vec![ |
| 170 | text(left), |
| 171 | nest(indent, concat(vec![hardline(), body])), |
| 172 | hardline(), |
| 173 | text(right), |
| 174 | ]) |
| 175 | } |
| 176 | |
| 177 | /// A trailing comma: present when broken, absent when flat. |
| 178 | pub fn trailing_comma() -> Doc { |
| 179 | if_break(empty(), text(",")) |
| 180 | } |