///|
/// Line wrapper for one block of text.
///
/// Text arrives as words. A word is held back until the next space or line
/// break, so it can be moved to the next line as a whole when it does not fit.
/// East Asian characters are their own break opportunity, which is what makes
/// wrapping work for text without spaces.
priv struct Wrap {
buf : StringBuilder
limit : Int32
mut col : Int32
pending : StringBuilder
mut pending_width : Int32
mut pending_nobreak : Bool
mut space : Bool
mut started : Bool
}
///|
fn Wrap::new(limit : Int32) -> Wrap {
{
buf: StringBuilder(),
limit,
col: 0,
pending: StringBuilder(),
pending_width: 0,
pending_nobreak: false,
space: false,
started: false,
}
}
///|
fn Wrap::push(self : Wrap, styled : String, width : Int32) -> Unit {
self.pending.write_string(styled)
self.pending_width = self.pending_width + width
}
///|
/// Push a chunk that must not start a line, such as a closing CJK bracket or
/// full stop: when it does not fit it overflows the line instead of wrapping.
fn Wrap::push_glued(self : Wrap, styled : String, width : Int32) -> Unit {
if self.pending.is_empty() {
self.pending_nobreak = true
}
self.push(styled, width)
}
///|
fn Wrap::newline(self : Wrap) -> Unit {
self.buf.write_char('\n')
self.col = 0
self.started = false
self.space = false
}
///|
fn Wrap::flush(self : Wrap) -> Unit {
if self.pending.is_empty() {
return
}
let gap = if self.space && self.started { 1 } else { 0 }
let overflow = self.pending_nobreak && self.started
if !overflow &&
self.started &&
self.limit > 0 &&
self.col + gap + self.pending_width > self.limit {
self.newline()
}
if self.space && self.started {
self.buf.write_char(' ')
self.col = self.col + 1
}
self.space = false
self.pending_nobreak = false
self.started = true
self.buf.write_string(self.pending.to_string())
self.col = self.col + self.pending_width
self.pending.reset()
self.pending_width = 0
}
///|
fn Wrap::space(self : Wrap) -> Unit {
self.flush()
if self.started {
self.space = true
}
}
///|
fn Wrap::hard_break(self : Wrap) -> Unit {
self.flush()
self.newline()
}
///|
/// Append text with `style`, breaking at spaces and between East Asian
/// characters.
fn Wrap::text(self : Wrap, t : String, style : Style) -> Unit {
let word = StringBuilder()
let mut word_width = 0
for c in t {
let cw = char_width(c)
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
if !word.is_empty() {
self.push(paint(word.to_string(), style), word_width)
word.reset()
word_width = 0
}
self.space()
} else if cw > 1 {
if !word.is_empty() {
self.push(paint(word.to_string(), style), word_width)
word.reset()
word_width = 0
}
if is_no_break_before(c) {
self.push_glued(paint(c.to_string(), style), cw)
} else {
self.flush()
self.push(paint(c.to_string(), style), cw)
}
} else {
word.write_char(c)
word_width = word_width + cw
}
}
if !word.is_empty() {
self.push(paint(word.to_string(), style), word_width)
}
}
///|
fn Wrap::finish(self : Wrap) -> String {
self.flush()
self.buf.to_string()
}
///|
/// Characters that may not begin a line.
fn is_no_break_before(c : Char) -> Bool {
match c {
'。'
| ','
| '、'
| ';'
| ':'
| '?'
| '!'
| ')'
| '」'
| '』'
| '】'
| '》'
| '〉'
| '〕'
| '}'
| '·'
| '…'
| '—'
| '~' => true
_ => false
}
}
///|
/// Prefix every line of `text`: `first` on the first line, `cont` on the rest.
/// Empty lines are left alone so trailing blanks carry no decoration.
fn indent_lines(text : String, first : String, cont : String) -> String {
let out = StringBuilder()
for i, line in text.split("\n").collect() {
if i > 0 {
out.write_char('\n')
}
let line = line.to_owned()
if line != "" {
out.write_string(if i == 0 { first } else { cont })
out.write_string(line)
}
}
out.to_string()
}