///|
// `source-highlighter: pygments`, as asciidoctor-pdf drives pygments.rb:
// the HTML Pygments writes for a block (inline styles, no wrapper),
// parsed as formatted text, and the block's colours from the style's CSS
// (Pygments::Ext::BlockStyles). Pygments itself is optional: the package
// bobzhang/asciidoctor-pdf/pygments registers it (`register_pygments`).
///|
/// What source highlighting needs from Pygments.
pub(all) struct PygmentsBackend {
/// pygments.rb's `Lexer.find_by_alias`: the name and first alias of the
/// lexer with this alias, if any
find_lexer : (String) -> (String, String)?
/// the lexer for a MIME type (`Lexer.find_by_mimetype`), as above
find_lexer_by_mimetype : (String) -> (String, String)?
/// `Pygments.highlight(code, lexer:, options:)` with the lexer of this
/// alias: the source as HTML with
/// the HTML formatter (options go to the lexer and the formatter alike),
/// right-stripped; None when Pygments fails
highlight : (String, String, Map[String, String]) -> String?
/// `Pygments.css(selector, style:)`: the style's CSS for `selector`
css : (String, String) -> String
/// `Pygments.styles`: the names of the styles
styles : () -> Array[String]
}
///|
let pygments_backend : Ref[PygmentsBackend?] = { val: None, }
///|
/// Make Pygments available to `source-highlighter: pygments` (the package
/// bobzhang/asciidoctor-pdf/pygments does this with bobzhang/pygments).
pub fn register_pygments(backend : PygmentsBackend) -> Unit {
pygments_backend.val = Some(backend)
// the block colours came from the backend replaced
pygments_block_styles.clear()
}
///|
/// The colours of a block in a Pygments style (`BlockStyles.for`):
/// background and text from the style's `.highlight` rule, and the
/// background of highlighted lines (`.hll`); six-digit colours only.
priv struct PygmentsBlockStyle {
background : Color?
font_color : Color?
highlight : Color?
}
///|
/// The block colours of the styles read so far, from the registered backend
/// (`register_pygments` clears them).
let pygments_block_styles : Map[String, PygmentsBlockStyle] = Map([])
///|
let block_selector_rx : @regex.Regex = @regex.re(
"^\\.highlight *\\{([^}]+?)\\}",
)
///|
let highlight_background_rx : @regex.Regex = @regex.re(
"^\\.highlight +\\.hll +\\{ *background(?:-color)?: *#(\\h{6})",
)
///|
let color_properties_rx : @regex.Regex = @regex.re(
"(?:^|;) *(background(?:-color)?|color): *#?(\\h{6}) *(?=$|;)",
)
///|
fn pygments_block_style(
backend : PygmentsBackend,
style : String,
) -> PygmentsBlockStyle {
match pygments_block_styles.get(style) {
Some(s) => return s
None => ()
}
let css = (backend.css)(".highlight", style)
let mut background = None
let mut font_color = None
match block_selector_rx.find(css) {
Some(m) =>
for p in color_properties_rx.find_all(m.group(1).unwrap_or("")) {
let value = parse_color(p.group(2).unwrap_or(""))
if p.group(1) == Some("color") {
font_color = value
} else {
background = value
}
}
None => ()
}
let highlight = highlight_background_rx
.find(css)
.bind(m => parse_color(m.group(1).unwrap_or("")))
let s = { background, font_color, highlight, }
pygments_block_styles[style] = s
s
}
///|
/// The fragments of a source block highlighted by Pygments (the `pygments`
/// branch of asciidoctor-pdf's `convert_code`), the block's background and
/// text colour, and whether the line numbers need SourceWrap.
fn pygments_chunks(
backend : PygmentsBackend,
node : @core.Node,
source : String,
callouts : Bool,
conums : Conums?,
base : Style,
) -> (Array[Fragment], Color?, Color?, Bool) {
let doc = node.document()
let style = match doc.attr("pygments-style") {
Some(s) if (backend.styles)().contains(s) => s
_ => "pastie"
}
let block = pygments_block_style(backend, style)
let base = match block.font_color {
Some(c) => { ..base, color: c, }
None => base
}
if source == "" {
return ([], block.background, block.font_color, false)
}
let language = node.attr("language").unwrap_or("text")
let lexer = match (backend.find_lexer)(language) {
Some(l) => Some(l)
None => (backend.find_lexer_by_mimetype)("text/plain")
}
let options : Map[String, String] = {
"nowrap": "True",
"noclasses": "True",
"stripnl": "False",
"style": style,
}
if lexer is Some(("PHP", _)) {
options["startinline"] = if node.has_option("mixed") {
"False"
} else {
"True"
}
}
let (source, conums) = if callouts {
extract_conums(source)
} else {
(source, conums)
}
// a lexer that fails leaves the source as it is (escaped)
let html = match lexer {
Some((_, alias_)) =>
match (backend.highlight)(source, alias_, options) {
Some(h) => h
None => node.apply_subs(source, [SpecialCharacters])
}
None => node.apply_subs(source, [SpecialCharacters])
}
// a style with no `.hll` background marks no line
let highlight_lines = match (node.attr("highlight"), block.highlight) {
(Some(spec), Some(color)) => {
let lines = @core.resolve_lines_to_highlight(source, spec)
if lines.is_empty() {
None
} else {
let m : Map[Int, Color] = Map([])
for l in lines {
m[l] = color
}
Some(m)
}
}
_ => None
}
let linenums = if node.has_option("linenums") || node.has_attr("linenums") {
Some(ruby_to_i(node.attr("start").unwrap_or("1")))
} else {
None
}
// the formatter makes one empty fragment of no text (a block of callouts
// alone), which restore_conums puts the callouts after
let fragments = match parse_formatted(html, base, normalize=false) {
[] => [({ text: "", style: base, anchor: None, } : Fragment)]
fragments => fragments
}
let fragments = if linenums is Some(_) ||
conums is Some(_) ||
highlight_lines is Some(_) {
restore_conums(
fragments,
conums,
base,
linenums?,
highlight?=highlight_lines,
)
} else {
fragments
}
(
guard_indentation_in_fragments(fragments),
block.background,
block.font_color,
linenums is Some(_),
)
}
///|
/// asciidoctor-pdf's `guard_indentation_in_fragments`: the first space of
/// each line becomes a no-break space, and so does the first space after
/// each line break inside a fragment.
fn guard_indentation_in_fragments(
fragments : Array[Fragment],
) -> Array[Fragment] {
let mut start_of_line = true
fragments.map(fragment => {
let text = fragment.text
if text == "" {
return fragment
}
let guarded = if start_of_line && text.has_prefix(" ") {
"\u{a0}" + text[1:].to_owned().replace_all(old="\n ", new="\n\u{a0}")
} else if text.contains("\n ") {
text.replace_all(old="\n ", new="\n\u{a0}")
} else {
text
}
start_of_line = text.has_suffix("\n")
{ ..fragment, text: guarded, }
})
}