///|
fn strs(items : Array[String]) -> AttrVal {
  List(items)
}

///|
fn hide_uri_scheme(doc_attrs : Attributes, target : String) -> String {
  if doc_attrs.contains("hide-uri-scheme") {
    uri_sniff_rx.sub(target, "")
  } else {
    target
  }
}

///|
/// Replaces inline macros (Ruby `sub_macros`).
pub fn Node::sub_macros(self : Node, text : String) -> String {
  let found_square_bracket = text.contains("[")
  let found_colon = text.contains(":")
  let found_macroish = found_square_bracket && found_colon
  let found_macroish_short = found_macroish && text.contains(":[")
  let doc = self.document()
  let doc_attrs = doc.attributes
  let mut text = text
  // extension inline macros
  match doc.extensions() {
    Some(exts) if !exts.inline_macros().is_empty() =>
      for ext in exts.inline_macros() {
        let rx = match ext.config.regexp {
          Some(r) => r
          None => inline_macro_regexp(ext.name, ext.config.format)
        }
        text = rx.replace(text, m => {
          let matched = m.matched()
          if matched.has_prefix("\\") {
            return @rb.from(matched, 1)
          }
          let (target0, content) = if m.has_names() {
            (m.named("target"), m.named("content"))
          } else {
            (m.group(1), m.group(2))
          }
          let mut target = target0
          let attributes = Attributes::new()
          for d in ext.config.default_attrs {
            attributes.set_str(d.0, d.1)
          }
          match content {
            Some(c) => {
              if c == "" {
                if ext.config.content_model != Some(Attributes) {
                  attributes.set_str("text", c)
                }
              } else {
                let c = normalize_text(
                  c,
                  normalize_whitespace=true,
                  unescape_closing_square_brackets=true,
                )
                if ext.config.content_model == Some(Attributes) {
                  self.parse_attributes(
                    c,
                    ext.config.positional_attrs.map(x => Some(x)),
                    into=attributes,
                  )
                  |> ignore
                } else {
                  attributes.set_str("text", c)
                }
              }
              if target is None && ext.config.format == Some("short") {
                target = Some(c)
              }
            }
            None => ()
          }
          match
            ((ext.process)(self, target.unwrap_or(""), attributes) catch {
              e => {
                self.abort_processing(e)
                None
              }
            }) {
            Some(InlineNode(replacement)) => {
              match replacement.attributes.remove_str("subs") {
                Some(inline_subs) =>
                  match
                    self.expand_subs(inline_subs, subject="custom inline macro") {
                    Some(s) =>
                      replacement.text_ = Some(
                        self.apply_subs(replacement.text_.unwrap_or(""), s),
                      )
                    None => ()
                  }
                None => ()
              }
              replacement.convert()
            }
            Some(InlineText(s)) => s
            None => ""
          }
        })
      }
    _ => ()
  }
  if doc_attrs.contains("experimental") {
    if found_macroish_short && (text.contains("kbd:") || text.contains("btn:")) {
      text = inline_kbd_btn_macro_rx.replace(text, m => {
        if m.has(1) {
          @rb.from(m.matched(), 1)
        } else if m.at(2) == "kbd" {
          let mut keys = @rb.strip(m.at(3))
          if keys.contains("]") {
            keys = keys.replace_all(old="\\]", new="]")
          }
          let key_list = if keys.length() > 1 {
            let comma = @rb.index_of(keys, ",", from=1)
            let plus = @rb.index_of(keys, "+", from=1)
            let delim_idx = match (comma, plus) {
              (Some(c), Some(p)) => Some(if c < p { c } else { p })
              (Some(c), None) => Some(c)
              (None, Some(p)) => Some(p)
              (None, None) => None
            }
            match delim_idx {
              Some(di) => {
                let delim = @rb.slice(keys, di, di + 1)
                if keys.has_suffix(delim) {
                  let ks = @rb.split(@rb.chop(keys), delim, limit=-1).map(
                    @rb.strip,
                  )
                  ks[ks.length() - 1] = ks[ks.length() - 1] + delim
                  ks
                } else {
                  @rb.split(keys, delim).map(@rb.strip)
                }
              }
              None => [keys]
            }
          } else {
            [keys]
          }
          let attrs = Attributes::new()
          attrs.set("keys", strs(key_list))
          Node::new_inline(self, Kbd, attributes=attrs).convert()
        } else {
          Node::new_inline(
            self,
            Button,
            text=normalize_text(
              m.at(3),
              normalize_whitespace=true,
              unescape_closing_square_brackets=true,
            ),
          ).convert()
        }
      })
    }
    if found_macroish && text.contains("menu:") {
      text = inline_menu_macro_rx.replace(text, m => {
        if m.matched().has_prefix("\\") {
          return @rb.from(m.matched(), 1)
        }
        let menu = m.at(1)
        let mut submenus : Array[String] = []
        let mut menuitem : String? = None
        match m.group(2) {
          Some(items0) => {
            let items = if items0.contains("]") {
              items0.replace_all(old="\\]", new="]")
            } else {
              items0
            }
            let delim = if items.contains(">") {
              Some(">")
            } else if items.contains(",") {
              Some(",")
            } else {
              None
            }
            match delim {
              Some(d) => {
                submenus = @rb.split(items, d).map(@rb.strip)
                menuitem = submenus.pop()
              }
              None => menuitem = Some(@rb.rstrip(items))
            }
          }
          None => ()
        }
        let attrs = Attributes::new()
        attrs.set_str("menu", menu)
        attrs.set("submenus", strs(submenus))
        attrs.set(
          "menuitem",
          match menuitem {
            Some(mi) => Str(mi)
            None => Nil
          },
        )
        Node::new_inline(self, Menu, attributes=attrs).convert()
      })
    }
    if text.contains("\"") && text.contains(">") {
      text = inline_menu_rx.replace(text, m => {
        if m.matched().has_prefix("\\") {
          return @rb.from(m.matched(), 1)
        }
        let parts = @rb.split(m.at(1), ">").map(@rb.strip)
        let menu = parts.remove(0)
        let menuitem = parts.pop()
        let attrs = Attributes::new()
        attrs.set_str("menu", menu)
        attrs.set("submenus", strs(parts))
        attrs.set(
          "menuitem",
          match menuitem {
            Some(mi) => Str(mi)
            None => Nil
          },
        )
        Node::new_inline(self, Menu, attributes=attrs).convert()
      })
    }
  }
  if found_macroish && (text.contains("image:") || text.contains("icon:")) {
    text = inline_image_macro_rx.replace(text, m => {
      let matched = m.matched()
      if matched.has_prefix("\\") {
        return @rb.from(matched, 1)
      }
      let (type_, posattrs) = if matched.has_prefix("icon:") {
        ("icon", [Some("size")])
      } else {
        ("image", [Some("alt"), Some("width"), Some("height")])
      }
      let target = m.at(1)
      let attrs = self.parse_attributes(m.at(2), posattrs, unescape_input=true)
      let mut id : String? = None
      if type_ != "icon" {
        id = attrs.str("id")
        doc.register_image(target)
        if !attrs.truthy("imagesdir") {
          match doc_attrs.get("imagesdir") {
            Some(v) => attrs.set("imagesdir", v)
            None => attrs.set("imagesdir", Nil)
          }
        }
      }
      if !attrs.truthy("alt") {
        let default_alt = basename(target, drop_ext=true)
          .replace_all(old="_", new=" ")
          .replace_all(old="-", new=" ")
        attrs.set_str("default-alt", default_alt)
        attrs.set_str("alt", default_alt)
      }
      Node::new_inline(self, Image, type_~, target~, id?, attributes=attrs).convert()
    })
  }
  if (text.contains("((") && text.contains("))")) ||
    (found_macroish_short && text.contains("dexterm")) {
    text = inline_indexterm_macro_rx.replace(text, m => {
      self.convert_indexterm(m)
    })
  }
  if found_colon && text.contains("://") {
    text = inline_link_rx.replace(text, m => {
      self.convert_inline_link(m, doc, doc_attrs)
    })
  }
  if found_macroish && (text.contains("link:") || text.contains("ilto:")) {
    text = inline_link_macro_rx.replace(text, m => {
      let matched = m.matched()
      if matched.has_prefix("\\") {
        return @rb.from(matched, 1)
      }
      let mailto = m.has(1)
      let mailto_text = m.at(2)
      let mut target = if mailto { "mailto:" + mailto_text } else { m.at(2) }
      let mut attrs : Attributes? = None
      let mut id : String? = None
      let mut link_text = m.at(3)
      if link_text != "" {
        if link_text.contains("]") {
          link_text = link_text.replace_all(old="\\]", new="]")
        }
        if mailto {
          if !doc.compat_mode() && link_text.contains(",") {
            let (lt, a) = self.extract_attributes_from_text(
              link_text,
              default_text=Some(""),
            )
            link_text = lt.unwrap_or("")
            attrs = Some(a)
            id = a.str("id")
            if a.contains_pos(2) {
              let subject = encode_uri_component(a.get_pos(2).unwrap().to_s())
              if a.contains_pos(3) {
                target = "\{target}?subject=\{subject}&body=\{encode_uri_component(a.get_pos(3).unwrap().to_s())}"
              } else {
                target = "\{target}?subject=\{subject}"
              }
            }
          }
        } else if !doc.compat_mode() && link_text.contains("=") {
          let (lt, a) = self.extract_attributes_from_text(
            link_text,
            default_text=Some(""),
          )
          link_text = lt.unwrap_or("")
          attrs = Some(a)
          id = a.str("id")
        }
        if link_text.has_suffix("^") {
          link_text = @rb.chop(link_text)
          match attrs {
            Some(a) => a.set_default("window", Str("_blank"))
            None =>
              attrs = Some(Attributes::from_array([("window", Str("_blank"))]))
          }
        }
      }
      if link_text == "" {
        if mailto {
          link_text = mailto_text
        } else {
          if doc_attrs.contains("hide-uri-scheme") {
            link_text = uri_sniff_rx.sub(target, "")
            if link_text == "" {
              link_text = target
            }
          } else {
            link_text = target
          }
          match attrs {
            Some(a) =>
              a.set_str(
                "role",
                match a.str("role") {
                  Some(r) if a.contains("role") => "bare \{r}"
                  _ => "bare"
                },
              )
            None =>
              attrs = Some(Attributes::from_array([("role", Str("bare"))]))
          }
        }
      }
      doc.register_link(target)
      Node::new_inline(
        self,
        Anchor,
        text=link_text,
        type_="link",
        target~,
        id?,
        attributes?=attrs,
      ).convert()
    })
  }
  if text.contains("@") {
    text = inline_email_rx.replace(text, m => {
      match m.group(1) {
        Some(lead) =>
          if lead == "\\" {
            @rb.from(m.matched(), 1)
          } else {
            m.matched()
          }
        None => {
          let address = m.matched()
          let target = "mailto:" + address
          doc.register_link(target)
          Node::new_inline(self, Anchor, text=address, type_="link", target~).convert()
        }
      }
    })
  }
  if found_square_bracket &&
    self.context == ListItem &&
    (match self.parent_ {
      Some(p) => p.style == Some("bibliography")
      None => false
    }) {
    text = inline_biblio_anchor_rx.replace_first(text, m => {
      Node::new_inline(
        self,
        Anchor,
        text?=m.group(2),
        type_="bibref",
        id=m.at(1),
      ).convert()
    })
  }
  if (found_square_bracket && text.contains("[[")) ||
    (found_macroish && text.contains("or:")) {
    text = inline_anchor_rx.replace(text, m => {
      if m.has(1) {
        return @rb.from(m.matched(), 1)
      }
      let (id, reftext) = match m.group(2) {
        Some(id) => (id, m.group(3))
        None => {
          let reftext = match m.group(5) {
            Some(r) if r.contains("]") =>
              Some(r.replace_all(old="\\]", new="]"))
            r => r
          }
          (m.at(4), reftext)
        }
      }
      Node::new_inline(self, Anchor, text?=reftext, type_="ref", id~).convert()
    })
  }
  if (text.contains("&") && text.contains(";&l")) ||
    (found_macroish && text.contains("xref:")) {
    text = inline_xref_macro_rx.replace(text, m => self.convert_xref(m, doc))
  }
  if found_macroish && text.contains("tnote") {
    text = inline_footnote_macro_rx.replace(text, m => {
      let matched = m.matched()
      if matched.has_prefix("\\") {
        return @rb.from(matched, 1)
      }
      let mut id : String? = None
      let mut content : String? = None
      if m.has(1) {
        if m.has(3) {
          let parts = @rb.split(m.at(3), ",", limit=2)
          id = parts.get(0)
          content = parts.get(1)
          if !doc.compat_mode() {
            log_warn(
              "found deprecated footnoteref macro: \{matched}; use footnote macro with target instead",
            )
          }
        } else {
          return matched
        }
      } else {
        id = m.group(2)
        content = m.group(3)
      }
      let mut index : String? = None
      let mut type_ : String? = None
      let mut target : String? = None
      match id {
        Some(fid) =>
          match doc.footnotes().search_by(f => f.id == Some(fid)) {
            Some(i) => {
              let footnote = doc.footnotes()[i]
              index = Some(footnote.index.to_string())
              content = Some(footnote.text)
              type_ = Some("xref")
              target = Some(fid)
              id = None
            }
            None =>
              match content {
                Some(c) => {
                  let c = self.restore_passthroughs(
                    normalize_text(
                      c,
                      normalize_whitespace=true,
                      unescape_closing_square_brackets=true,
                    ),
                  )
                  content = Some(c)
                  let idx = doc.counter("footnote-number")
                  index = Some(idx)
                  doc.register_footnote({
                    index: @rb.to_i(idx),
                    id: Some(fid),
                    text: c,
                  })
                  type_ = Some("ref")
                }
                None => {
                  log_warn("invalid footnote reference: \{fid}")
                  type_ = Some("xref")
                  target = Some(fid)
                  content = Some(fid)
                  id = None
                }
              }
          }
        None =>
          match content {
            Some(c) => {
              let c = self.restore_passthroughs(
                normalize_text(
                  c,
                  normalize_whitespace=true,
                  unescape_closing_square_brackets=true,
                ),
              )
              content = Some(c)
              let idx = doc.counter("footnote-number")
              index = Some(idx)
              doc.register_footnote({ index: @rb.to_i(idx), id: None, text: c, })
            }
            None => return matched
          }
      }
      let attrs = Attributes::new()
      attrs.set(
        "index",
        match index {
          Some(i) =>
            match @rb.parse_int(i) {
              Some(n) => Int(n)
              None => Str(i)
            }
          None => Nil
        },
      )
      Node::new_inline(
        self,
        Footnote,
        text?=content,
        attributes=attrs,
        id?,
        target?,
        type_?,
      ).convert()
    })
  }
  text
}

