|
| 1 | +//! A Wadler/Prettier-style document algebra and its layout engine. |
| 2 | +//! |
| 3 | +//! Rules build a [`Doc`]; [`layout`] renders it, choosing for each [`Doc::Group`] |
| 4 | +//! whether it fits the target width flat or must break onto multiple lines. |
| 5 | +
|
| 6 | +#[derive(Clone)] |
| 7 | +pub(crate) enum Doc { |
| 8 | + Nil, |
| 9 | + Text(String), |
| 10 | + /// A space when flat, a newline (+ indent) when broken. |
| 11 | + Line, |
| 12 | + /// Nothing when flat, a newline (+ indent) when broken. |
| 13 | + Softline, |
| 14 | + /// Always a newline; forces every enclosing group to break. |
| 15 | + Hardline, |
| 16 | + Concat(Vec<Doc>), |
| 17 | + Nest(usize, Box<Doc>), |
| 18 | + Group(Box<Doc>), |
| 19 | +} |
| 20 | + |
| 21 | +pub(crate) fn text(s: impl Into<String>) -> Doc { |
| 22 | + Doc::Text(s.into()) |
| 23 | +} |
| 24 | + |
| 25 | +pub(crate) fn concat(docs: Vec<Doc>) -> Doc { |
| 26 | + Doc::Concat(docs) |
| 27 | +} |
| 28 | + |
| 29 | +pub(crate) fn nest(indent: usize, doc: Doc) -> Doc { |
| 30 | + Doc::Nest(indent, Box::new(doc)) |
| 31 | +} |
| 32 | + |
| 33 | +pub(crate) fn group(doc: Doc) -> Doc { |
| 34 | + Doc::Group(Box::new(doc)) |
| 35 | +} |
| 36 | + |
| 37 | +pub(crate) fn line() -> Doc { |
| 38 | + Doc::Line |
| 39 | +} |
| 40 | + |
| 41 | +pub(crate) fn softline() -> Doc { |
| 42 | + Doc::Softline |
| 43 | +} |
| 44 | + |
| 45 | +pub(crate) fn hardline() -> Doc { |
| 46 | + Doc::Hardline |
| 47 | +} |
| 48 | + |
| 49 | +/// `items` interleaved with `sep`. |
| 50 | +pub(crate) fn join(sep: Doc, items: Vec<Doc>) -> Doc { |
| 51 | + let mut out = Vec::with_capacity(items.len() * 2); |
| 52 | + for (i, doc) in items.into_iter().enumerate() { |
| 53 | + if i > 0 { |
| 54 | + out.push(sep.clone()); |
| 55 | + } |
| 56 | + out.push(doc); |
| 57 | + } |
| 58 | + Doc::Concat(out) |
| 59 | +} |
| 60 | + |
| 61 | +#[derive(Clone, Copy, PartialEq)] |
| 62 | +enum Mode { |
| 63 | + Flat, |
| 64 | + Break, |
| 65 | +} |
| 66 | + |
| 67 | +pub(crate) fn layout(doc: &Doc, width: usize) -> String { |
| 68 | + let mut out = String::new(); |
| 69 | + let mut col = 0usize; |
| 70 | + let mut stack: Vec<(usize, Mode, &Doc)> = vec![(0, Mode::Break, doc)]; |
| 71 | + |
| 72 | + while let Some((indent, mode, doc)) = stack.pop() { |
| 73 | + match doc { |
| 74 | + Doc::Nil => {} |
| 75 | + Doc::Text(s) => { |
| 76 | + out.push_str(s); |
| 77 | + col += s.chars().count(); |
| 78 | + } |
| 79 | + Doc::Concat(docs) => { |
| 80 | + for child in docs.iter().rev() { |
| 81 | + stack.push((indent, mode, child)); |
| 82 | + } |
| 83 | + } |
| 84 | + Doc::Nest(n, inner) => stack.push((indent + n, mode, inner)), |
| 85 | + Doc::Line => match mode { |
| 86 | + Mode::Flat => { |
| 87 | + out.push(' '); |
| 88 | + col += 1; |
| 89 | + } |
| 90 | + Mode::Break => col = newline(&mut out, indent), |
| 91 | + }, |
| 92 | + Doc::Softline => match mode { |
| 93 | + Mode::Flat => {} |
| 94 | + Mode::Break => col = newline(&mut out, indent), |
| 95 | + }, |
| 96 | + Doc::Hardline => col = newline(&mut out, indent), |
| 97 | + Doc::Group(inner) => { |
| 98 | + let mode = if fits(width.saturating_sub(col), indent, inner, &stack) { |
| 99 | + Mode::Flat |
| 100 | + } else { |
| 101 | + Mode::Break |
| 102 | + }; |
| 103 | + stack.push((indent, mode, inner)); |
| 104 | + } |
| 105 | + } |
| 106 | + } |
| 107 | + out |
| 108 | +} |
| 109 | + |
| 110 | +fn newline(out: &mut String, indent: usize) -> usize { |
| 111 | + out.push('\n'); |
| 112 | + for _ in 0..indent { |
| 113 | + out.push(' '); |
| 114 | + } |
| 115 | + indent |
| 116 | +} |
| 117 | + |
| 118 | +/// Whether `group_inner` laid out flat, followed by whatever is already queued, |
| 119 | +/// stays within `remaining` columns before the next line break. |
| 120 | +fn fits<'a>( |
| 121 | + remaining: usize, |
| 122 | + indent: usize, |
| 123 | + group_inner: &'a Doc, |
| 124 | + rest: &[(usize, Mode, &'a Doc)], |
| 125 | +) -> bool { |
| 126 | + let mut budget = remaining.min(isize::MAX as usize) as isize; |
| 127 | + let mut stack: Vec<(usize, Mode, &Doc)> = rest.to_vec(); |
| 128 | + stack.push((indent, Mode::Flat, group_inner)); |
| 129 | + |
| 130 | + while budget >= 0 { |
| 131 | + let (indent, mode, doc) = match stack.pop() { |
| 132 | + Some(item) => item, |
| 133 | + None => return true, |
| 134 | + }; |
| 135 | + match doc { |
| 136 | + Doc::Nil => {} |
| 137 | + Doc::Text(s) => budget -= s.chars().count() as isize, |
| 138 | + Doc::Concat(docs) => { |
| 139 | + for child in docs.iter().rev() { |
| 140 | + stack.push((indent, mode, child)); |
| 141 | + } |
| 142 | + } |
| 143 | + Doc::Nest(n, inner) => stack.push((indent + n, mode, inner)), |
| 144 | + Doc::Line => match mode { |
| 145 | + Mode::Flat => budget -= 1, |
| 146 | + Mode::Break => return true, |
| 147 | + }, |
| 148 | + Doc::Softline => match mode { |
| 149 | + Mode::Flat => {} |
| 150 | + Mode::Break => return true, |
| 151 | + }, |
| 152 | + // A forced break ends this line, so everything up to here fit. |
| 153 | + Doc::Hardline => return true, |
| 154 | + Doc::Group(inner) => stack.push((indent, Mode::Flat, inner)), |
| 155 | + } |
| 156 | + } |
| 157 | + false |
| 158 | +} |
0 commit comments