Expand description
§typeset
An embedded DSL for defining source code pretty printers.
A layout is a tree of text literals joined by compositions that may or may not break across lines, under wrappers that decide how a group of compositions breaks and how continuation lines indent. The layout language is designed to fit over a structurally recursive pass of the data you want to print. Compiling a layout resolves the wrappers into a document; rendering a document lays it out greedily at a tab width and a target line width, fitting as many literals on each line as the wrappers allow.
use typeset::*;
let args = pack(seq(join_with_commas([text("x"), text("y"), text("z")])));
let call = unpad(text("f("), fix_unpad(args, text(")")));
let doc = call.compile();
assert_eq!(doc.render(2, 80), "f(x, y, z)");
assert_eq!(doc.render(2, 6), "f(x,\n y,\n z)");The crate has no dependencies. The typeset-macro crate adds the same
language as a compile-time macro (see the end of this page).
§Literals and the empty layout
text is a literal, a single unit that never breaks. null is the empty
layout: it vanishes from the document, together with any wrapper on it, and
is neutral in every composition. It is the natural result for optional data
that is absent.
use typeset::*;
let foobar = unpad(text("foo"), unpad(null(), text("bar")));
assert_eq!(foobar.compile().render(2, 80), "foobar");A literal wider than the target width still renders on one line; the width is a target, not a limit.
§Compositions
comp(left, right, Pad, Break) joins two layouts. The Pad axis puts a
space between them when they share a line; the Break axis says whether the
composition may break, which moves right to the next line. The four
combinations have names:
| Shortcut | DSL | Space | Breaks |
|---|---|---|---|
unpad | & | no | yes |
pad | + | yes | yes |
fix_unpad | !& | no | never |
fix_pad | !+ | yes | never |
line(left, right) always breaks.
use typeset::*;
assert_eq!(unpad(text("foo"), text("bar")).compile().render(2, 80), "foobar");
assert_eq!(pad(text("foo"), text("bar")).compile().render(2, 80), "foo bar");
assert_eq!(line(text("foo"), text("bar")).compile().render(2, 80), "foo\nbar");
// A blank line is a line break onto the empty layout.
assert_eq!(line(text("a"), line(null(), text("b"))).compile().render(2, 80), "a\n\nb");A fixed composition binds the rightmost literal of its left operand to the
leftmost literal of its right operand; everything else in either operand
may still break. That is how punctuation attaches: a comma to the item
before it, a closing delimiter to the last thing it encloses. "foo" & ("bar" !& "baz") is the same layout as "foo" & fix ("bar" & "baz"):
use typeset::*;
let infix = unpad(text("foo"), fix_unpad(text("bar"), text("baz")));
let wrapped = unpad(text("foo"), fix(unpad(text("bar"), text("baz"))));
assert_eq!(infix.compile().render(2, 5), "foo\nbarbaz");
assert_eq!(wrapped.compile().render(2, 5), "foo\nbarbaz");The literals a fixed composition binds form one unbreakable run, and that
run keeps the nest/pack wrappers of its first literal. So fix a
delimiter to what precedes it, and compose an opening delimiter with
unpad: fix_unpad(text("("), pack(args)) would take the first argument
out of the pack.
§Wrappers
§fix
fix never breaks anything inside it: the layout is treated as a literal.
use typeset::*;
let foobar = fix(unpad(text("foo"), text("bar")));
assert_eq!(foobar.compile().render(2, 2), "foobar");§grp
grp keeps its compositions from breaking as long as a composition to its
left can break instead: from the outside the group is measured as a block.
When the group itself does not fit, its compositions break.
use typeset::*;
let grouped = unpad(text("foo"), grp(unpad(text("bar"), text("baz")))).compile();
assert_eq!(grouped.render(2, 10), "foobarbaz");
assert_eq!(grouped.render(2, 7), "foo\nbarbaz");
assert_eq!(grouped.render(2, 4), "foo\nbar\nbaz");
// Without the group the greedy solver fills the first line instead.
let plain = unpad(text("foo"), unpad(text("bar"), text("baz")));
assert_eq!(plain.compile().render(2, 7), "foobar\nbaz");§seq
seq breaks every one of its compositions as soon as one of them breaks:
once one item of a list goes on a new line, they all do. A sequence that
does not fit breaks every composition beneath it, nested sequences
included; only a grp lets nested content fit on its own.
use typeset::*;
let items = seq(unpad(text("foo"), unpad(text("bar"), text("baz"))));
assert_eq!(items.compile().render(2, 7), "foo\nbar\nbaz");
let list = pad(text("x"), seq(join_with_spaces([text("aa"), text("bb"), text("cc")])));
assert_eq!(list.compile().render(2, 8), "x aa\nbb\ncc");§nest
nest indents every line its content breaks onto by one tab, the first
argument of render.
use typeset::*;
let nested = unpad(text("foo"), nest(unpad(text("bar"), text("baz")))).compile();
assert_eq!(nested.render(2, 7), "foobar\n baz");
assert_eq!(nested.render(2, 4), "foo\n bar\n baz");§pack
pack aligns the lines its content breaks onto to the column where the
content started: hanging indentation, as in a Lisp call whose arguments
line up under the first one. The column is max(indentation, mark), so a
mark never pulls a line left of its indentation.
use typeset::*;
let packed = unpad(text("foo"), pack(unpad(text("bar"), text("baz")))).compile();
assert_eq!(packed.render(2, 7), "foobar\n baz");
assert_eq!(packed.render(2, 4), "foo\nbar\nbaz");§Joins
join_with_spaces, join_with_commas and join_with_lines fold a
collection with pad, a comma fixed to each item followed by pad, and
line respectively. An empty collection is null.
use typeset::*;
let doc = join_with_commas([text("x"), text("y"), text("z")]).compile();
assert_eq!(doc.render(2, 80), "x, y, z");
assert_eq!(doc.render(2, 3), "x,\ny,\nz");§Compile, then render
Layout::compile is infallible and runs in constant native stack: a
layout of any depth compiles, with depth costing heap. Doc::render only
borrows the document, so one compiled document renders at several widths,
which is what a buffer of variable width needs. Break decisions are O(1),
so rendering does not slow down with the target width. Width is counted in
characters.
use typeset::*;
let doc = pad(text("This"), pad(text("is"), pad(text("a"), text("test")))).compile();
assert_eq!(doc.render(2, 100), "This is a test");
assert_eq!(doc.render(2, 5), "This\nis a\ntest");§The DSL
The same language as a string, which Layout parses (FromStr) and
prints (Display) at run time, or as a compile-time macro from the
typeset-macro crate, where a bare identifier names a Layout in scope:
null the empty layout
"x" a literal
fix u grp u seq u nest u pack u
u & v u + v u !& v u !+ v u @ v u @@ vAll binary operators share one precedence level and associate to the right; parenthesize for any other grouping.
let layout: typeset::Layout = r#"nest ("function" + "name()") @ "{ body }""#.parse()?;
assert_eq!(layout.to_string(), r#"nest ("function" + "name()") @ "{ body }""#);
assert_eq!(layout.compile().render(2, 40), " function name()\n{ body }");Modules§
- dsl
- The layout DSL.
Structs§
- Doc
- A compiled layout: the output of
Layout::compileand the input toDoc::render. Callers never construct or inspect aDoc.CloneandDropare derived and structurally deep-safe (they touch only flatVecs), so no amount of document nesting can overflow the stack. - Layout
- A layout: the input to
Layout::compile. Built with the constructor functions (text,comp,nest, …); never inspected directly.
Enums§
- Break
- Whether a composition may break across lines — the break axis of
comp.Fixedis the composition-level analogue of wrapping infix. - Pad
- Whether a composition puts a space between its two operands when they share
a line — the padding axis of
comp.
Functions§
- comp
- The general composition of two layouts.
Padchooses whether a space separates them when they share a line;Breakchooses whether the composition may break (Break::Fixedforbids it, like wrapping infix). When a breakable composition doesn’t fit, the right operand moves to the next line. Thepad/unpad/fix_pad/fix_unpadshortcuts name the four combinations. - fix
- Wraps a layout as a fixed unit that never breaks across lines.
- fix_pad
- Padded composition that never breaks —
comp(left, right, Pad::Padded, Break::Fixed). The fix binds the rightmost literal ofleftto the leftmost literal ofright; anything else in either operand may still break. - fix_
unpad - Unpadded composition that never breaks —
comp(left, right, Pad::Unpadded, Break::Fixed). The way to attach punctuation to the literal beside it, such as a separator to the item before it. - grp
- Wraps a layout as a group: every breakable composition inside it breaks
together, all-or-nothing. Contrast
seq, where a break cascades forward. - join_
with_ commas - Joins
layoutsas a comma-separated list: each comma is fixed to the item before it, and the composition after it is padded and breakable. - join_
with_ lines - Joins
layoutswith forcedline()breaks — one element per line. - join_
with_ spaces - Joins
layoutswith padded compositions: a space between neighbours that share a line, nothing where a line breaks. - line
- A forced line break:
lefton one line,righton the next (respecting the current indentation). Unlikecomp, this always breaks. - nest
- Wraps a layout so that lines it breaks onto are indented by a fixed width
(the
tabpassed to rendering). Single-line content is unaffected. - null
- The empty layout: produces no output and is neutral in compositions. Useful as a placeholder when building layouts conditionally. It is the empty text, and vanishes from the document along with any wrappers on it.
- pack
- Wraps a layout so that lines it breaks onto align to the column where its
first element started (hanging indentation), rather than the fixed-width
indentation of
nest. - pad
- Padded, breakable composition —
comp(left, right, Pad::Padded, Break::Breakable). - seq
- Wraps a layout as a sequence: once one composition breaks, every later one
in the sequence breaks too (a cascading break). Contrast
grp. - text
- A text literal: the fundamental visible content. Text is a single unit that never breaks across lines.
- unpad
- Unpadded, breakable composition —
comp(left, right, Pad::Unpadded, Break::Breakable).