Skip to main content

Crate typeset

Crate typeset 

Source
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:

ShortcutDSLSpaceBreaks
unpad&noyes
pad+yesyes
fix_unpad!&nonever
fix_pad!+yesnever

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 @@ v

All 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::compile and the input to Doc::render. Callers never construct or inspect a Doc. Clone and Drop are derived and structurally deep-safe (they touch only flat Vecs), 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. Fixed is the composition-level analogue of wrapping in fix.
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. Pad chooses whether a space separates them when they share a line; Break chooses whether the composition may break (Break::Fixed forbids it, like wrapping in fix). When a breakable composition doesn’t fit, the right operand moves to the next line. The pad/unpad/fix_pad/fix_unpad shortcuts 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 of left to the leftmost literal of right; 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 layouts as 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 layouts with forced line() breaks — one element per line.
join_with_spaces
Joins layouts with padded compositions: a space between neighbours that share a line, nothing where a line breaks.
line
A forced line break: left on one line, right on the next (respecting the current indentation). Unlike comp, this always breaks.
nest
Wraps a layout so that lines it breaks onto are indented by a fixed width (the tab passed 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).