///|
let inline_macro_rx_cache : Map[String, @regex.Regex] = Map([])

///|
/// Default regexp for an inline macro extension (Ruby `resolve_regexp`).
fn inline_macro_regexp(name : String, format : String?) -> @regex.Regex {
  let key = name + ":" + format.unwrap_or("long")
  match inline_macro_rx_cache.get(key) {
    Some(r) => r
    None => {
      // Ruby: /\\?#{name}:#{format == :short ? '(){0}' : '(\S+?)'}\[(|#{CC_ANY}*?[^\\])\]/
      let target_group = if format == Some("short") {
        "(){0}"
      } else {
        "(\\S+?)"
      }
      let r = @regex.re("\\\\?\{name}:\{target_group}\\[(|.*?[^\\\\])\\]")
      inline_macro_rx_cache[key] = r
      r
    }
  }
}

///|
fn Node::convert_indexterm(self : Node, m : @regex.MatchData) -> String {
  let matched = m.matched()
  let see_also_list = fn(see_also : String) -> AttrVal {
    if see_also.contains(",") {
      List(@rb.split(see_also, ",").map(@rb.lstrip))
    } else {
      List([see_also])
    }
  }
  match m.group(1) {
    Some("indexterm") => {
      if matched.has_prefix("\\") {
        return @rb.from(matched, 1)
      }
      let attrlist = normalize_text(
        m.at(2),
        normalize_whitespace=true,
        unescape_closing_square_brackets=true,
      )
      let attrs = if attrlist.contains("=") {
        let attrs = parse_attribute_list(attrlist, block=self)
        match attrs.pos_str(1) {
          Some(primary) => {
            let terms = [primary]
            match attrs.pos_str(2) {
              Some(secondary) => {
                terms.push(secondary)
                match attrs.pos_str(3) {
                  Some(tertiary) => terms.push(tertiary)
                  None => ()
                }
              }
              None => ()
            }
            attrs.set("terms", List(terms))
            match attrs.str("see-also") {
              Some(sa) => attrs.set("see-also", see_also_list(sa))
              None => ()
            }
            attrs
          }
          None => Attributes::from_array([("terms", Str(attrlist))])
        }
      } else {
        Attributes::from_array([("terms", List(split_simple_csv(attrlist)))])
      }
      Node::new_inline(self, Indexterm, attributes=attrs).convert()
    }
    Some(_) => {
      // indexterm2
      if matched.has_prefix("\\") {
        return @rb.from(matched, 1)
      }
      let mut term = normalize_text(
        m.at(2),
        normalize_whitespace=true,
        unescape_closing_square_brackets=true,
      )
      let mut attrs : Attributes? = None
      if term.contains("=") {
        let a = parse_attribute_list(term, block=self)
        match a.pos_str(1) {
          Some(t) => {
            term = t
            attrs = Some(a)
            match a.str("see-also") {
              Some(sa) => a.set("see-also", see_also_list(sa))
              None => ()
            }
          }
          None => ()
        }
      }
      Node::new_inline(
        self,
        Indexterm,
        text=term,
        attributes?=attrs,
        type_="visible",
      ).convert()
    }
    None => {
      let mut encl_text = m.at(3)
      let mut visible = true
      let mut before : String? = None
      let mut after = ""
      if matched.has_prefix("\\") {
        if encl_text.has_prefix("(") && encl_text.has_suffix(")") {
          encl_text = @rb.slice(encl_text, 1, encl_text.length() - 1)
          before = Some("(")
          after = ")"
        } else {
          return @rb.from(matched, 1)
        }
      } else if encl_text.has_prefix("(") {
        if encl_text.has_suffix(")") {
          encl_text = @rb.slice(encl_text, 1, encl_text.length() - 1)
          visible = false
        } else {
          encl_text = @rb.from(encl_text, 1)
          before = Some("(")
          after = ""
        }
      } else if encl_text.has_suffix(")") {
        encl_text = @rb.chop(encl_text)
        before = Some("")
        after = ")"
      }
      let subbed_term = if visible {
        let mut term = normalize_text(encl_text, normalize_whitespace=true)
        let mut attrs : Attributes? = None
        if term.contains(";&") {
          if term.contains(" >> ") {
            let (t, _, see) = @rb.partition(term, " >> ")
            term = t
            attrs = Some(Attributes::from_array([("see", Str(see))]))
          } else if term.contains(" &> ") {
            let parts = @rb.split(term, " &> ")
            term = parts.remove(0)
            attrs = Some(Attributes::from_array([("see-also", List(parts))]))
          }
        }
        Node::new_inline(
          self,
          Indexterm,
          text=term,
          attributes?=attrs,
          type_="visible",
        ).convert()
      } else {
        let attrs = Attributes::new()
        let mut terms = normalize_text(encl_text, normalize_whitespace=true)
        if terms.contains(";&") {
          if terms.contains(" >> ") {
            let (t, _, see) = @rb.partition(terms, " >> ")
            terms = t
            attrs.set_str("see", see)
          } else if terms.contains(" &> ") {
            let parts = @rb.split(terms, " &> ")
            terms = parts.remove(0)
            attrs.set("see-also", List(parts))
          }
        }
        attrs.set("terms", List(split_simple_csv(terms)))
        Node::new_inline(self, Indexterm, attributes=attrs).convert()
      }
      match before {
        Some(b) => "\{b}\{subbed_term}\{after}"
        None => subbed_term
      }
    }
  }
}

