///|
// Text helpers of asciidoctor-pdf's source highlighting (convert_code):
// tab expansion, callout extraction and restoration, indentation guards.
///|
let tab_size = 4
///|
/// asciidoctor-pdf's `expand_tabs`: tabs to spaces, at 4-column stops.
fn expand_tabs(text : String) -> String {
if !text.contains("\t") {
return text
}
let sb = StringBuilder()
let mut column = 0
for c in text {
if c == '\t' {
let spaces = tab_size - column % tab_size
for _ in 0.. ?(?=(?:\\\\? ?)*$)",
)
///|
/// asciidoctor-pdf's `extract_conums`: the source without its callouts
/// (an escaped one loses its backslash) and where they were. Like Ruby's
/// `split`, trailing empty lines are dropped.
fn extract_conums(text : String) -> (String, Conums?) {
let lines = text.split("\n").map(l => l.to_owned()).collect()
while lines.length() > 0 && lines[lines.length() - 1] == "" {
ignore(lines.pop())
}
let mapping : Map[Int, (String?, Array[Int])] = Map([])
let mut auto_num = 0
let out = []
for line_num, line in lines {
let mut line = line
if line.contains("<") {
line = callout_extract_rx.replace(line, fn(m) {
if m.group(1) == Some("\\") {
// honor the escape: the match without its first backslash
let matched = m.matched()
match matched.find("\\") {
Some(i) => matched[:i].to_owned() + matched[i + 1:].to_owned()
None => matched
}
} else {
let number = if m.group(3) == Some(".") {
auto_num += 1
auto_num
} else {
ruby_to_i(m.group(3).unwrap_or("0"))
}
match mapping.get(line_num) {
Some((_, numbers)) => numbers.push(number)
None => mapping[line_num] = (None, [number])
}
""
}
})
// the white space before the callouts is kept apart
match mapping.get(line_num) {
Some((_, numbers)) if line.has_suffix(" ") => {
let trimmed = ruby_rstrip(line)
mapping[line_num] = (
Some(line[trimmed.length():].to_owned()),
numbers,
)
line = trimmed
}
_ => ()
}
}
out.push(line)
}
(
out.join("\n"),
if mapping.is_empty() {
None
} else {
Some({ lines: mapping, })
},
)
}
///|
/// Ruby's `String#rstrip` (white space and NUL).
fn ruby_rstrip(s : String) -> String {
let mut end = s.length()
while end > 0 {
let c = s[end - 1]
if c == ' ' ||
c == '\t' ||
c == '\n' ||
c == '\r' ||
c == '\u{0B}' ||
c == '\u{0C}' ||
c == '\u{00}' {
end -= 1
} else {
break
}
}
s[:end].to_owned()
}
///|
/// The glyphs of the conum numbers `numbers` (asciidoctor-pdf's
/// `conum_glyph`), joined by spaces.
fn conum_text(numbers : Array[Int]) -> String {
numbers.map(n => conum_glyph(n.to_string())).join(" ")
}
///|
/// asciidoctor-pdf's `restore_conums`: the fragments regrouped by line,
/// each line given its line number (`linenums`, the first line's number,
/// right-aligned with spaces, in `code_linenum_font_color`) and the mark
/// of its highlight (`highlight`: the background by line number), and
/// ended by its callouts (in the conum font and colour).
fn restore_conums(
fragments : Array[Fragment],
conums : Conums?,
base : Style,
linenums? : Int,
highlight? : Map[Int, Color],
) -> Array[Fragment] {
// a line is made when a fragment is on it (Ruby's `lines[n] ||= []`): no
// fragments, no lines, and no callouts (a block of callouts alone)
let lines : Array[Array[Fragment]] = []
let line_at = (n : Int) => {
while lines.length() <= n {
lines.push([])
}
lines[n]
}
let mut line_num = 0
for fragment in fragments {
let text = fragment.text
let line = line_at(line_num)
if text == "\n" {
line_num += 1
ignore(line_at(line_num))
} else if text.contains("\n") {
for idx, part in text.split("\n").collect() {
let line = if idx == 0 {
line
} else {
line_num += 1
line_at(line_num)
}
if part != "" {
line.push({ ..fragment, text: part.to_owned(), })
}
}
} else {
line.push(fragment)
}
}
let conum_family = match t_str("conum_font_family") {
Some(f) if f != base.family => Some(f)
_ => None
}
let conum_color = t_color("conum_font_color")
let last = lines.length() - 1
let pad = (last + 1).to_string().length()
let linenum_color = t_color_or("code_linenum_font_color", rgb_hex(0x999999))
let out : Array[Fragment] = []
for index, line in lines {
let visible = index + linenums.unwrap_or(1)
if linenums is Some(_) {
let number = visible.to_string()
let padded = " ".repeat(@cmp.maximum(0, pad - number.length())) +
number +
" "
out.push({
text: padded,
style: { ..base, color: linenum_color, linenum: true, },
anchor: None,
})
}
match highlight.bind(h => h.get(visible)) {
Some(color) => out.push(line_mark(base, color, FullLine))
None => ()
}
out.append(line)
match conums.bind(c => c.lines.get(index)) {
Some((space, numbers)) => {
match space {
Some(s) => out.push({ text: s, style: base, anchor: None, })
None => ()
}
let style = {
..base,
color: conum_color.unwrap_or(base.color),
family: conum_family.unwrap_or(base.family),
font_set: base.font_set || conum_family is Some(_),
}
out.push({ text: conum_text(numbers), style, anchor: None, })
}
None => ()
}
if index != last {
out.push({ text: "\n", style: base, anchor: None, })
}
}
out
}
///|
/// The zero-width mark that paints a highlighted source line (Rouge's
/// `highlight_line_fragment`, asciidoctor-pdf's `restore_conums`).
fn line_mark(base : Style, color : Color, extent : CodeBackground) -> Fragment {
{
text: "\u{200B}",
style: {
..base,
background: Some(color),
code_background: Some(extent),
line_mark: true,
},
anchor: None,
}
}