// The imperative builder: what a printer actually calls.
//
// Streams tokens into an Engine. The comment machinery (defer_eol,
// with_held_eol) lives here rather than in the engine, because whether a
// trailing comment must be held back past a separator is a question about the
// DOCUMENT being built, not about how it lays out.
///|
/// A printer.
pub struct Printer {
engine : Engine
/// The most recent break not yet emitted.
///
/// Held so a following break can coalesce with it, and so `force_eol` and
/// `skip_space` can drop it -- a break already fed to the engine cannot be
/// taken back.
mut pending_break : BreakStrength?
/// Whether anything has been emitted since the last forced end of line.
mut has_emitted : Bool
/// A trailing end-of-line comment, deferred until the next ordinary token.
mut pending_eol : (() -> Unit)?
mut holding_eol : Bool
}
///|
pub fn Printer::new(width? : Int = 78) -> Printer {
{
engine: Engine::new(width~),
pending_break: None,
has_emitted: false,
pending_eol: None,
holding_eol: false,
}
}
///|
/// Emit a non-break token, flushing any pending break first.
///
/// Order matters: a break always precedes the following text or group and
/// follows the preceding group's content.
fn Printer::emit(self : Printer, tok : Token) -> Unit {
match self.pending_break {
Some(s) => {
self.pending_break = None
self.engine.feed(TBreak(s))
}
None => ()
}
self.engine.feed(tok)
}
///|
/// Record a break, coalescing with a pending one into the stronger of the two.
fn Printer::push_break(self : Printer, s : BreakStrength) -> Unit {
self.pending_break = Some(
match self.pending_break {
Some(s0) => if s.strength() >= s0.strength() { s } else { s0 }
None => s
},
)
}
///|
/// Drop a pending break, so a deferred end-of-line comment hugs the preceding
/// token instead of being pushed past the break.
fn Printer::drop_trailing_break(self : Printer) -> BreakStrength? {
let b = self.pending_break
self.pending_break = None
b
}
///|
/// Emit a deferred end-of-line comment, if one is waiting.
fn Printer::force_eol(self : Printer) -> Unit {
match self.pending_eol {
None => ()
Some(emit_comment) => {
self.pending_eol = None
let dropped = self.drop_trailing_break()
emit_comment()
// The comment ends the line. A dropped blank line stays a blank line.
let next_brk : BreakStrength = match dropped {
Some(BlankLine) => BlankLine
_ => Newline
}
self.push_break(next_brk)
self.has_emitted = false
}
}
}
///|
/// Record a trailing line comment, to be emitted at the end of the current
/// line.
///
/// It is flushed before the next ordinary token or group, or at end of output.
/// Emitting it ends the line.
pub fn Printer::defer_eol(self : Printer, emit_comment : () -> Unit) -> Unit {
self.force_eol()
self.pending_eol = Some(emit_comment)
}
///|
/// Run `f` without flushing a pending end-of-line comment first.
///
/// A list separator printed inside `f` then appears on the comment's line,
/// AHEAD of the comment -- which is where a reader expects the comma of
/// `foo, // why`.
pub fn Printer::with_held_eol(self : Printer, f : () -> Unit) -> Unit {
let prev = self.holding_eol
self.holding_eol = true
f()
self.holding_eol = prev
}
///|
/// Whether a trailing comment is currently deferred.
///
/// Lets a caller suppress a trailing comma when the last element already
/// carries a comment: adding the comma would push the comment off that element
/// and change where it re-attaches on a reparse.
pub fn Printer::has_pending_eol(self : Printer) -> Bool {
self.pending_eol is Some(_)
}
///|
fn Printer::text(self : Printer, len : Int, s : String) -> Unit {
if !self.holding_eol {
self.force_eol()
}
self.has_emitted = true
self.emit(TText(len, s))
}
///|
/// Print a string, measured at its DISPLAY width.
pub fn Printer::string(self : Printer, s : String) -> Unit {
self.text(@unicode.terminal_width(s), s)
}
///|
/// Print a string with an explicitly stated display width.
///
/// For text whose printed width is not its measured width -- a coloured span,
/// where the escape sequences occupy no columns.
pub fn Printer::string_as(self : Printer, len : Int, s : String) -> Unit {
self.text(len, s)
}
///|
/// A break that becomes a space when flat.
///
/// Suppressed at the start of a line: there is nothing to separate from.
pub fn Printer::space(self : Printer) -> Unit {
if self.has_emitted {
self.push_break(Space)
}
}
///|
/// A break of zero width when flat.
pub fn Printer::cut(self : Printer) -> Unit {
self.push_break(Cut)
}
///|
/// A break that always ends the line.
pub fn Printer::newline(self : Printer) -> Unit {
self.push_break(Newline)
}
///|
/// A break that always leaves a blank line.
pub fn Printer::blank_line(self : Printer) -> Unit {
self.push_break(BlankLine)
}
///|
/// Run `f` with break-indentation increased by `n`.
pub fn Printer::indent(self : Printer, n : Int, f : () -> Unit) -> Unit {
self.emit(TNest(n))
f()
self.emit(TEnd)
}
///|
/// Emit `f`'s content only when the enclosing group is laid out broken.
pub fn Printer::if_broken(self : Printer, f : () -> Unit) -> Unit {
self.emit(TIfBroken)
f()
self.emit(TEnd)
}
///|
fn Printer::scoped(
self : Printer,
kind : GKind,
skip_space : Bool,
indent : Int,
f : () -> Unit,
) -> Unit {
if !self.holding_eol {
self.force_eol()
}
if skip_space {
match self.pending_break {
Some(Cut) | Some(Space) => self.pending_break = None
_ => ()
}
}
self.emit(TBegin(kind))
// The group's own indent is a nest wrapping its whole body, so every break
// inside indents from base + indent.
if indent != 0 {
self.emit(TNest(indent))
}
f()
if indent != 0 {
self.emit(TEnd)
}
self.emit(TEnd)
}
///|
/// Fill: each soft break wraps independently, as needed.
pub fn Printer::box(
self : Printer,
f : () -> Unit,
skip_space? : Bool = false,
indent? : Int = 0,
) -> Unit {
self.scoped(GBox, skip_space, indent, f)
}
///|
/// All-or-nothing: the whole group flat, or every soft break wraps.
pub fn Printer::hvbox(
self : Printer,
f : () -> Unit,
skip_space? : Bool = false,
indent? : Int = 0,
) -> Unit {
self.scoped(GHv, skip_space, indent, f)
}
///|
pub fn Printer::hovbox(
self : Printer,
f : () -> Unit,
skip_space? : Bool = false,
indent? : Int = 0,
) -> Unit {
self.scoped(GHov, skip_space, indent, f)
}
///|
/// Every soft break wraps.
pub fn Printer::vbox(
self : Printer,
f : () -> Unit,
skip_space? : Bool = false,
indent? : Int = 0,
) -> Unit {
self.scoped(GV, skip_space, indent, f)
}
///|
/// Soft breaks never wrap; hard ones still do.
pub fn Printer::hbox(
self : Printer,
f : () -> Unit,
skip_space? : Bool = false,
) -> Unit {
self.scoped(GH, skip_space, 0, f)
}
///|
/// Flush a trailing deferred comment, then drain the engine.
///
/// A trailing break is left pending and never fed, so it is dropped -- no
/// trailing whitespace.
fn Printer::finalize(self : Printer) -> Unit {
self.force_eol()
self.engine.finish_stream()
}
///|
/// Lay `f` out and return the text.
pub fn run_string(f : (Printer) -> Unit, width? : Int = 78) -> String {
let p = Printer::new(width~)
f(p)
p.finalize()
p.engine.contents()
}
///|
/// Run `f` and return the TOKEN STREAM it builds, laying nothing out.
///
/// The builder is engine-independent -- width never reaches it -- so this is
/// the whole document, in the form a layout engine consumes it. It exists so an
/// alternative engine can be fed exactly what this one is fed; see
/// `printer_pp`.
pub fn run_tokens(f : (Printer) -> Unit, width? : Int = 78) -> Array[Token] {
let p = Printer::new(width~)
let rec : Array[Token] = []
p.engine.record = Some(rec)
p.engine.discarding = true
f(p)
p.finalize()
rec
}
///|
/// Run `f` producing NO output.
///
/// For the dry pass that only needs the printer's side effects -- recording
/// which source spans get looked up, so trivia can be associated to them --
/// without building and laying out a document just to discard it.
pub fn run_discard(f : (Printer) -> Unit) -> Unit {
let p = Printer::new()
p.engine.discarding = true
f(p)
p.finalize()
}