// Rendering a message: the same value, three ways.
///|
/// Emit prose.
///
/// Lines are separated by hard breaks and, within a line, words by soft breaks
/// so an enclosing fill box reflows them. Runs of spaces -- including leading
/// and trailing ones -- collapse to a single soft break, which is what lets
/// fragments joined with `seq` carry their own separating space. A trailing
/// newline is dropped rather than emitting a dangling blank line.
fn render_words(p : @printer.Printer, s : String) -> Unit {
let lines = s.split("\n").map(v => v.to_owned()).collect()
// Drop ONLY a trailing empty line, not every empty one: a blank line in the
// middle of a message is a deliberate paragraph break.
if lines.length() > 1 && lines[lines.length() - 1] == "" {
lines.pop() |> ignore
}
for li, line in lines {
if li > 0 {
p.newline()
}
let mut first = true
for tok in line.split(" ") {
if !first {
p.space()
}
first = false
if tok.length() > 0 {
p.string(tok.to_owned())
}
}
}
}
///|
fn render_doc(p : @printer.Printer, theme : @colors.Theme, d : Message) -> Unit {
match d {
Empty => ()
Words(s) => render_words(p, s)
Atom(style, quote, s) =>
// The emphasis rule: quote only when this style has no colour to carry
// the emphasis. So the same atom is coloured on a terminal and quoted in
// JSON, and never both.
if quote && theme.escape_sequence(style) == "" {
p.string("'" + s + "'")
} else {
print_styled(p, theme, style, s)
}
Raw(f) => f(p, theme)
Seq(a, b) => {
render_doc(p, theme, a)
render_doc(p, theme, b)
}
Sep(a, b) => {
render_doc(p, theme, a)
p.space()
render_doc(p, theme, b)
}
Group(inner) => p.box(indent=2, () => render_doc(p, theme, inner))
}
}
///|
/// Emit `s` in the theme's colour for `style`, counting only its display width.
///
/// The escape sequences go through `string_as 0` so they occupy no columns, and
/// the text itself is measured by TERMINAL width rather than character count --
/// a wide glyph takes two columns.
pub fn print_styled(
p : @printer.Printer,
theme : @colors.Theme,
style : @colors.Style,
s : String,
) -> Unit {
let seq = theme.escape_sequence(style)
if seq != "" {
p.string_as(0, seq)
}
p.string_as(@unicode.terminal_width(s), s)
if seq != "" {
p.string_as(0, theme.reset)
}
}
///|
/// Emit `m` into `p` as a greedy fill box, so prose reflows at the enclosing
/// layout's width.
pub fn render(p : @printer.Printer, theme : @colors.Theme, m : Message) -> Unit {
p.hovbox(() => render_doc(p, theme, m))
}
///|
/// A margin wide enough that a hard-broken message still emits one physical
/// line per break. A stray break is harmless: JSON escapes it, so one object
/// still occupies one physical line.
const FLAT_MARGIN : Int = 1_000_000
///|
/// Render flat and ANSI-free, for the JSON and short output formats.
pub fn to_plain_string(m : Message) -> String {
@printer.run_string(width=FLAT_MARGIN, p => render(p, @colors.no_color, m))
}