# File lib/asciidoctor/substitutors.rb, line 533
  def sub_macros(source)
    return source if source.nil_or_empty?

    # some look ahead assertions to cut unnecessary regex calls
    found = {}
    found[:square_bracket] = source.include?('[')
    found[:round_bracket] = source.include?('(')
    found[:colon] = found_colon = source.include?(':')
    found[:macroish] = (found[:square_bracket] && found_colon)
    found[:macroish_short_form] = (found[:square_bracket] && found_colon && source.include?(':['))
    use_link_attrs = @document.attributes.has_key?('linkattrs')
    experimental = @document.attributes.has_key?('experimental')

    # NOTE interpolation is faster than String#dup
    result = %(#{source})

    if experimental
      if found[:macroish_short_form] && (result.include?('kbd:') || result.include?('btn:'))
        result = result.gsub(KbdBtnInlineMacroRx) {
          # alias match for Ruby 1.8.7 compat
          m = $~
          # honor the escape
          if (captured = m[0]).start_with? '\\'
            next captured[1..-1]
          end

          if captured.start_with?('kbd')
            keys = unescape_bracketed_text m[1]

            if keys == '+'
              keys = ['+']
            else
              # need to use closure to work around lack of negative lookbehind
              keys = keys.split(KbdDelimiterRx).inject([]) {|c, key|
                if key.end_with?('++')
                  c << key[0..-3].strip
                  c << '+'
                else
                  c << key.strip
                end
                c
              }
            end
            Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).convert
          elsif captured.start_with?('btn')
            label = unescape_bracketed_text m[1]
            Inline.new(self, :button, label).convert
          end
        }
      end

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

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

          if !items
            submenus = []
            menuitem = nil
          else
            if (delim = items.include?('&gt;') ? '&gt;' : (items.include?(',') ? ',' : nil))
              submenus = items.split(delim).map {|it| it.strip }
              menuitem = submenus.pop
            else
              submenus = []
              menuitem = items.rstrip
            end
          end

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

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

          input = m[1]

          menu, *submenus = input.split('&gt;').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? '\\'
            next m[0][1..-1]
          end

          target = m[1]
          attributes = if extension.config[:format] == :short
            {}
          else
            if extension.config[:content_model] == :attributes
              parse_attributes m[2], (extension.config[:pos_attrs] || []), :sub_input => true, :unescape_input => true
            else
              { 'text' => (unescape_bracketed_text m[2]) }
            end
          end
          extension.process_method[self, target, attributes]
        }
      end
    end

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

        raw_attrs = unescape_bracketed_text m[2]
        if m[0].start_with? 'icon:'
          type = 'icon'
          posattrs = ['size']
        else
          type = 'image'
          posattrs = ['alt', 'width', 'height']
        end
        target = sub_attributes(m[1])
        unless type == 'icon'
          @document.register(:images, target)
        end
        attrs = parse_attributes(raw_attrs, posattrs)
        attrs['alt'] ||= Helpers.basename(target, true).tr('_-', ' ')
        Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).convert
      }
    end

    if found[:macroish_short_form] || found[:round_bracket]
      # indexterm:[Tigers,Big cats]
      # (((Tigers,Big cats)))
      # indexterm2:[Tigers]
      # ((Tigers))
      result = result.gsub(IndextermInlineMacroRx) {
        # alias match for Ruby 1.8.7 compat
        m = $~

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

        # fix non-matching group results in Opal under Firefox
        if ::RUBY_ENGINE_OPAL
          m[1] = nil if m[1] == ''
        end

        num_brackets = 0
        text_in_brackets = nil
        unless (macro_name = m[1])
          text_in_brackets = m[3]
          if (text_in_brackets.start_with? '(') && (text_in_brackets.end_with? ')')
            text_in_brackets = text_in_brackets[1...-1]
            num_brackets = 3
          else
            num_brackets = 2
          end
        end

        # non-visible
        if macro_name == 'indexterm' || num_brackets == 3
          if !macro_name
            # (((Tigers,Big cats)))
            terms = split_simple_csv normalize_string(text_in_brackets)
          else
            # indexterm:[Tigers,Big cats]
            terms = split_simple_csv normalize_string(m[2], true)
          end
          @document.register(:indexterms, [*terms])
          Inline.new(self, :indexterm, nil, :attributes => {'terms' => terms}).convert
        # visible
        else
          if !macro_name
            # ((Tigers))
            text = normalize_string text_in_brackets
          else
            # indexterm2:[Tigers]
            text = normalize_string m[2], true
          end
          @document.register(:indexterms, [text])
          Inline.new(self, :indexterm, text, :type => :visible).convert
        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? '\\'
          # must enclose string following next in " for Opal
          next "#{m[1]}#{m[2][1..-1]}#{m[3]}"
        end
        # fix non-matching group results in Opal under Firefox
        if ::RUBY_ENGINE_OPAL
          m[3] = nil if m[3] == ''
        end
        # not a valid macro syntax w/o trailing square brackets
        # we probably shouldn't even get here...our regex is doing too much
        if m[1] == 'link:' && !m[3]
          next m[0]
        end
        prefix = (m[1] != 'link:' ? m[1] : '')
        target = m[2]
        suffix = ''
        unless m[3] || target !~ UriTerminator
          case $~[0]
          when ')'
            # strip the trailing )
            target = target[0..-2]
            suffix = ')'
          when ';'
            # strip the <> around the link
            if prefix.start_with?('&lt;') && target.end_with?('&gt;')
              prefix = prefix[4..-1]
              target = target[0..-5]
            # strip the ); from the end of the link
            elsif target.end_with?(');')
              target = target[0..-3]
              suffix = ');'
            else
              target = target[0..-2]
              suffix = ';'
            end
          when ':'
            # strip the ): from the end of the link
            if target.end_with?('):')
              target = target[0..-3]
              suffix = '):'
            else
              target = target[0..-2]
              suffix = ':'
            end
          end
        end
        @document.register(:links, target)

        link_opts = { :type => :link, :target => target }
        attrs = nil
        #text = m[3] ? sub_attributes(m[3].gsub('\]', ']')) : ''
        if m[3].nil_or_empty?
          text = ''
        else
          if use_link_attrs && (m[3].start_with?('"') || (m[3].include?(',') && m[3].include?('=')))
            attrs = parse_attributes(sub_attributes(m[3].gsub('\]', ']')), [])
            link_opts[:id] = (attrs.delete 'id') if attrs.has_key? 'id'
            text = attrs[1] || ''
          else
            text = sub_attributes(m[3].gsub('\]', ']'))
          end

          # TODO enable in Asciidoctor 1.5.1
          # support pipe-separated text and title
          #unless attrs && (attrs.has_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?
          if @document.attr? 'hide-uri-scheme'
            text = target.sub UriSniffRx, ''
          else
            text = target
          end

          if attrs
            attrs['role'] = %(bare #{attrs['role']}).chomp ' '
          else
            attrs = {'role' => 'bare'}
          end
        end

        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(LinkInlineMacroRx) {
        # alias match for Ruby 1.8.7 compat
        m = $~
        # honor the escape
        if m[0].start_with? '\\'
          next m[0][1..-1]
        end
        raw_target = m[1]
        mailto = m[0].start_with?('mailto:')
        target = mailto ? %(mailto#{raw_target}) : raw_target#{raw_target}) : raw_target

        link_opts = { :type => :link, :target => target }
        attrs = nil
        #text = sub_attributes(m[2].gsub('\]', ']'))
        text = if use_link_attrs && (m[2].start_with?('"') || m[2].include?(','))
          attrs = parse_attributes(sub_attributes(m[2].gsub('\]', ']')), [])
          link_opts[:id] = (attrs.delete 'id') if attrs.key? 'id'
          if mailto
            if attrs.key? 2
              target = link_opts[:target] = "#{target}?subject=#{Helpers.encode_uri(attrs[2])}"

              if attrs.key? 3
                target = link_opts[:target] = "#{target}&amp;body=#{Helpers.encode_uri(attrs[3])}"
              end
            end
          end
          attrs[1]
        else
          sub_attributes(m[2].gsub('\]', ']'))
        end

        # QUESTION should a mailto be registered as an e-mail address?
        @document.register(:links, target)

        # TODO enable in Asciidoctor 1.5.1
        # 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

        if text.empty?
          # mailto is a special case, already processed
          if mailto
            text = raw_target
          else
            if @document.attr? 'hide-uri-scheme'
              text = raw_target.sub UriSniffRx, ''
            else
              text = raw_target
            end

            if attrs
              attrs['role'] = %(bare #{attrs['role']}).chomp ' '
            else
              attrs = {'role' => 'bare'}
            end
          end
        end

        link_opts[:attributes] = attrs if attrs
        Inline.new(self, :anchor, text, link_opts).convert
      }
    end

    if result.include? '@'
      result = result.gsub(EmailInlineMacroRx) {
        # alias match for Ruby 1.8.7 compat
        m = $~
        address = m[0]
        if (lead = m[1])
          case lead
          when '\\'
            next address[1..-1]
          else
            next address
          end
        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_form] && result.include?('footnote')
      result = result.gsub(FootnoteInlineMacroRx) {
        # alias match for Ruby 1.8.7 compat
        m = $~
        # honor the escape
        if m[0].start_with? '\\'
          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
          # NOTE In Opal, text is set to empty string if comma is missing
          if text.nil_or_empty?
            if (footnote = @document.references[:footnotes].find {|fn| fn.id == id })
              index = footnote.index
              text = footnote.text
            else
              index = nil
              text = id
            end
            target = id
            id = nil
            type = :xref
          else
            # 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
          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