///|
pub using @core {
  type Node,
  type Options,
  type Attributes,
  type AttrVal,
  type Context,
  type Logger,
  type LogMessage,
  type Severity,
  type MemoryVfs,
  logger,
  set_logger,
  with_memory_logger,
}

///|
/// Parses attribute overrides given as a string (`"a=b c!"`, Ruby option form).
/// Spaces can be escaped with a backslash.
pub fn parse_attributes_option(attrs : String) -> Array[(String, AttrVal)] {
  let out = []
  // split on unescaped whitespace
  let entries = []
  let cur = StringBuilder()
  let mut i = 0
  let n = attrs.length()
  while i < n {
    let c = attrs[i]
    if c == '\\' && i + 1 < n && (attrs[i + 1] == ' ' || attrs[i + 1] == '\t') {
      cur.write_char(attrs[i + 1].to_int().unsafe_to_char())
      i += 2
      continue
    }
    if c == ' ' || c == '\t' || c == '\n' {
      if !cur.is_empty() {
        entries.push(cur.to_string())
        cur.reset()
      }
    } else {
      cur.write_char(c.to_int().unsafe_to_char())
    }
    i += 1
  }
  if !cur.is_empty() {
    entries.push(cur.to_string())
  }
  for entry in entries {
    let (k, _, v) = @rb.partition(entry, "=")
    out.push((k, @core.AttrVal::Str(v)))
  }
  out
}

///|
/// Registers the built-in converters unless a converter is already registered
/// for their backend, so a converter the user registered for `html5`,
/// `docbook5` or `manpage` takes precedence (Ruby `Converter.register`).
fn ensure_converters() -> Unit {
  if @core.lookup_converter("html5") is None {
    @html5.register()
  }
  if @core.lookup_converter("manpage") is None {
    @manpage.register()
  }
  if @core.lookup_converter("docbook5") is None {
    @docbook5.register()
  }
}

///|
/// Loads (parses) an AsciiDoc document from a string (Ruby `Asciidoctor.load`).
///
/// Where Ruby raises (e.g. no converter is registered for the backend), the
/// document reports the error through `@core.Node::processing_error` and is
/// left unparsed; `convert` raises it.
pub fn load(input : String, options? : Options = Options::new()) -> Node {
  ensure_converters()
  if options.timings is Some(t) {
    t.start("read")
  }
  let lines = @core.prepare_source_string(input)
  load_prepared(lines, options)
}

///|
fn load_prepared(lines : Array[String], options : Options) -> Node {
  if options.timings is Some(t) {
    t.record("read")
    t.start("parse")
  }
  let doc = @core.new_document(Some(lines), options)
  let doc = if options.parse { doc.parse() } else { doc }
  if options.timings is Some(t) {
    t.record("parse")
  }
  doc
}

///|
/// Loads a document from lines.
pub fn load_lines(
  lines : Array[String],
  options? : Options = Options::new(),
) -> Node {
  ensure_converters()
  if options.timings is Some(t) {
    t.start("read")
  }
  load_prepared(lines, options)
}

///|
/// Converts an AsciiDoc string (Ruby `Asciidoctor.convert` without file output).
///
/// Raises the error that aborted processing, like Ruby: a `@core.ProcessingError`
/// (`MissingConverter` when no converter is registered for the backend,
/// `ConversionFailed` when the converter gives up) or an error raised by an
/// extension (e.g. `@core.ArgumentError`).
///
/// ```mbt check
/// test {
///   let html = @asciidoctor.convert("Hello, *World*!")
///   inspect(
///     html,
///     content=(
///       #|
/// #|

Hello, World!

/// #|
/// ), /// ) /// } /// ``` pub fn convert( input : String, options? : Options = Options::new(), ) -> String raise { let doc = load(input, options~) check_processing_error(doc) let output = doc.convert() check_processing_error(doc) output } ///| fn check_processing_error(doc : Node) -> Unit raise { match doc.processing_error() { Some(e) => raise e None => () } }