///|
fn Node::convert_inline_link(
  self : Node,
  m : @regex.MatchData,
  doc : Node,
  doc_attrs : Attributes,
) -> String {
  let matched = m.matched()
  let g1 = m.at(1)
  if m.has(2) && !m.has(5) {
    if g1.has_prefix("\\") {
      return @rb.from(matched, 1)
    }
    if m.at(3).has_prefix("\\") {
      return "\{g1}\{@rb.from(matched, g1.length() + 1)}"
    }
    guard m.group(6) is Some(g6) else { return matched }
    let target = m.at(3) + g6
    doc.register_link(target)
    let link_text = hide_uri_scheme(doc_attrs, target)
    return Node::new_inline(
      self,
      Anchor,
      text=link_text,
      type_="link",
      target~,
      attributes=Attributes::from_array([("role", Str("bare"))]),
    ).convert()
  }
  if m.at(3).has_prefix("\\") {
    return "\{g1}\{@rb.from(matched, g1.length() + 1)}"
  }
  let mut prefix = g1
  let mut target = m.at(3) +
    (match m.group(4) {
      Some(g4) => g4
      None => m.at(7)
    })
  let mut suffix = ""
  let mut link_text : String? = None
  match m.group(5) {
    Some(g5) => {
      if prefix == "link:" {
        prefix = ""
      }
      link_text = if g5 == "" { None } else { Some(g5) }
    }
    None => {
      match prefix {
        "link:" | "\"" | "'" => return matched
        _ => ()
      }
      match m.group(8) {
        Some(";") => {
          target = @rb.chop(target)
          if target.has_suffix(")") {
            target = @rb.chop(target)
            suffix = ");"
          } else {
            suffix = ";"
          }
          if target == m.at(3) {
            return matched
          }
        }
        Some(":") => {
          target = @rb.chop(target)
          if target.has_suffix(")") {
            target = @rb.chop(target)
            suffix = "):"
          } else {
            suffix = ":"
          }
          if target == m.at(3) {
            return matched
          }
        }
        _ => ()
      }
    }
  }
  let mut attrs : Attributes? = None
  let mut id : String? = None
  let mut bare = false
  match link_text {
    Some(lt0) => {
      let mut lt = lt0
      if lt.contains("]") {
        lt = lt.replace_all(old="\\]", new="]")
      }
      let mut new_link_text : String? = Some(lt)
      if !doc.compat_mode() && lt.contains("=") {
        let (t, a) = self.extract_attributes_from_text(
          lt,
          default_text=Some(""),
        )
        lt = t.unwrap_or("")
        new_link_text = Some(lt)
        attrs = Some(a)
        id = a.str("id")
      }
      if lt.has_suffix("^") {
        lt = @rb.chop(lt)
        new_link_text = Some(lt)
        match attrs {
          Some(a) => a.set_default("window", Str("_blank"))
          None =>
            attrs = Some(Attributes::from_array([("window", Str("_blank"))]))
        }
      }
      if new_link_text == Some("") {
        lt = hide_uri_scheme(doc_attrs, target)
        bare = true
      }
      link_text = Some(lt)
    }
    None => {
      link_text = Some(hide_uri_scheme(doc_attrs, target))
      bare = true
    }
  }
  if bare {
    match attrs {
      Some(a) =>
        a.set_str(
          "role",
          if a.contains("role") {
            "bare \{a.str("role").unwrap_or("")}"
          } else {
            "bare"
          },
        )
      None => attrs = Some(Attributes::from_array([("role", Str("bare"))]))
    }
  }
  doc.register_link(target)
  "\{prefix}\{Node::new_inline(self, Anchor, text?=link_text, type_="link", target~, id?=id, attributes?=attrs).convert()}\{suffix}"
}

