module Asciidoctor::Substitutors

def apply_header_subs(text)

returns - A String with header substitutions performed

text - String containing the text process

Public: Apply substitutions for header metadata and attribute assignments
def apply_header_subs(text)
  apply_subs text, SUBS[:header]
end

def apply_normal_subs(lines)

returns - A String with normal substitutions performed

lines - The lines of text to process. Can be a String or a String Array

Public: Apply normal substitutions.
def apply_normal_subs(lines)
  apply_subs lines.is_a?(::Array) ? (lines * EOL) : lines
end

def apply_subs source, subs = :normal, expand = false

returns Either a String or String Array, whichever matches the type of the first argument

expand - A Boolean to control whether sub aliases are expanded (default: true)
subs - The substitutions to perform. Can be a Symbol or a Symbol Array (default: :normal)
source - The String or String Array of text to process

Public: Apply the specified substitutions to the lines of text
def apply_subs source, subs = :normal, expand = false
  if !subs
    return source
  elsif subs == :normal
    subs = SUBS[:normal]
  elsif expand
    if subs.is_a? ::Symbol
      subs = COMPOSITE_SUBS[subs] || [subs]
    else
      effective_subs = []
      subs.each do |key|
        if COMPOSITE_SUBS.has_key? key
          effective_subs += COMPOSITE_SUBS[key]
        else
          effective_subs << key
        end
      end
      subs = effective_subs
    end
  end
  return source if subs.empty?
  text = (multiline = source.is_a? ::Array) ? (source * EOL) : source
  if (has_passthroughs = subs.include? :macros)
    text = extract_passthroughs text
    has_passthroughs = false if @passthroughs.empty?
  end
  subs.each do |type|
    case type
    when :specialcharacters
      text = sub_specialcharacters text
    when :quotes
      text = sub_quotes text
    when :attributes
      text = sub_attributes(text.split EOL) * EOL
    when :replacements
      text = sub_replacements text
    when :macros
      text = sub_macros text
    when :highlight
      text = highlight_source text, (subs.include? :callouts)
    when :callouts
      text = sub_callouts text unless subs.include? :highlight
    when :post_replacements
      text = sub_post_replacements text
    else
      warn %(asciidoctor: WARNING: unknown substitution type #{type})
    end
  end
  text = restore_passthroughs text if has_passthroughs
  multiline ? (text.split EOL) : text
end

def apply_title_subs(title)

returns - A String with title substitutions performed

title - The String title to process

Public: Apply substitutions for titles.
def apply_title_subs(title)
  apply_subs title, SUBS[:title]
end

def convert_quoted_text(match, type, scope)

Returns The converted String text for the quoted text region

scope - The scope of the quoting (constrained or unconstrained)
type - The quoting type (single, double, strong, emphasis, monospaced, etc)
match - The MatchData for the quoted text region

Internal: Convert a quoted text region
def convert_quoted_text(match, type, scope)
  unescaped_attrs = nil
  if match[0].start_with? '\\'
    if scope == :constrained && !(attrs = match[2]).nil_or_empty?
      unescaped_attrs = %([#{attrs}])
    else
      return match[0][1..-1]
    end
  end
  if scope == :constrained
    if unescaped_attrs
      %(#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], :type => type).convert})
    else
      if (attributes = parse_quoted_text_attributes(match[2]))
        id = attributes.delete 'id'
        type = :unquoted if type == :mark
      else
        id = nil
      end
      %(#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :id => id, :attributes => attributes).convert})
    end
  else
    if (attributes = parse_quoted_text_attributes(match[1]))
      id = attributes.delete 'id'
      type = :unquoted if type == :mark
    else
      id = nil
    end
    Inline.new(self, :quoted, match[2], :type => type, :id => id, :attributes => attributes).convert
  end
end

def do_replacement m, replacement, restore

returns The String text with the replacement characters substituted

Internal: Substitute replacement text for matched location
def do_replacement m, replacement, restore
  if (matched = m[0]).include? '\\'
    matched.tr '\\', ''
  else
    case restore
    when :none
      replacement
    when :leading
      %(#{m[1]}#{replacement})
    when :bounding
      %(#{m[1]}#{replacement}#{m[2]})
    end
  end
end

def extract_passthroughs(text)

returns - The text with the passthrough region substituted with placeholders

text - The String from which to extract passthrough fragements

Internal: Extract the passthrough text from the document for reinsertion after processing.
def extract_passthroughs(text)
  compat_mode = @document.compat_mode
  text = text.gsub(PassInlineMacroRx) {
    # alias match for Ruby 1.8.7 compat
    m = $~
    preceding = nil
    if (boundary = m[4]).nil_or_empty? # pass:[]
      if m[6] == '\\'
        # NOTE we don't look for nested pass:[] macros
        next m[0][1..-1]
      end
      @passthroughs[pass_key = @passthroughs.size] = {:text => (unescape_brackets m[8]), :subs => (m[7].nil_or_empty? ? [] : (resolve_pass_subs m[7]))}
    else # $$, ++ or +++
      # skip ++ in compat mode, handled as normal quoted text
      if compat_mode && boundary == '++'
        next m[2].nil_or_empty? ?
            %(#{m[1]}#{m[3]}++#{extract_passthroughs m[5]}++) :
            %(#{m[1]}[#{m[2]}]#{m[3]}++#{extract_passthroughs m[5]}++)
      end
      attributes = m[2]
      # fix non-matching group results in Opal under Firefox
      if ::RUBY_ENGINE_OPAL
        attributes = nil if attributes == ''
      end
      escape_count = m[3].size
      content = m[5]
      old_behavior = false
      if attributes
        if escape_count > 0
          # NOTE we don't look for nested unconstrained pass macros
          # must enclose string following next in " for Opal
          next "#{m[1]}[#{attributes}]#{'\\' * (escape_count - 1)}#{boundary}#{m[5]}#{boundary})"
        elsif m[1] == '\\'
          preceding = %([#{attributes}])
          attributes = nil
        else
          if boundary == '++' && (attributes.end_with? 'x-')
            old_behavior = true
            attributes = attributes[0...-2]
          end
          attributes = parse_attributes attributes
        end
      elsif escape_count > 0
        # NOTE we don't look for nested unconstrained pass macros
        # must enclose string following next in " for Opal
        next "#{m[1]}[#{attributes}]#{'\\' * (escape_count - 1)}#{boundary}#{m[5]}#{boundary}"
      end
      subs = (boundary == '+++' ? [] : [:specialcharacters])
      pass_key = @passthroughs.size
      if attributes
        if old_behavior
          @passthroughs[pass_key] = {:text => content, :subs => SUBS[:normal], :type => :monospaced, :attributes => attributes}
        else
          @passthroughs[pass_key] = {:text => content, :subs => subs, :type => :unquoted, :attributes => attributes}
        end
      else
        @passthroughs[pass_key] = {:text => content, :subs => subs}
      end
    end
    %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END})
  } if (text.include? '++') || (text.include? '$$') || (text.include? 'ss:')
  pass_inline_char1, pass_inline_char2, pass_inline_rx = PassInlineRx[compat_mode]
  text = text.gsub(pass_inline_rx) {
    # alias match for Ruby 1.8.7 compat
    m = $~
    preceding = m[1]
    attributes = m[2]
    escape_mark = (m[3].start_with? '\\') ? '\\' : nil
    format_mark = m[4]
    content = m[5]
    # fix non-matching group results in Opal under Firefox
    if ::RUBY_ENGINE_OPAL
      attributes = nil if attributes == ''
    end
    if compat_mode
      old_behavior = true
    else
      if (old_behavior = (attributes && (attributes.end_with? 'x-')))
        attributes = attributes[0...-2]
      end
    end
    if attributes
      if format_mark == '`' && !old_behavior
        # must enclose string following next in " for Opal
        next "#{preceding}[#{attributes}]#{escape_mark}`#{extract_passthroughs content}`"
      end
      if escape_mark
        # honor the escape of the formatting mark (must enclose string following next in " for Opal)
        next "#{preceding}[#{attributes}]#{m[3][1..-1]}"
      elsif preceding == '\\'
        # honor the escape of the attributes
        preceding = %([#{attributes}])
        attributes = nil
      else
        attributes = parse_attributes attributes
      end
    elsif format_mark == '`' && !old_behavior
      # must enclose string following next in " for Opal
      next "#{preceding}#{escape_mark}`#{extract_passthroughs content}`"
    elsif escape_mark
      # honor the escape of the formatting mark (must enclose string following next in " for Opal)
      next "#{preceding}#{m[3][1..-1]}"
    end
    pass_key = @passthroughs.size
    if compat_mode
      @passthroughs[pass_key] = {:text => content, :subs => [:specialcharacters], :attributes => attributes, :type => :monospaced}
    elsif attributes
      if old_behavior
        subs = (format_mark == '`' ? [:specialcharacters] : SUBS[:normal])
        @passthroughs[pass_key] = {:text => content, :subs => subs, :attributes => attributes, :type => :monospaced}
      else
        @passthroughs[pass_key] = {:text => content, :subs => [:specialcharacters], :attributes => attributes, :type => :unquoted}
      end
    else
      @passthroughs[pass_key] = {:text => content, :subs => [:specialcharacters]}
    end
    %(#{preceding}#{PASS_START}#{pass_key}#{PASS_END})
  } if (text.include? pass_inline_char1) || (pass_inline_char2 && (text.include? pass_inline_char2))
  # NOTE we need to do the stem in a subsequent step to allow it to be escaped by the former
  text = text.gsub(StemInlineMacroRx) {
    # alias match for Ruby 1.8.7 compat
    m = $~
    # honor the escape
    if m[0].start_with? '\\'
      next m[0][1..-1]
    end
    if (type = m[1].to_sym) == :stem
      type = ((default_stem_type = document.attributes['stem']).nil_or_empty? ? 'asciimath' : default_stem_type).to_sym
    end
    content = unescape_brackets m[3]
    if m[2].nil_or_empty?
      subs = (@document.basebackend? 'html') ? [:specialcharacters] : []
    else
      subs = resolve_pass_subs m[2]
    end
    @passthroughs[pass_key = @passthroughs.size] = {:text => content, :subs => subs, :type => type}
    %(#{PASS_START}#{pass_key}#{PASS_END})
  } if (text.include? ':') && ((text.include? 'stem:') || (text.include? 'math:'))
  text
end

def highlight_source(source, process_callouts, highlighter = nil)

on the document, otherwise the unprocessed text
returns the highlighted source code, if a source highlighter is defined

process_callouts - a Boolean flag indicating whether callout marks should be substituted
source - the source code String to highlight

incorrectly processed by the source highlighter.
highlighter, then later restored in converted form, so they are not
Callout marks are stripped from the source prior to passing it to the

on the document, otherwise return the text unprocessed
Public: Highlight the source code if a source highlighter is defined
def highlight_source(source, process_callouts, highlighter = nil)
  highlighter ||= @document.attributes['source-highlighter']
  Helpers.require_library highlighter, (highlighter == 'pygments' ? 'pygments.rb' : highlighter)
  lineno = 0
  callout_on_last = false
  if process_callouts
    callout_marks = {}
    last = -1
    # FIXME cache this dynamic regex
    callout_rx = (attr? 'line-comment') ? /(?:#{::Regexp.escape(attr 'line-comment')} )?#{CalloutExtractRxt}/ : CalloutExtractRx
    # extract callout marks, indexed by line number
    source = source.split(EOL).map {|line|
      lineno = lineno + 1
      line.gsub(callout_rx) {
        # alias match for Ruby 1.8.7 compat
        m = $~
        # honor the escape
        if m[1] == '\\'
          m[0].sub('\\', '')
        else
          (callout_marks[lineno] ||= []) << m[3]
          last = lineno
          nil
        end
      }
    } * EOL
    callout_on_last = (last == lineno)
    callout_marks = nil if callout_marks.empty?
  else
    callout_marks = nil
  end
  linenums_mode = nil
  case highlighter
  when 'coderay'
    result = ::CodeRay::Duo[attr('language', :text, false).to_sym, :html, {
        :css => (@document.attributes['coderay-css'] || :class).to_sym,
        :line_numbers => (linenums_mode = ((attr? 'linenums') ? (@document.attributes['coderay-linenums-mode'] || :table).to_sym : nil)),
        :line_number_anchors => false}].highlight source
  when 'pygments'
    lexer = ::Pygments::Lexer[attr('language', nil, false)] || ::Pygments::Lexer['text']
    opts = { :cssclass => 'pyhl', :classprefix => 'tok-', :nobackground => true }
    unless (@document.attributes['pygments-css'] || 'class') == 'class'
      opts[:noclasses] = true
      opts[:style] = (@document.attributes['pygments-style'] || Stylesheets::DEFAULT_PYGMENTS_STYLE)
    end
    if attr? 'linenums'
      # TODO we could add the line numbers in ourselves instead of having to strip out the junk
      # FIXME move these regular expressions into constants
      if (opts[:linenos] = @document.attributes['pygments-linenums-mode'] || 'table') == 'table'
        linenums_mode = :table
        # NOTE these subs clean out HTML that messes up our styles
        result = lexer.highlight(source, :options => opts).
            sub(/<div class="pyhl">(.*)<\/div>/m, '\1').
            gsub(/<pre[^>]*>(.*?)<\/pre>\s*/m, '\1')
      else
        result = lexer.highlight(source, :options => opts).
            sub(/<div class="pyhl"><pre[^>]*>(.*?)<\/pre><\/div>/m, '\1')
      end
    else
      # nowrap gives us just the highlighted source; won't work when we need linenums though
      opts[:nowrap] = true
      result = lexer.highlight(source, :options => opts)
    end
  end
  # fix passthrough placeholders that got caught up in syntax highlighting
  unless @passthroughs.empty?
    result = result.gsub PASS_MATCH_HI, %(#{PASS_START}\\1#{PASS_END})
  end
  if process_callouts && callout_marks
    lineno = 0
    reached_code = linenums_mode != :table
    result.split(EOL).map {|line|
      unless reached_code
        unless line.include?('<td class="code">')
          next line
        end
        reached_code = true
      end
      lineno = lineno + 1
      if (conums = callout_marks.delete(lineno))
        tail = nil
        if callout_on_last && callout_marks.empty?
          # QUESTION when does this happen?
          if (pos = line.index '</pre>')
            tail = line[pos..-1]
            line = %(#{line[0...pos].chomp ' '} )
          else
            # Give conum on final line breathing room if trailing space in source is dropped
            line = %(#{line.chomp ' '} )
          end
        end
        if conums.size == 1
          %(#{line}#{Inline.new(self, :callout, conums[0], :id => @document.callouts.read_next_id).convert }#{tail})
        else
          conums_markup = conums.map {|conum| Inline.new(self, :callout, conum, :id => @document.callouts.read_next_id).convert } * ' '
          %(#{line}#{conums_markup}#{tail})
        end
      else
        line
      end
    } * EOL
  else
    result
  end
end

def lock_in_subs

Returns nothing

content model of the block.
Otherwise, assigns a set of default substitutions based on the
substitutions and assigns it to the subs property on this block.
Looks for an attribute named "subs". If present, resolves the

Internal: Lock-in the substitutions for this block
def lock_in_subs
  if @default_subs
    default_subs = @default_subs
  else
    case @content_model
    when :simple
      default_subs = SUBS[:normal]
    when :verbatim
      if @context == :listing || (@context == :literal && !(option? 'listparagraph'))
        default_subs = SUBS[:verbatim]
      elsif @context == :verse
        default_subs = SUBS[:normal]
      else
        default_subs = SUBS[:basic]
      end
    when :raw
      if @context == :stem
        default_subs = SUBS[:basic]
      else
        default_subs = SUBS[:pass]
      end
    else
      return
    end
  end
  if (custom_subs = @attributes['subs'])
    @subs = resolve_block_subs custom_subs, default_subs, @context
  else
    @subs = default_subs.dup
  end
  # QUESION delegate this logic to a method?
  if @context == :listing && @style == 'source' && @attributes['language'] &&
      @document.basebackend?('html') && SUB_HIGHLIGHT.include?(@document.attributes['source-highlighter'])
    @subs = @subs.map {|sub| sub == :specialcharacters ? :highlight : sub }
  end
end

def normalize_string str, unescape_brackets = false

Internal: Strip bounding whitespace and fold endlines
def normalize_string str, unescape_brackets = false
  if str.empty?
    ''
  elsif unescape_brackets
    unescape_brackets str.strip.tr(EOL, ' ')
  else
    str.strip.tr(EOL, ' ')
  end
end

def parse_attributes(attrline, posattrs = ['role'], opts = {})

returns nil if attrline is nil, an empty Hash if attrline is empty, otherwise a Hash of parsed attributes

posattrs - The keys for positional attributes
attrline - A String of unprocessed attributes (key/value pairs)

Internal: Parse the attributes in the attribute line
def parse_attributes(attrline, posattrs = ['role'], opts = {})
  return unless attrline
  return {} if attrline.empty?
  attrline = @document.sub_attributes(attrline) if opts[:sub_input]
  attrline = unescape_bracketed_text(attrline) if opts[:unescape_input]
  block = nil
  if opts.fetch(:sub_result, true)
    # substitutions are only performed on attribute values if block is not nil
    block = self
  end
  if (into = opts[:into])
    AttributeList.new(attrline, block).parse_into(into, posattrs)
  else
    AttributeList.new(attrline, block).parse(posattrs)
  end
end

def parse_quoted_text_attributes(str)

returns nil if str is nil, an empty Hash if str is empty, otherwise a Hash of attributes (role and id only)

str - A String of unprocessed attributes (space-separated roles or the id/role shorthand syntax)

Internal: Parse the attributes that are defined on quoted text
def parse_quoted_text_attributes(str)
  return unless str
  return {} if str.empty?
  str = sub_attributes(str) if str.include?('{')
  str = str.strip
  # for compliance, only consider first positional attribute
  str, _ = str.split(',', 2) if str.include?(',')
  if str.empty?
    {}
  elsif (str.start_with?('.') || str.start_with?('#')) && Compliance.shorthand_property_syntax
    segments = str.split('#', 2)
    if segments.length > 1
      id, *more_roles = segments[1].split('.')
    else
      id = nil
      more_roles = []
    end
    roles = segments[0].empty? ? [] : segments[0].split('.')
    if roles.length > 1
      roles.shift
    end
    if more_roles.length > 0
      roles.concat more_roles
    end
    attrs = {}
    attrs['id'] = id if id
    attrs['role'] = roles * ' ' unless roles.empty?
    attrs
  else
    {'role' => str}
  end
end

def resolve_block_subs subs, defaults, subject

def resolve_block_subs subs, defaults, subject
  resolve_subs subs, :block, defaults, subject
end

def resolve_pass_subs subs

def resolve_pass_subs subs
  resolve_subs subs, :inline, nil, 'passthrough macro'
end

def resolve_subs subs, type = :block, defaults = nil, subject = nil

returns An Array of Symbols representing the substitution operation

subs - A comma-delimited String of substitution aliases

Internal: Resolve the list of comma-delimited subs against the possible options.
def resolve_subs subs, type = :block, defaults = nil, subject = nil
  return [] if subs.nil_or_empty?
  candidates = nil
  modifiers_present = SubModifierSniffRx =~ subs
  subs.split(',').each do |val|
    key = val.strip
    modifier_operation = nil
    if modifiers_present
      if (first = key.chr) == '+'
        modifier_operation = :append
        key = key[1..-1]
      elsif first == '-'
        modifier_operation = :remove
        key = key[1..-1]
      elsif key.end_with? '+'
        modifier_operation = :prepend
        key = key.chop
      end
    end
    key = key.to_sym
    # special case to disable callouts for inline subs
    if type == :inline && (key == :verbatim || key == :v)
      resolved_keys = [:specialcharacters]
    elsif COMPOSITE_SUBS.key? key
      resolved_keys = COMPOSITE_SUBS[key]
    elsif type == :inline && key.length == 1 && (SUB_SYMBOLS.key? key)
      resolved_key = SUB_SYMBOLS[key]
      if (candidate = COMPOSITE_SUBS[resolved_key])
        resolved_keys = candidate
      else
        resolved_keys = [resolved_key]
      end
    else
      resolved_keys = [key]
    end
    if modifier_operation
      candidates ||= (defaults ? defaults.dup : [])
      case modifier_operation
      when :append
        candidates += resolved_keys
      when :prepend
        candidates = resolved_keys + candidates
      when :remove
        candidates -= resolved_keys
      end
    else
      candidates ||= []
      candidates += resolved_keys
    end
  end
  # weed out invalid options and remove duplicates (first wins)
  # TODO may be use a set instead?
  resolved = candidates & SUB_OPTIONS[type]
  unless (candidates - resolved).empty?
    invalid = candidates - resolved
    warn %(asciidoctor: WARNING: invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : nil}#{subject}: #{invalid * ', '})
  end
  resolved
end

def restore_passthroughs text, outer = true

returns The String text with the passthrough text restored

outer - A Boolean indicating whether we are in the outer call (default: true)
text - The String text into which to restore the passthrough text

Internal: Restore the passthrough text by reinserting into the placeholder positions
def restore_passthroughs text, outer = true
  if outer && (@passthroughs.empty? || !text.include?(PASS_START))
    return text
  end
  text.gsub(PASS_MATCH) {
    # NOTE we can't remove entry from map because placeholder may have been duplicated by other substitutions
    pass = @passthroughs[$~[1].to_i]
    subbed_text = (subs = pass[:subs]) ? apply_subs(pass[:text], subs) : pass[:text]
    if (type = pass[:type])
      subbed_text = Inline.new(self, :quoted, subbed_text, :type => type, :attributes => pass[:attributes]).convert
    end
    subbed_text.include?(PASS_START) ? restore_passthroughs(subbed_text, false) : subbed_text
  }
ensure
  # free memory if in outer call...we don't need these anymore
  @passthroughs.clear if outer
end

def split_simple_csv str

for double-quoted values (in which commas are ignored)
Internal: Split text formatted as CSV with support
def split_simple_csv str
  if str.empty?
    values = []
  elsif str.include? '"'
    values = []
    current = []
    quote_open = false
    str.each_char do |c|
      case c
      when ','
        if quote_open
          current.push c
        else
          values << current.join.strip
          current = []
        end
      when '"'
        quote_open = !quote_open
      else
        current.push c
      end
    end

    values << current.join.strip
  else
    values = str.split(',').map {|it| it.strip }
  end

  values
end

def sub_attributes data, opts = {}

when attribute-undefined and/or attribute-missing is drop-line
so that a missing key doesn't wipe out the whole block of data
NOTE it's necessary to perform this substitution line-by-line
--
returns The String text with the attribute references replaced with attribute values

text - The String text to process

If an attribute referenced in the line is missing, the line is dropped.

Attribute references are in the format +{name}+.

Public: Substitute attribute references
def sub_attributes data, opts = {}
  return data if data.nil_or_empty?
  # normalizes data type to an array (string becomes single-element array)
  if (string_data = String === data)
    data = [data]
  end
  doc_attrs = @document.attributes
  attribute_missing = nil
  result = []
  data.each do |line|
    reject = false
    reject_if_empty = false
    line = line.gsub(AttributeReferenceRx) {
      # alias match for Ruby 1.8.7 compat
      m = $~
      # escaped attribute, return unescaped
      if m[1] == '\\' || m[4] == '\\'
        %({#{m[2]}})
      elsif !m[3].nil_or_empty?
        offset = (directive = m[3]).length + 1
        expr = m[2][offset..-1]
        case directive
        when 'set'
          args = expr.split(':')
          _, value = Parser.store_attribute(args[0], args[1] || '', @document)
          unless value
            # since this is an assignment, only drop-line applies here (skip and drop imply the same result)
            if doc_attrs.fetch('attribute-undefined', Compliance.attribute_undefined) == 'drop-line'
              reject = true
              break ''
            end
          end
          reject_if_empty = true
          ''
        when 'counter', 'counter2'
          args = expr.split(':')
          val = @document.counter(args[0], args[1])
          if directive == 'counter2'
            reject_if_empty = true
            ''
          else
            val
          end
        else
          # if we get here, our AttributeReference regex is too loose
          warn %(asciidoctor: WARNING: illegal attribute directive: #{m[3]})
          m[0]
        end
      elsif doc_attrs.key?(key = m[2].downcase)
        doc_attrs[key]
      elsif INTRINSIC_ATTRIBUTES.key? key
        INTRINSIC_ATTRIBUTES[key]
      else
        case (attribute_missing ||= (opts[:attribute_missing] || doc_attrs.fetch('attribute-missing', Compliance.attribute_missing)))
        when 'skip'
          m[0]
        when 'drop-line'
          warn %(asciidoctor: WARNING: dropping line containing reference to missing attribute: #{key})
          reject = true
          break ''
        when 'warn'
          warn %(asciidoctor: WARNING: skipping reference to missing attribute: #{key})
          m[0]
        else # 'drop'
          # QUESTION should we warn in this case?
          reject_if_empty = true
          ''
        end
      end
    } if line.include? '{'
    result << line unless reject || (reject_if_empty && line.empty?)
  end
  string_data ? (result * EOL) : result
end

def sub_callouts(text)

Returns the converted String text

text - The String text to process

Public: Substitute callout source references
def sub_callouts(text)
  # FIXME cache this dynamic regex
  callout_rx = (attr? 'line-comment') ? /(?:#{::Regexp.escape(attr 'line-comment')} )?#{CalloutSourceRxt}/ : CalloutSourceRx
  text.gsub(callout_rx) {
    # alias match for Ruby 1.8.7 compat
    m = $~
    # honor the escape
    if m[1] == '\\'
      # we have to do a sub since we aren't sure it's the first char
      next m[0].sub('\\', '')
    end
    Inline.new(self, :callout, m[3], :id => @document.callouts.read_next_id).convert
  }
end

def sub_inline_anchors(text, found = nil)

Internal: Substitute normal and bibliographic anchors
def sub_inline_anchors(text, found = nil)
  if (!found || found[:square_bracket]) && text.include?('[[[')
    text = text.gsub(InlineBiblioAnchorRx) {
      # alias match for Ruby 1.8.7 compat
      m = $~
      # honor the escape
      if m[0].start_with? '\\'
        next m[0][1..-1]
      end
      id = reftext = m[1]
      Inline.new(self, :anchor, reftext, :type => :bibref, :target => id).convert
    }
  end
  if ((!found || found[:square_bracket]) && text.include?('[[')) ||
      ((!found || found[:macroish]) && text.include?('anchor:'))
    text = text.gsub(InlineAnchorRx) {
      # 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] == ''
        m[2] = nil if m[2] == ''
        m[4] = nil if m[4] == ''
      end
      id = m[1] || m[3]
      reftext = m[2] || m[4] || %([#{id}])
      # enable if we want to allow double quoted values
      #id = id.sub(DoubleQuotedRx, '\2')
      #if reftext
      #  reftext = reftext.sub(DoubleQuotedMultiRx, '\2')
      #else
      #  reftext = "[#{id}]"
      #end
      Inline.new(self, :anchor, reftext, :type => :ref, :target => id).convert
    }
  end
  text
end

def sub_inline_xrefs(text, found = nil)

Internal: Substitute cross reference links
def sub_inline_xrefs(text, found = nil)
  if (!found || found[:macroish]) || text.include?('&lt;&lt;')
    text = text.gsub(XrefInlineMacroRx) {
      # 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
      if m[1]
        id, reftext = m[1].split(',', 2).map {|it| it.strip }
        id = id.sub(DoubleQuotedRx, '\2')
        # NOTE In Opal, reftext is set to empty string if comma is missing
        reftext = if reftext.nil_or_empty?
          nil
        else
          reftext.sub(DoubleQuotedMultiRx, '\2')
        end
      else
        id = m[2]
        reftext = m[3] unless m[3].nil_or_empty?
      end
      if id.include? '#'
        path, fragment = id.split('#')
      else
        path = nil
        fragment = id
      end
      # handles forms: doc#, doc.adoc#, doc#id and doc.adoc#id
      if path
        path = Helpers.rootname(path)
        # the referenced path is this document, or its contents has been included in this document
        if @document.attributes['docname'] == path || @document.references[:includes].include?(path)
          refid = fragment
          path = nil
          target = %(##{fragment})
        else
          refid = fragment ? %(#{path}##{fragment}) : path
          path = "#{@document.attributes['relfileprefix']}#{path}#{@document.attributes.fetch 'outfilesuffix', '.html'}"
          target = fragment ? %(#{path}##{fragment}) : path
        end
      # handles form: id or Section Title
      else
        # resolve fragment as reftext if cannot be resolved as refid and looks like reftext
        if !(@document.references[:ids].has_key? fragment) &&
            ((fragment.include? ' ') || fragment.downcase != fragment) &&
            (resolved_id = RUBY_MIN_VERSION_1_9 ? (@document.references[:ids].key fragment) : (@document.references[:ids].index fragment))
          fragment = resolved_id
        end
        refid = fragment
        target = %(##{fragment})
      end
      Inline.new(self, :anchor, reftext, :type => :xref, :target => target, :attributes => {'path' => path, 'fragment' => fragment, 'refid' => refid}).convert
    }
  end
  text
end

def sub_macros(source)

returns The converted String text

source - The String text to process

Replace inline macros, which may span multiple lines, in the provided text

Public: Substitute inline macros (e.g., links, images, etc)
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.config[: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'] ||= File.basename(target, File.extname(target))
      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?
        text = if @document.attr? 'hide-uri-scheme'
          target.sub UriSniffRx, ''
        else
          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
      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

def sub_post_replacements(text)

Returns the converted String text

text - The String text to process

Public: Substitute post replacements
def sub_post_replacements(text)
  if (@document.attributes.has_key? 'hardbreaks') || (@attributes.has_key? 'hardbreaks-option')
    lines = (text.split EOL)
    return text if lines.size == 1
    last = lines.pop
    lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(LINE_BREAK), :type => :line).convert }.push(last) * EOL
  elsif text.include? '+'
    text.gsub(LineBreakRx) { Inline.new(self, :break, $~[1], :type => :line).convert }
  else
    text
  end
end

def sub_quotes(text)

returns The converted String text

text - The String text to process

Public: Substitute quoted text (includes emphasis, strong, monospaced, etc)
def sub_quotes(text)
  if ::RUBY_ENGINE_OPAL
    result = text
    QUOTE_SUBS[@document.compat_mode].each {|type, scope, pattern|
      result = result.gsub(pattern) { convert_quoted_text $~, type, scope }
    }
  else
    # NOTE interpolation is faster than String#dup
    result = %(#{text})
    # NOTE using gsub! here as an MRI Ruby optimization
    QUOTE_SUBS[@document.compat_mode].each {|type, scope, pattern|
      result.gsub!(pattern) { convert_quoted_text $~, type, scope }
    }
  end
  result
end

def sub_replacements(text)

returns The String text with the replacement characters substituted

text - The String text to process

Public: Substitute replacement characters (e.g., copyright, trademark, etc)
def sub_replacements(text)
  if ::RUBY_ENGINE_OPAL
    result = text
    REPLACEMENTS.each {|pattern, replacement, restore|
      result = result.gsub(pattern) {
        do_replacement $~, replacement, restore
      }
    }
  else
    # NOTE interpolation is faster than String#dup
    result = %(#{text})
    # NOTE Using gsub! as optimization
    REPLACEMENTS.each {|pattern, replacement, restore|
      result.gsub!(pattern) {
        do_replacement $~, replacement, restore
      }
    }
  end
  result
end

def sub_specialcharacters(text)

returns The String text with special characters replaced

text - The String text to process

Special characters are defined in the Asciidoctor::SPECIAL_CHARS Array constant

Public: Substitute special characters (i.e., encode XML)
def sub_specialcharacters(text)
  SUPPORTS_GSUB_RESULT_HASH ?
    text.gsub(SPECIAL_CHARS_PATTERN, SPECIAL_CHARS) :
    text.gsub(SPECIAL_CHARS_PATTERN) { SPECIAL_CHARS[$&] }
end

def unescape_bracketed_text(text)

square brackets from text extracted from brackets
Internal: Strip bounding whitespace, fold endlines and unescaped closing
def unescape_bracketed_text(text)
  return '' if text.empty?
  # FIXME make \] a regex
  text.strip.tr(EOL, ' ').gsub('\]', ']')
end

def unescape_brackets str

Intended for text extracted from square brackets.
Internal: Unescape closing square brackets.
def unescape_brackets str
  # FIXME make \] a regex
  str.empty? ? '' : str.gsub('\]', ']')
end