# File lib/asciidoctor/substitutors.rb, line 524
  def sub_macros(source)
    #return source if source.nil_or_empty?
    # some look ahead assertions to cut unnecessary regex calls
    found = {}
    found_square_bracket = found[:square_bracket] = (source.include? '[')
    found_colon = source.include? ':'
    found_macroish = found[:macroish] = found_square_bracket && found_colon
    found_macroish_short = found_macroish && (source.include? ':[')
    doc_attrs = @document.attributes
    use_link_attrs = doc_attrs.key? 'linkattrs'
    result = source

    if doc_attrs.key? 'experimental'
      if found_macroish_short && ((result.include? 'kbd:') || (result.include? 'btn:'))
        result = result.gsub(InlineKbdBtnMacroRx) {
          # honor the escape
          if $1
            $&.slice 1, $&.length
          elsif $2 == 'kbd'
            if (keys = $3.strip).include? R_SB
              keys = keys.gsub ESC_R_SB, R_SB
            end
            if keys.length > 1 && (delim_idx = (delim_idx = keys.index ',', 1) ?
                [delim_idx, (keys.index '+', 1)].compact.min : (keys.index '+', 1))
              delim = keys.slice delim_idx, 1
              # NOTE handle special case where keys ends with delimiter (e.g., Ctrl++ or Ctrl,,)
              if keys.end_with? delim
                keys = (keys.chop.split delim, -1).map {|key| key.strip }
                keys[-1] = %(#{keys[-1]}#{delim})
              else
                keys = keys.split(delim).map {|key| key.strip }
              end
            else
              keys = [keys]
            end
            (Inline.new self, :kbd, nil, :attributes => { 'keys' => keys }).convert
          else # $2 == 'btn'
            (Inline.new self, :button, (unescape_bracketed_text $3)).convert
          end
        }
      end

      if found_macroish && (result.include? 'menu:')
        result = result.gsub(InlineMenuMacroRx) {
          # alias match for Ruby 1.8.7 compat
          m = $~
          # honor the escape
          if (captured = m[0]).start_with? RS
            next captured[1..-1]
          end

          menu, items = m[1], m[2]

          if items
            items = items.gsub ESC_R_SB, R_SB if items.include? R_SB
            if (delim = items.include?('>') ? '>' : (items.include?(',') ? ',' : nil))
              submenus = items.split(delim).map {|it| it.strip }
              menuitem = submenus.pop
            else
              submenus, menuitem = [], items.rstrip
            end
          else
            submenus, menuitem = [], nil
          end

          Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).convert
        }
      end

      if (result.include? '"') && (result.include? '>')
        result = result.gsub(MenuInlineRx) {
          # alias match for Ruby 1.8.7 compat
          m = $~
          # honor the escape
          if (captured = m[0]).start_with? RS
            next captured[1..-1]
          end

          input = m[1]

          menu, *submenus = input.split('>').map {|it| it.strip }
          menuitem = submenus.pop
          Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).convert
        }
      end
    end

    # FIXME this location is somewhat arbitrary, probably need to be able to control ordering
    # TODO this handling needs some cleanup
    if (extensions = @document.extensions) && extensions.inline_macros? # && found_macroish
      extensions.inline_macros.each do |extension|
        result = result.gsub(extension.instance.regexp) {
          # alias match for Ruby 1.8.7 compat
          m = $~
          # honor the escape
          if m[0].start_with? RS
            next m[0][1..-1]
          end

          if (m.names rescue []).empty?
            target, content, extconf = m[1], m[2], extension.config
          else
            target, content, extconf = (m[:target] rescue nil), (m[:content] rescue nil), extension.config
          end
          attributes = (attributes = extconf[:default_attrs]) ? attributes.dup : {}
          if content.nil_or_empty?
            attributes['text'] = content if content && extconf[:content_model] != :attributes
          else
            content = unescape_bracketed_text content
            if extconf[:content_model] == :attributes
              # QUESTION should we store the text in the _text key?
              # QUESTION why is the sub_result option false? why isn't the unescape_input option true?
              parse_attributes content, extconf[:pos_attrs] || [], :sub_result => false, :into => attributes
            else
              attributes['text'] = content
            end
          end
          # NOTE use content if target is not set (short form only); deprecated - remove in 1.6.0
          replacement = extension.process_method[self, target || content, attributes]
          Inline === replacement ? replacement.convert : replacement
        }
      end
    end

    if found_macroish && ((result.include? 'image:') || (result.include? 'icon:'))
      # image:filename.png[Alt Text]
      result = result.gsub(InlineImageMacroRx) {
        # alias match for Ruby 1.8.7 compat
        m = $~
        # honor the escape
        if (captured = $&).start_with? RS
          next captured[1..-1]
        end

        if captured.start_with? 'icon:'
          type, posattrs = 'icon', ['size']
        else
          type, posattrs = 'image', ['alt', 'width', 'height']
        end
        if (target = m[1]).include? ATTR_REF_HEAD
          # TODO remove this special case once titles use normal substitution order
          target = sub_attributes target
        end
        @document.register(:images, target) unless type == 'icon'
        attrs = parse_attributes(m[2], posattrs, :unescape_input => true)
        attrs['alt'] ||= (attrs['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
        Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).convert
      }
    end

    if ((result.include? '((') && (result.include? '))')) ||
        (found_macroish_short && (result.include? 'indexterm'))
      # (((Tigers,Big cats)))
      # indexterm:[Tigers,Big cats]
      # ((Tigers))
      # indexterm2:[Tigers]
      result = result.gsub(InlineIndextermMacroRx) {
        # alias match for Ruby 1.8.7 compat
        m = $~

        # honor the escape
        if m[0].start_with? RS
          next m[0][1..-1]
        end

        case m[1]
        when 'indexterm'
          # indexterm:[Tigers,Big cats]
          terms = split_simple_csv(normalize_string m[2], true)
          @document.register :indexterms, terms
          (Inline.new self, :indexterm, nil, :attributes => { 'terms' => terms }).convert
        when 'indexterm2'
          # indexterm2:[Tigers]
          term = normalize_string m[2], true
          @document.register :indexterms, [term]
          (Inline.new self, :indexterm, term, :type => :visible).convert
        else
          text, visible, before, after = m[3], true, nil, nil
          if text.start_with? '('
            if text.end_with? ')'
              text, visible = (text.slice 1, text.length - 2), false
            else
              text, before, after = (text.slice 1, text.length - 1), '(', ''
            end
          elsif text.end_with? ')'
            if text.start_with? '('
              text, visible = (text.slice 1, text.length - 2), false
            else
              text, before, after = (text.slice 0, text.length - 1), '', ')'
            end
          end
          if visible
            # ((Tigers))
            term = normalize_string text
            @document.register :indexterms, [term]
            result = (Inline.new self, :indexterm, term, :type => :visible).convert
          else
            # (((Tigers,Big cats)))
            terms = split_simple_csv(normalize_string text)
            @document.register :indexterms, terms
            result = (Inline.new self, :indexterm, nil, :attributes => { 'terms' => terms }).convert
          end
          before ? %(#{before}#{result}#{after}) : result
        end
      }
    end

    if found_colon && (result.include? '://')
      # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>)
      result = result.gsub(LinkInlineRx) {
        # alias match for Ruby 1.8.7 compat
        m = $~
        # honor the escape
        if m[2].start_with? RS
          next %(#{m[1]}#{m[2][1..-1]}#{m[3]})
        end
        # NOTE if text is non-nil, then we've matched a formal macro (i.e., trailing square brackets)
        prefix, target, text, suffix = m[1], m[2], (macro = m[3]) || '', ''
        if prefix == 'link:'
          if macro
            prefix = ''
          else
            # invalid macro syntax (link: prefix w/o trailing square brackets)
            # we probably shouldn't even get here...our regex is doing too much
            next m[0]
          end
        end
        unless macro || UriTerminatorRx !~ target
          case $&
          when ')'
            # strip trailing )
            target = target.chop
            suffix = ')'
          when ';'
            # strip <> around URI
            if prefix.start_with?('&lt;') && target.end_with?('&gt;')
              prefix = prefix[4..-1]
              target = target[0...-4]
            else
              # strip trailing ;
              # check for trailing );
              if (target = target.chop).end_with?(')')
                target = target.chop
                suffix = ');'
              else
                suffix = ';'
              end
            end
          when ':'
            # strip trailing :
            # check for trailing ):
            if (target = target.chop).end_with?(')')
              target = target.chop
              suffix = '):'
            else
              suffix = ':'
            end
          end
        end

        attrs, link_opts = nil, { :type => :link }
        unless text.empty?
          text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
          if use_link_attrs && ((text.start_with? '"') || ((text.include? ',') && (text.include? '=')))
            attrs = parse_attributes text, []
            link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
            text = attrs[1] || ''
          end

          # TODO enable in Asciidoctor 1.6.x
          # support pipe-separated text and title
          #unless attrs && (attrs.key? 'title')
          #  if text.include? '|'
          #    attrs ||= {}
          #    text, attrs['title'] = text.split '|', 2
          #  end
          #end

          if text.end_with? '^'
            text = text.chop
            if attrs
              attrs['window'] ||= '_blank'
            else
              attrs = { 'window' => '_blank' }
            end
          end
        end

        if text.empty?
          text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
          if attrs
            attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
          else
            attrs = { 'role' => 'bare' }
          end
        end

        @document.register :links, (link_opts[:target] = target)
        link_opts[:attributes] = attrs if attrs
        %(#{prefix}#{Inline.new(self, :anchor, text, link_opts).convert}#{suffix})
      }
    end

    if found_macroish && ((result.include? 'link:') || (result.include? 'mailto:'))
      # inline link macros, link:target[text]
      result = result.gsub(InlineLinkMacroRx) {
        # alias match for Ruby 1.8.7 compat
        m = $~
        # honor the escape
        if m[0].start_with? RS
          next m[0][1..-1]
        end
        target = (mailto = m[1]) ? %(mailto:#{m[2]}) : m[2]
        attrs, link_opts = nil, { :type => :link }
        unless (text = m[3]).empty?
          text = text.gsub ESC_R_SB, R_SB if text.include? R_SB
          if use_link_attrs && ((text.start_with? '"') || ((text.include? ',') && (mailto || (text.include? '='))))
            attrs = parse_attributes text, []
            link_opts[:id] = attrs.delete 'id' if attrs.key? 'id'
            if mailto
              if attrs.key? 2
                if attrs.key? 3
                  target = %(#{target}?subject=#{Helpers.uri_encode attrs[2]}&amp;body=#{Helpers.uri_encode attrs[3]})
                else
                  target = %(#{target}?subject=#{Helpers.uri_encode attrs[2]})
                end
              end
            end
            text = attrs[1] || ''
          end

          # TODO enable in Asciidoctor 1.6.x
          # support pipe-separated text and title
          #unless attrs && (attrs.key? 'title')
          #  if text.include? '|'
          #    attrs ||= {}
          #    text, attrs['title'] = text.split '|', 2
          #  end
          #end

          if text.end_with? '^'
            text = text.chop
            if attrs
              attrs['window'] ||= '_blank'
            else
              attrs = { 'window' => '_blank' }
            end
          end
        end

        if text.empty?
          # mailto is a special case, already processed
          if mailto
            text = m[2]
          else
            text = (doc_attrs.key? 'hide-uri-scheme') ? (target.sub UriSniffRx, '') : target
            if attrs
              attrs['role'] = (attrs.key? 'role') ? %(bare #{attrs['role']}) : 'bare'
            else
              attrs = { 'role' => 'bare' }
            end
          end
        end

        # QUESTION should a mailto be registered as an e-mail address?
        @document.register :links, (link_opts[:target] = target)
        link_opts[:attributes] = attrs if attrs
        Inline.new(self, :anchor, text, link_opts).convert
      }
    end

    if result.include? '@'
      result = result.gsub(EmailInlineRx) {
        address, tip = $&, $1
        if tip
          next (tip == RS ? address[1..-1] : address)
        end

        target = %(mailto:#{address})
        # QUESTION should this be registered as an e-mail address?
        @document.register(:links, target)

        Inline.new(self, :anchor, address, :type => :link, :target => target).convert
      }
    end

    if found_macroish_short && (result.include? 'footnote')
      result = result.gsub(InlineFootnoteMacroRx) {
        # alias match for Ruby 1.8.7 compat
        m = $~
        # honor the escape
        if m[0].start_with? RS
          next m[0][1..-1]
        end
        if m[1] == 'footnote'
          id = nil
          # REVIEW it's a dirty job, but somebody's gotta do it
          text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string m[2], true)), false)
          index = @document.counter('footnote-number')
          @document.register(:footnotes, Document::Footnote.new(index, id, text))
          type = nil
          target = nil
        else
          id, text = m[2].split(',', 2)
          id = id.strip
          if text
            # REVIEW it's a dirty job, but somebody's gotta do it
            text = restore_passthroughs(sub_inline_xrefs(sub_inline_anchors(normalize_string text, true)), false)
            index = @document.counter('footnote-number')
            @document.register(:footnotes, Document::Footnote.new(index, id, text))
            type = :ref
            target = nil
          else
            if (footnote = @document.footnotes.find {|fn| fn.id == id })
              index = footnote.index
              text = footnote.text
            else
              index = nil
              text = id
            end
            target = id
            id = nil
            type = :xref
          end
        end
        Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).convert
      }
    end

    sub_inline_xrefs(sub_inline_anchors(result, found), found)
  end