///|
fn Node::convert_xref(self : Node, m : @regex.MatchData, doc : Node) -> String {
  let matched = m.matched()
  if matched.has_prefix("\\") {
    return @rb.from(matched, 1)
  }
  let mut attrs = Attributes::new()
  let mut refid = ""
  let mut link_text : String? = None
  let mut macro_ = false
  match m.group(1) {
    Some(r) => {
      refid = r
      if r.contains(",") {
        let (a, _, b) = @rb.partition(r, ",")
        refid = a
        let lt = @rb.lstrip(b)
        link_text = if lt == "" { None } else { Some(lt) }
      }
    }
    None => {
      macro_ = true
      refid = m.at(2)
      match m.group(3) {
        Some(lt0) => {
          let mut lt = lt0
          if lt.contains("]") {
            lt = lt.replace_all(old="\\]", new="]")
          }
          link_text = Some(lt)
          if !doc.compat_mode() && lt.contains("=") {
            let (t, a) = self.extract_attributes_from_text(lt)
            link_text = t
            attrs = a
          }
        }
        None => ()
      }
    }
  }
  let mut fragment : String? = None
  let mut path : String? = None
  let mut src2src : String? = None
  let mut target : String? = None
  if doc.compat_mode() {
    fragment = Some(refid)
  } else {
    match refid.find("#") {
      Some(hash_idx) if (if hash_idx == 0 {
          refid[refid.length() - 1]
        } else {
          refid[hash_idx - 1]
        }) !=
        '&' =>
        if hash_idx > 0 {
          let fragment_len = refid.length() - 1 - hash_idx
          if fragment_len > 0 {
            path = Some(@rb.slice(refid, 0, hash_idx))
            fragment = Some(@rb.slice(refid, hash_idx + 1, refid.length()))
          } else {
            path = Some(@rb.chop(refid))
          }
          let p = path.unwrap()
          if macro_ {
            if p.has_suffix(".adoc") {
              let p2 = @rb.slice(p, 0, p.length() - 5)
              path = Some(p2)
              src2src = Some(p2)
            } else if !has_extname(p) {
              src2src = Some(p)
            }
          } else if [".adoc", ".asciidoc", ".asc", ".ad", ".txt"]
            .iter()
            .any(e => p.has_suffix(e)) {
            let p2 = @rb.slice(p, 0, p.rev_find(".").unwrap())
            path = Some(p2)
            src2src = Some(p2)
          } else {
            src2src = Some(p)
          }
        } else {
          target = Some(refid)
          fragment = Some(@rb.from(refid, 1))
        }
      _ =>
        if macro_ {
          if refid.has_suffix(".adoc") {
            let p = @rb.slice(refid, 0, refid.length() - 5)
            path = Some(p)
            src2src = Some(p)
          } else if has_extname(refid) {
            path = Some(refid)
          } else {
            fragment = Some(refid)
          }
        } else {
          fragment = Some(refid)
        }
    }
  }
  let refs = doc.catalog().refs
  let mut refid_out : String? = None
  if target is Some(_) {
    refid_out = fragment
    if logger().is_info() && !refs.contains(fragment.unwrap_or("")) {
      log_info("possible invalid reference: \{fragment.unwrap_or("")}")
    }
  } else if path is Some(p) {
    if src2src is Some(_) &&
      (
        doc.attributes.str("docname") == Some(p) ||
        doc.catalog().includes.get(p) == Some(true)
      ) {
      match fragment {
        Some(f) => {
          refid_out = Some(f)
          path = None
          target = Some("#\{f}")
          if logger().is_info() && !refs.contains(f) {
            log_info("possible invalid reference: \{f}")
          }
        }
        None => {
          refid_out = None
          path = None
          target = Some("#")
        }
      }
    } else {
      let suffix = if src2src is Some(_) {
        match doc.attributes.get("relfilesuffix") {
          Some(v) => v.to_s()
          None => doc.outfilesuffix()
        }
      } else {
        ""
      }
      let full = "\{doc.attributes.str("relfileprefix").unwrap_or("")}\{p}\{suffix}"
      refid_out = Some(p)
      path = Some(full)
      match fragment {
        Some(f) => {
          refid_out = Some("\{p}#\{f}")
          target = Some("\{full}#\{f}")
        }
        None => target = Some(full)
      }
    }
  } else {
    let f = fragment.unwrap_or("")
    if doc.compat_mode() || !compliance.natural_xrefs {
      refid_out = Some(f)
      target = Some("#\{f}")
      if logger().is_info() && !refs.contains(f) {
        log_info("possible invalid reference: \{f}")
      }
    } else if refs.contains(f) {
      refid_out = Some(f)
      target = Some("#\{f}")
    } else {
      match
        (if f.contains(" ") || @rb.downcase(f) != f {
          doc.resolve_id(f)
        } else {
          None
        }) {
        Some(r) => {
          refid_out = Some(r)
          fragment = Some(r)
          target = Some("#\{r}")
        }
        None => {
          refid_out = Some(f)
          target = Some("#\{f}")
          if logger().is_info() {
            log_info("possible invalid reference: \{f}")
          }
        }
      }
    }
  }
  let opt = fn(v : String?) -> AttrVal {
    match v {
      Some(s) => Str(s)
      None => Nil
    }
  }
  attrs.set("path", opt(path))
  attrs.set("fragment", opt(fragment))
  attrs.set("refid", opt(refid_out))
  Node::new_inline(
    self,
    Anchor,
    text?=link_text,
    type_="xref",
    target?,
    attributes=attrs,
  ).convert()
}