///|
// Source blocks as asciidoctor-pdf lays them out when they are highlighted:
// the backgrounds its highlighters' fragment callbacks paint
// (Rouge's BackgroundColorizer, TextBackgroundAndBorderRenderer) and its
// SourceWrap extension of Prawn's text box (formatted_text/source_wrap.rb).
///|
/// How far the background of a fragment of a highlighted source block
/// reaches, when not just its glyph box.
pub(all) enum CodeBackground {
/// Rouge's BackgroundColorizer on an inline block: the glyph box grown by
/// the line gap (`code_line_gap`, half above, half below), and to the
/// right edge of the bounds when the fragment extends
Gap(Double, Bool)
/// asciidoctor-pdf's TextBackgroundAndBorderRenderer on an inline block:
/// the height of the font's line centred on the glyph box, to the right
/// edge of the bounds
FullLine
} derive(Eq)
///|
pub extend CodeBackground with Eq::{not_equal, equal}
///|
/// The background of a fragment of a highlighted source block that reaches
/// past its glyph box, for a run at `x`, `width` wide, on the line at
/// `baseline` (the fragment's font `face` at `size`).
fn Flow::code_background_item(
self : Flow,
extent : CodeBackground,
x : Double,
width : Double,
baseline : Double,
face : Face,
size : Double,
color : @pagelayout.Color,
) -> @pagelayout.PageItem {
let a = face.ascender_pt(size)
let d = face.descender_pt(size)
match extent {
Gap(gap, to_edge) =>
rect_item(
x,
baseline - a - gap * 0.5,
if to_edge {
self.right - x
} else {
width
},
a + d + gap,
fill=color,
)
FullLine => {
let h = face.height_pt(size)
rect_item(
x,
baseline - a - (h - (a + d)) * 0.5,
self.right - x,
h,
fill=color,
)
}
}
}
///|
/// The style of what source_wrap adds for a wrapped line, from its line
/// number's: the number's blank (a line number, but unstyled, as
/// asciidoctor-pdf's spacer has no style of its own: no background, no
/// bold) and the line break before it.
fn plain_number_style(style : Style) -> Style {
{
..style,
bold: false,
italic: false,
underline: false,
strike: false,
background: None,
code_background: None,
border_offset: 0.0,
}
}
///|
/// asciidoctor-pdf's SourceWrap: `fragments` (a numbered source block, each
/// line starting with its line number, `Style::linenum`) with every line
/// that wraps in `width` broken where it wraps: a line break, then the
/// number's blank (a no-break space and spaces as wide as the number, in
/// no style of its own), the line's highlight mark if it has one
/// (`Style::line_mark`), and the rest of the line without the blanks it
/// starts with. A number (or blank) wider than the line breaks before its
/// trailing blanks, the line's text going on a line of its own, as Prawn
/// breaks it. Each pass breaks the first line that wraps, consuming some of
/// the source's text or of a number's blanks, so the passes end.
///
/// None when a line cannot fit, as Prawn raises `Prawn::Errors::CannotFit`:
/// its number (or blank) without its blanks is wider than the line, or it
/// would hold nothing else.
pub fn source_wrap(
fragments : Array[Fragment],
ctx : TextContext,
width : Double,
) -> Array[Fragment]? {
guard fragments.get(0) is Some(first) && first.style.linenum else {
return Some(fragments)
}
// the blank of every wrapped line, as wide as the block's first number
// (SourceWrap's `linenum_spacer ||=`)
let spacer = "\u{a0}" + " ".repeat(first.text.length() - 1)
let mut fragments = ctx.apply_fallbacks(fragments)
while true {
let (items, _, ranges) = break_lines(fragments, ctx, width, width)
// whether an item is text of the source (not a number, a mark, a
// dropped space or the padding of a bordered fragment)
let is_text = (item : Item) => {
let style = fragments[item.fragment].style
!style.linenum &&
!style.line_mark &&
item.unit != 0x200B &&
item.unit != pad_unit &&
item.offset >= 0
}
let mut cut : (Int, Int)? = None
let mut split_number : Array[Fragment]? = None
for index, range in ranges {
let (start, end, hard) = range
// a line whose first glyph is wider than it (text a too-wide number
// put on a line of its own, say): Prawn's CannotFit
let mut first = start
while first < end && items[first].fit <= 0.0 {
first += 1
}
if first < end &&
!fragments[items[first].fragment].style.linenum &&
items[first].fit > width + 0.0001 {
return None
}
// the number (or blank) the line starts with, when it is wider than
// the line: Prawn breaks it before its trailing blanks, which the
// next line strips, so the line's text goes on a line of its own, or
// within its digits when they are too wide (a digit wider than the
// line cannot fit)
if start < end && fragments[items[start].fragment].style.linenum {
let f = items[start].fragment
let text = fragments[f].text
let mut keep = text.length()
while keep > 0 && (text[keep - 1] == ' ' || text[keep - 1] == '\t') {
keep -= 1
}
let mut full = 0.0
let mut digits = 0.0
let mut k = start
while k < items.length() && items[k].fragment == f {
full += items[k].fit
if items[k].offset < keep {
digits += items[k].fit
}
k += 1
}
if full > width + 0.0001 {
if keep == 0 {
return None
}
let plain = plain_number_style(fragments[f].style)
if digits > width + 0.0001 {
// its digits are wider than the line: Prawn wraps them by
// character, the rest of the number starting the next line
// (CannotFit when not even one fits)
let mut used = 0.0
let mut fits = 0
let mut k = start
while k < items.length() &&
items[k].fragment == f &&
items[k].offset < keep &&
used + items[k].fit <= width + 0.0001 {
used += items[k].fit
fits = items[k].offset + 1
k += 1
}
if fits == 0 {
return None
}
split_number = Some(
[
..fragments[:f],
{ ..fragments[f], text: text[:fits].to_owned(), },
{
text: "\n",
style: { ..plain, linenum: false, wrap_break: true, },
anchor: None,
},
{ ..fragments[f], text: text[fits:].to_owned(), },
..fragments[f + 1:],
],
)
break
}
split_number = Some(
[
..fragments[:f],
{ ..fragments[f], text: text[:keep].to_owned(), },
{
text: "\n",
style: { ..plain, linenum: false, wrap_break: true, },
anchor: None,
},
..strip_line_start([..fragments[f + 1:]]),
],
)
break
}
}
if hard || index == ranges.length() - 1 || end >= items.length() {
continue
}
cut = Some((start, end))
break
}
if split_number is Some(next) {
fragments = next
continue
}
guard cut is Some((start, at)) else { break }
let f = items[at].fragment
let fragment = fragments[f]
// what the line keeps of the fragment the break falls in: up to the
// character; at the padding after a bordered fragment, which the line
// cannot hold, the fragment wraps itself, at its last blank that leaves
// room for the padding, else (with no text before it on the line) by
// character; else it goes to the next line whole
let mut at = at
if items[at].unit == pad_unit && at > start && items[at - 1].fragment == f {
let pad = items[at].fit
let mut used = 0.0
for k in start.. start &&
items[fits - 1].fragment == f &&
items[fits - 1].offset >= 0 &&
used + pad > width + 0.0001 {
fits -= 1
used -= items[fits].fit
}
let mut blank = fits
while blank > start &&
items[blank - 1].fragment == f &&
items[blank - 1].offset >= 0 &&
items[blank - 1].unit != ' '.to_int() {
blank -= 1
}
let before = items[start:at]
.iter()
.any(it => is_text(it) && it.fragment < f)
at = if blank > start &&
items[blank - 1].fragment == f &&
items[blank - 1].offset >= 0 &&
blank < at {
blank
} else if !before &&
fits > start &&
items[fits - 1].fragment == f &&
items[fits - 1].offset >= 0 {
fits
} else {
at
}
}
let item = items[at]
let offset = if item.offset >= 0 { item.offset } else { 0 }
// the line must keep some text, or (a line of the number alone, the
// text wrapping behind the blank) the rest of a number: consuming it
// is progress. A line of the blank alone is not: what follows would
// wrap the same way again (Ruby prints blanks until the box is full);
// Prawn's CannotFit
let kept = items[start:at]
.iter()
.any(it => is_text(it) && (it.fragment < f || offset > 0))
let number_alone = items[start:at]
.iter()
.any(it => {
let fragment = fragments[it.fragment]
fragment.style.linenum &&
fragment.text.iter().any(c => c != '\u{a0}' && c != ' ')
})
if !kept && !number_alone {
return None
}
// the number and mark of the source line this one continues
let mut number = f
while number >= 0 && !fragments[number].style.linenum {
number -= 1
}
guard number >= 0 else { return None }
let linenum = fragments[number]
// its mark: right after the number, or after the line break where the
// number was too wide and broke
let mark = fragments[number + 1:f].iter().find_first(m => m.style.line_mark)
let plain = plain_number_style(linenum.style)
let next : Array[Fragment] = []
for i in 0.. 0 {
next.push({ ..fragment, text: fragment.text[:offset].to_owned(), })
}
next.push({
text: "\n",
style: { ..plain, linenum: false, wrap_break: true, },
anchor: None,
})
next.push({ text: spacer, style: plain, anchor: None, })
match mark {
Some(m) => next.push(m)
None => ()
}
// the rest of the line, without the blanks it starts with
next.append(
strip_line_start(
[
{ ..fragment, text: fragment.text[offset:].to_owned(), },
..fragments[f + 1:],
],
),
)
fragments = next
}
Some(fragments)
}
///|
/// `rest`, the fragments of a line that wrapped, its first fragment with
/// text without the blanks it starts with (SourceWrap's and Prawn's
/// `lstrip`; a line break stays), empty fragments dropped.
fn strip_line_start(rest : Array[Fragment]) -> Array[Fragment] {
let out = []
let mut stripping = true
for r in rest {
if stripping && r.style.line_mark {
// a mark takes no room: the line is still empty after it
out.push(r)
} else if stripping && r.text != "" {
let text = strip_blanks(r.text)
stripping = false
if text != "" {
out.push({ ..r, text, })
}
} else if r.text != "" || r.anchor is Some(_) {
out.push(r)
}
}
out
}
///|
/// The text without the blanks it starts with (Ruby's `lstrip`, but a line
/// break, which in Prawn would be a fragment of its own, stays).
fn strip_blanks(s : String) -> String {
let mut start = 0
while start < s.length() {
let c = s[start]
if c == ' ' ||
c == '\t' ||
c == '\r' ||
c == '\u{0B}' ||
c == '\u{0C}' ||
c == '\u{00}' {
start += 1
} else {
break
}
}
s[start:].to_owned()
}