|
| 1 | +# Printer — Wadler-Lindig Pretty-Printer |
| 2 | + |
| 3 | +A pretty-printing library based on the Wadler-Lindig document algebra |
| 4 | +(Philip Wadler, *"A prettier printer"*, 2003; Christian Lindig's strict |
| 5 | +formulation). The implementation draws heavily from Prettier's internal |
| 6 | +`doc-printer`. |
| 7 | + |
| 8 | +## How it works |
| 9 | + |
| 10 | +Pretty-printing is a two-phase process: |
| 11 | + |
| 12 | +1. **Build** a *document tree* (`Doc`) using combinators (see below). The tree |
| 13 | + describes the *structure* of possible layouts — it contains no concrete line |
| 14 | + breaks or indentation yet. |
| 15 | +2. **Layout** the tree into a concrete string. The layout engine fits as much |
| 16 | + content on each line as possible (up to `printWidth`), breaking `group` |
| 17 | + nodes when the flat representation would exceed the available width. |
| 18 | + |
| 19 | +Example: |
| 20 | + |
| 21 | +```ts |
| 22 | +import { text, group, indent, line, softline, join, layout } from './printer'; |
| 23 | + |
| 24 | +const doc = group([ |
| 25 | + text('SELECT'), |
| 26 | + indent([line, join( |
| 27 | + [text(','), line], |
| 28 | + [text('a'), text('b'), text('c')])] |
| 29 | + ), |
| 30 | + line, |
| 31 | + text('FROM t'), |
| 32 | +]); |
| 33 | + |
| 34 | +layout(doc, { printWidth: 40 }); |
| 35 | +// SELECT |
| 36 | +// a, |
| 37 | +// b, |
| 38 | +// c |
| 39 | +// FROM t |
| 40 | + |
| 41 | +layout(doc, { printWidth: 80 }); |
| 42 | +// SELECT a, b, c FROM t |
| 43 | +``` |
| 44 | + |
| 45 | +## `layout(tree, opts?)` |
| 46 | + |
| 47 | +The main entry point. Takes a *print tree* and returns the formatted string. |
| 48 | + |
| 49 | +Options: |
| 50 | + |
| 51 | +| Option | Type | Default | Description | |
| 52 | +|--------------|----------------------------------------|---------|------------------------------------------| |
| 53 | +| `printWidth` | `number` | `80` | Target maximum line width. | |
| 54 | +| `tabWidth` | `number` | `2` | Spaces per indentation level. | |
| 55 | +| `useTabs` | `boolean` | `false` | Indent with tabs instead of spaces. | |
| 56 | +| `endOfLine` | `'\n'` \| `'\r\n'` \| `'\r'` | `'\n'` | Line-ending character(s). | |
| 57 | + |
| 58 | + |
| 59 | +## Combinators / Commands |
| 60 | + |
| 61 | +The printer provides a set of combinators (or "commands") for building print |
| 62 | +trees. These are the building blocks for describing the structure of the output. |
| 63 | + |
| 64 | +### Primitives |
| 65 | + |
| 66 | +- `text(string)` — Literal text, printed as-is. Must **not** contain |
| 67 | + newline characters. |
| 68 | +- `Doc[]` (array) — Concatenation. An array of docs is rendered left to |
| 69 | + right. |
| 70 | + |
| 71 | +### Line breaks |
| 72 | + |
| 73 | +- `line` — Space `" "` in flat mode; *newline + indent* in break mode. |
| 74 | +- `softline` — Nothing `""` in flat mode; *newline + indent* in break mode. |
| 75 | +- `hardline` — Always a *newline + indent*. Forces the enclosing group to |
| 76 | + break. |
| 77 | +- `hardlineWithoutBreakParent` — Like `hardline` but does **not** force |
| 78 | + the parent group to break. |
| 79 | +- `literalline` — Hard line break that indents to the *root* position |
| 80 | + (set by `markAsRoot`) instead of the current nesting level. Preserves |
| 81 | + trailing whitespace. Forces enclosing groups to break. |
| 82 | +- `literallineWithoutBreakParent` — Like `literalline` but does **not** |
| 83 | + force the parent group to break. |
| 84 | + |
| 85 | +### Grouping |
| 86 | + |
| 87 | +- `group(tree, opts?)` — The core break-decision unit. Tries to render |
| 88 | + print tree node flat (single line). If it exceeds the remaining width, switches |
| 89 | + to break mode. Options: `shouldBreak` (force break), `id` (symbol for |
| 90 | + cross-referencing with `ifBreak`/`indentIfBreak`). |
| 91 | +- `conditionalGroup(states, opts?)` — Multi-state group. Provide |
| 92 | + alternative layouts from most compact to most expanded. The engine tries each |
| 93 | + in order and uses the first one that fits; if none fits, the last state is |
| 94 | + rendered in break mode. |
| 95 | +- `fill(parts)` — Wrapping layout. `parts` alternate between content and |
| 96 | + line-break docs: `[item, line, item, line, ...]`. Fills each line with as |
| 97 | + many items as fit before breaking — unlike `group`, which is all-or-nothing. |
| 98 | + |
| 99 | +### Indentation |
| 100 | + |
| 101 | +- `indent(tree)` — Increase indentation by one tab level. |
| 102 | +- `align(n, tree)` — Increase indentation by exactly `n` spaces (not a |
| 103 | + tab level). Special values: `-1` (dedent one level), |
| 104 | + `Number.NEGATIVE_INFINITY` (reset to root), `{ type: 'root' }` (mark current |
| 105 | + indent as root). |
| 106 | +- `dedent(tree)` — Decrease indentation by one level. Shorthand for |
| 107 | + `align(-1, tree)`. |
| 108 | +- `dedentToRoot(tree)` — Reset indentation to root (column 0 or the |
| 109 | + position set by `markAsRoot`). |
| 110 | +- `markAsRoot(tree)` — Mark the current indentation as the "root" |
| 111 | + position for `literalline`. |
| 112 | + |
| 113 | +### Conditional content |
| 114 | + |
| 115 | +- `ifBreak(breakDoc, flatDoc?, opts?)` — Emit `breakDoc` when the |
| 116 | + enclosing group (or a group referenced by `groupId`) is in break mode; |
| 117 | + otherwise emit `flatDoc`. |
| 118 | +- `indentIfBreak(tree, opts)` — Conditionally indent `doc` based on a |
| 119 | + referenced group's mode. Optimized form of |
| 120 | + `ifBreak(indent(tree), doc, { groupId })`. Set `negate: true` to reverse. |
| 121 | +- `breakParent` — Force all ancestor groups to break. Propagated during |
| 122 | + a pre-pass (`propagateBreaks`). |
| 123 | + |
| 124 | +### Comments & line suffixes |
| 125 | + |
| 126 | +- `lineSuffix(tree)` — Buffer `doc` and emit it at the end of the current |
| 127 | + line, just before the next line break. Used for trailing comments. |
| 128 | +- `lineSuffixBoundary` — Force-flush any pending `lineSuffix` content. |
| 129 | + Prevents trailing comments from leaking past syntactic boundaries. |
| 130 | + |
| 131 | +### Miscellaneous |
| 132 | + |
| 133 | +- `label(lbl, tree)` — Annotate a doc with an opaque label for |
| 134 | + language-specific heuristic introspection. No effect on layout. |
| 135 | +- `trim` — Remove all trailing whitespace from the output at the current |
| 136 | + position. |
| 137 | +- `cursor` — Placeholder for cursor-position tracking. The layout engine |
| 138 | + emits a sentinel; callers can locate it to preserve cursor position after |
| 139 | + formatting. |
| 140 | +- `join(separator, trees)` — Join an array of docs with `separator` |
| 141 | + between each pair. |
| 142 | +- `bracketedList(open, close, sep, items)` — Convenience for a grouped, |
| 143 | + indented, bracketed list. Flat: `(a, b, c)`. Broken: |
| 144 | + ``` |
| 145 | + ( |
| 146 | + a, |
| 147 | + b, |
| 148 | + c |
| 149 | + ) |
| 150 | + ``` |
| 151 | +- `addAlignmentToDoc(doc, size, tabWidth)` — Convert an absolute |
| 152 | + indentation size into nested `indent`/`align` wrappers inside `dedentToRoot`. |
| 153 | + Used for embedded content needing a specific indentation level. |
| 154 | + |
| 155 | +## Traversal |
| 156 | + |
| 157 | +- `traverse(doc, { onEnter?, onExit? })` — Generic depth-first traversal |
| 158 | + of a doc tree using an explicit stack (no recursion). Return `false` from |
| 159 | + `onEnter` to skip a node's children. |
0 commit comments