///|
/// Traits describing a backend (Ruby `Converter::BackendTraits`).
pub(all) struct BackendTraits {
  basebackend : String
  filetype : String
  htmlsyntax : String?
  outfilesuffix : String
  supports_templates : Bool
} derive(Debug)

///|
/// Derives backend traits from a backend name (Ruby `Converter.derive_backend_traits`).
pub fn derive_backend_traits(
  backend : String,
  basebackend? : String,
) -> BackendTraits {
  let base = match basebackend {
    Some(b) => b
    None => trailing_digits_rx.gsub(backend, "")
  }
  let (outfilesuffix, filetype) = match base {
    "html" => (".html", "html")
    "docbook" => (".xml", "xml")
    "pdf" => (".pdf", "pdf")
    "epub" => (".epub", "epub")
    "manpage" => (".man", "man")
    "asciidoc" => (".adoc", "adoc")
    _ => (".\{base}", base)
  }
  {
    basebackend: base,
    filetype,
    htmlsyntax: if filetype == "html" {
      Some("html")
    } else {
      None
    },
    outfilesuffix,
    supports_templates: false,
  }
}

///|
/// A converter turns nodes into output. `transform` is the node name
/// (`paragraph`, `inline_quoted`, ...) or `document`/`embedded`/`outline`.
pub(open) trait Converter {
  fn convert(Self, Node, String) -> String
  fn backend_traits(Self) -> BackendTraits
}

///|
/// Creates a converter for a backend: (backend, htmlsyntax, document) -> converter.
pub type ConverterFactory = (String, String?, Node) -> &Converter

///|
let converter_registry : Map[String, ConverterFactory] = Map([])

///|
/// Registers a converter factory for the given backend names.
pub fn register_converter(
  backends : Array[String],
  factory : ConverterFactory,
) -> Unit {
  for b in backends {
    converter_registry[b] = factory
  }
}

///|
/// Looks up the converter factory registered for a backend.
pub fn lookup_converter(backend : String) -> ConverterFactory? {
  converter_registry.get(backend)
}

///|
/// Names of the registered backends.
pub fn registered_backends() -> Array[String] {
  converter_registry.keys().collect()
}

///|
/// A converter that renders nothing (used when no converter is registered).
priv struct NullConverter {
  traits : BackendTraits
}

///|
impl Converter for NullConverter with fn convert(_self, _node, _transform) {
  ""
}

///|
impl Converter for NullConverter with fn backend_traits(self) {
  self.traits
}

///|
/// A converter that dispatches individual transforms to handler functions and
/// falls back to a delegate (Ruby `CompositeConverter` / converter subclassing).
pub struct CompositeConverter {
  handlers : Map[String, (Node) -> String]
  delegate : &Converter
}

///|
pub fn CompositeConverter::new(
  delegate : &Converter,
  handlers? : Map[String, (Node) -> String] = {},
) -> CompositeConverter {
  { handlers, delegate, }
}

///|
pub impl Converter for CompositeConverter with fn convert(self, node, transform) {
  match self.handlers.get(transform) {
    Some(h) => h(node)
    None => self.delegate.convert(node, transform)
  }
}

///|
pub impl Converter for CompositeConverter with fn backend_traits(self) {
  self.delegate.backend_traits()
}

// ---------------------------------------------------------------------------
// Syntax highlighting

///|
/// A syntax highlighter adapter (Ruby `SyntaxHighlighter`). Only client-side
/// highlighters (highlight.js, prettify, ...) are provided by this port.
pub(open) trait SyntaxHighlighter {
  fn name(Self) -> String
  /// Whether the highlighter handles the listing itself (server-side).
  fn handles_highlighting(Self) -> Bool
  /// Formats the (already converted) source block.
  fn format(Self, Node, String?, Map[String, String]) -> String
  /// Whether docinfo is emitted at the location (`head` or `footer`).
  fn has_docinfo(Self, String) -> Bool
  /// Docinfo markup for the location; opts: linkcss, cdn_base_url, self_closing_tag_slash.
  fn docinfo(Self, String, Node, Map[String, String]) -> String
  /// Highlights source (server-side highlighters only): (node, source, lang, opts) -> (html, lineno offsets?)
  fn highlight(Self, Node, String, String, Map[String, String]) -> String
}

///|
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
  "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 handles_highlighting(_self) {
  false
}

///|
impl SyntaxHighlighter for ClientHighlighter with fn highlight(
  _self,
  _node,
  source,
  _lang,
  _opts,
) {
  source
}

///|
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.contains("nowrap")
  let data_lang = match lang {
    Some(l) => " data-lang=\"\{l}\""
    None => ""
  }
  match self.kind {
    Unavailable(n) => {
      warn_unavailable(n)
      let pre_class = if n == "coderay" { "CodeRay" } else { n }
      let pre_class = if nowrap {
        "\{pre_class} highlight nowrap"
      } else {
        "\{pre_class} highlight"
      }
      "
\{node.content()}
" } 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" "
\{node.content()}
" } Prettify => { let mut pre_class = if nowrap { "prettyprint highlight nowrap" } else { "prettyprint highlight" } if node.has_option("linenums") { pre_class = pre_class + (match node.attr("start") { Some(start) => " linenums:\{start}" None => " linenums" }) } "
\{node.content()}
" } } } ///| impl SyntaxHighlighter for ClientHighlighter with fn docinfo( self, location, doc, opts, ) { let cdn_base_url = opts.get("cdn_base_url").unwrap_or("") let slash = opts.get("self_closing_tag_slash").unwrap_or("") match self.kind { HighlightJs => { let base_url = doc .attr("highlightjsdir", default="\{cdn_base_url}/highlight.js/9.18.3") .unwrap_or("") if location == "head" { let theme = doc .attr("highlightjs-theme", default="github") .unwrap_or("github") "" } else { let langs = if doc.has_attr("highlightjs-languages") { @rb.split(doc.attr("highlightjs-languages").unwrap_or(""), ",") .map(l => { "\n" }) .join("") } else { "" } "\n\{langs}" } } Prettify => { let base_url = doc .attr("prettifydir", default="\{cdn_base_url}/prettify/r298") .unwrap_or("") if location == "head" { let theme = doc .attr("prettify-theme", default="prettify") .unwrap_or("prettify") let theme_url = if theme.has_prefix("http://") || theme.has_prefix("https://") { theme } else { "\{base_url}/\{theme}.min.css" } "" } else { "" } } HtmlPipeline | Unavailable(_) => "" } } ///| /// Registers a syntax highlighter factory under the given names. pub fn register_syntax_highlighter( names : Array[String], factory : SyntaxHighlighterFactory, ) -> Unit { for n in names { syntax_highlighter_registry[n] = factory } } ///| /// Creates the registered syntax highlighter for `name`, if any (Ruby /// `SyntaxHighlighter.create`). pub fn create_syntax_highlighter( name : String, backend : String, doc : Node, ) -> &SyntaxHighlighter? { match syntax_highlighter_registry.get(name) { Some(f) => f(name, backend, doc) None => None } }