// Syntax highlighter adapters (Ruby syntax_highlighter.rb and the client-side
// adapters highlightjs.rb, prettify.rb, html_pipeline.rb).
///|
/// Callout marks extracted from the source of a source block, indexed by
/// 1-based line number: the (guard, numeral) pairs of the callouts on that
/// line, in order (Ruby `extract_callouts`). The guard is `Nil`, the line
/// comment prefix (`Str`) or `List([""])` for XML callouts.
pub type CalloutMarks = Map[Int, Array[(AttrVal, String)]]
///|
/// Options passed to `SyntaxHighlighter::highlight` (Ruby's opts Hash).
pub(all) struct HighlightOptions {
/// Callouts extracted from the source (`None` when there are none or
/// callouts are not processed). An adapter may remove the entries it
/// handles itself; the remaining ones are restored after highlighting.
callouts : CalloutMarks?
/// The CSS mode (`-css` attribute, default `"class"`).
css_mode : String
/// 1-based line numbers to highlight (`highlight` attribute).
highlight_lines : Array[Int]?
/// `"table"` or `"inline"` (`-linenums-mode`) when lines are numbered.
number_lines : String?
/// The first line number (when lines are numbered).
start_line_number : Int?
/// The style (theme) name (`-style` attribute).
style : String?
}
///|
/// Creates highlight options (defaults as Ruby's opts Hash with no entries,
/// except `css_mode`, which Asciidoctor always sets).
pub fn HighlightOptions::new(
callouts? : CalloutMarks,
css_mode? : String = "class",
highlight_lines? : Array[Int],
number_lines? : String,
start_line_number? : Int,
style? : String,
) -> HighlightOptions {
{
callouts,
css_mode,
highlight_lines,
number_lines,
start_line_number,
style,
}
}
///|
/// Options passed to `SyntaxHighlighter::format`.
pub(all) struct FormatOptions {
/// Whether wrapping is disabled (the `nowrap` option or no `prewrap`).
nowrap : Bool
/// The CSS mode (only set when the adapter highlights on the server).
css_mode : String?
/// The style name (only set when the adapter highlights on the server).
style : String?
}
///|
pub fn FormatOptions::new(
nowrap? : Bool = false,
css_mode? : String,
style? : String,
) -> FormatOptions {
{ nowrap, css_mode, style, }
}
///|
/// Options passed to `SyntaxHighlighter::docinfo`.
pub(all) struct DocinfoOptions {
/// Whether stylesheets should be linked instead of embedded.
linkcss : Bool
/// The base URL for assets loaded from the CDN.
cdn_base_url : String
/// `"/"` if the converter emits self-closing tags, otherwise `""`.
self_closing_tag_slash : String
}
///|
pub fn DocinfoOptions::new(
linkcss? : Bool = false,
cdn_base_url? : String = "https://cdnjs.cloudflare.com/ajax/libs",
self_closing_tag_slash? : String = "",
) -> DocinfoOptions {
{ linkcss, cdn_base_url, self_closing_tag_slash, }
}
///|
/// A syntax highlighter adapter (Ruby `SyntaxHighlighter`).
///
/// A server-side adapter returns `true` from `handles_highlighting` (Ruby
/// `highlight?`); `highlight` then handles the `specialcharacters`
/// substitution of source blocks. A client-side adapter only formats the
/// block and inserts docinfo markup. Every method but `name` has a default
/// (Ruby `SyntaxHighlighter::Base`).
pub(open) trait SyntaxHighlighter {
/// The name used in messages and attribute names (e.g. `pygments-style`).
fn name(Self) -> String
/// Whether the adapter highlights the source (Ruby `highlight?`).
fn handles_highlighting(Self) -> Bool = _
/// Highlights the raw source of a source block (node, source, language,
/// options) and returns the highlighted source together with an optional
/// offset: when the highlighted source contains markup before the first
/// source line (e.g. a line number column), the offset of the first line.
/// Callouts are restored to the lines after the offset.
fn highlight(Self, Node, String, String?, HighlightOptions) -> (String, Int?) = _
/// Wraps the converted source block in `
`/`` (Ruby `format`).
fn format(Self, Node, String?, FormatOptions) -> String = _
/// Whether docinfo is emitted at the location (`head` or `footer`).
fn has_docinfo(Self, String) -> Bool = _
/// Docinfo markup for the location (location, document, options).
fn docinfo(Self, String, Node, DocinfoOptions) -> String = _
/// Whether the adapter wants a stylesheet written next to the output when
/// `linkcss` and `copycss` are set (Ruby `write_stylesheet?`).
fn writes_stylesheet(Self, Node) -> Bool = _
/// The stylesheet files to write (Ruby `write_stylesheet`), as (file name
/// relative to the stylesheet output directory, contents). The core is
/// pure, so the file-system driver (`@io`) writes them.
fn write_stylesheet(Self, Node) -> Array[(String, String)] = _
}
///|
impl SyntaxHighlighter with fn handles_highlighting(_self) {
false
}
///|
impl SyntaxHighlighter with fn highlight(_self, node, source, _lang, opts) {
(node.sub_source(source, opts.callouts is Some(_)), None)
}
///|
impl SyntaxHighlighter with fn format(self, node, lang, opts) {
format_source(self.name(), node, lang, opts.nowrap)
}
///|
impl SyntaxHighlighter with fn has_docinfo(_self, _location) {
false
}
///|
impl SyntaxHighlighter with fn docinfo(_self, _location, _doc, _opts) {
""
}
///|
impl SyntaxHighlighter with fn writes_stylesheet(_self, _doc) {
false
}
///|
impl SyntaxHighlighter with fn write_stylesheet(_self, _doc) {
[]
}
///|
/// Ruby `SyntaxHighlighter::Base#format`: wraps the converted content of
/// `node` in `
`.
/// `pre_style` adds a `style` attribute to the `
` element (the Ruby
/// `:transform` hook used by the Pygments and Rouge adapters).
pub fn format_source(
pre_class : String,
node : Node,
lang : String?,
nowrap : Bool,
pre_style? : String,
) -> String {
let class_attr_val = if nowrap {
"\{pre_class} highlight nowrap"
} else {
"\{pre_class} highlight"
}
let style_attr = match pre_style {
Some(s) => " style=\"\{s}\""
None => ""
}
let data_lang = match lang {
Some(l) => " data-lang=\"\{l}\""
None => ""
}
"
\{node.content()}
"
}
///|
pub type SyntaxHighlighterFactory = (String, String, Node) -> &SyntaxHighlighter?
///|
let syntax_highlighter_registry : Map[String, SyntaxHighlighterFactory] = {
"highlightjs": (_, _, _) => Some(ClientHighlighter::new(HighlightJs)),
"highlight.js": (_, _, _) => Some(ClientHighlighter::new(HighlightJs)),
"prettify": (_, _, _) => Some(ClientHighlighter::new(Prettify)),
"html-pipeline": (_, _, _) => Some(ClientHighlighter::new(HtmlPipeline)),
// server-side highlighters behave as in Ruby when their gem is missing;
// an optional package (e.g. highlighter/pygments) replaces the entry
"rouge": (_, _, _) => Some(ClientHighlighter::new(Unavailable("rouge"))),
"coderay": (_, _, _) => Some(ClientHighlighter::new(Unavailable("coderay"))),
"pygments": (_, _, _) => Some(ClientHighlighter::new(Unavailable("pygments"))),
}
///|
fn warn_unavailable(name : String) -> Unit {
if !once_per_process("unavailable:\{name}") {
return
}
let msg = match name {
"pygments" =>
"optional gem 'pygments.rb' is not available (reason: cannot load 'pygments'). Functionality disabled."
_ => "optional gem '\{name}' is not available. Functionality disabled."
}
log_warn(msg)
}
///|
priv enum ClientKind {
HighlightJs
Prettify
HtmlPipeline
Unavailable(String) // rouge, coderay, pygments without their Ruby gems
}
///|
/// Client-side syntax highlighter adapters (highlight.js, prettify, html-pipeline).
priv struct ClientHighlighter {
kind : ClientKind
}
///|
fn ClientHighlighter::new(kind : ClientKind) -> &SyntaxHighlighter {
({ kind, } : ClientHighlighter)
}
///|
impl SyntaxHighlighter for ClientHighlighter with fn name(self) {
match self.kind {
HighlightJs => "highlightjs"
Prettify => "prettify"
HtmlPipeline => "html-pipeline"
Unavailable(n) => n
}
}
///|
impl SyntaxHighlighter for ClientHighlighter with fn has_docinfo(
self,
_location,
) {
match self.kind {
HtmlPipeline | Unavailable(_) => false
_ => true
}
}
///|
impl SyntaxHighlighter for ClientHighlighter with fn format(
self,
node,
lang,
opts,
) {
let nowrap = opts.nowrap
let data_lang = match lang {
Some(l) => " data-lang=\"\{l}\""
None => ""
}
match self.kind {
Unavailable(n) => {
warn_unavailable(n)
format_source(
if n == "coderay" {
"CodeRay"
} else {
n
},
node,
lang,
nowrap,
)
}
HtmlPipeline => {
let lang_attr = match lang {
Some(l) => " lang=\"\{l}\""
None => ""
}
"
\{node.content()}
"
}
HighlightJs => {
let mut pre_class = if nowrap {
"highlightjs highlight nowrap"
} else {
"highlightjs highlight"
}
if node.has_attr("nohighlight-option") {
pre_class = pre_class.replace(old=" highlight", new="")
}
let code_class = "language-\{lang.unwrap_or("none")} hljs"
"