class Asciidoctor::PreprocessorReader

directives as each line is read off the Array of lines.
Public: Methods for retrieving lines from AsciiDoc source files, evaluating preprocessor

def exceeded_max_depth?

def exceeded_max_depth?
  if (abs_maxdepth = @maxdepth[:abs]) > 0 && @include_stack.size >= abs_maxdepth
    @maxdepth[:rel]
  else
    false
  end
end

def include_depth

def include_depth
  @include_stack.size 
end

def include_processors?

def include_processors?
  if !@include_processor_extensions
    if @document.extensions? && @document.extensions.include_processors?
      @include_processor_extensions = @document.extensions.include_processors
      true
    else
      @include_processor_extensions = false
      false
    end
  else
    @include_processor_extensions != false
  end
end

def initialize document, data = nil, cursor = nil

Public: Initialize the PreprocessorReader object
def initialize document, data = nil, cursor = nil
  @document = document
  super data, cursor, :normalize => true
  include_depth_default = document.attributes.fetch('max-include-depth', 64).to_i
  include_depth_default = 0 if include_depth_default < 0
  # track both absolute depth for comparing to size of include stack and relative depth for reporting
  @maxdepth = {:abs => include_depth_default, :rel => include_depth_default}
  @include_stack = []
  @includes = (document.references[:includes] ||= [])
  @skipping = false
  @conditional_stack = []
  @include_processor_extensions = nil
end

def peek_line direct = false

Returns nil if there are no more lines remaining and the include stack is empty.
in the current include context or a parent include context.
Returns the next line of the source data as a String if there are lines remaining

one include on the stack.
stack if the last line has been reached and there's at least
Public: Override the Reader#peek_line method to pop the include
def peek_line direct = false
  if (line = super)
    line
  elsif @include_stack.empty?
    nil
  else
    pop_include
    peek_line direct
  end
end

def pop_include

def pop_include
  if @include_stack.size > 0
    @lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines = @include_stack.pop
    # FIXME kind of a hack
    #Document::AttributeEntry.new('infile', @file).save_to_next_block @document
    #Document::AttributeEntry.new('indir', ::File.dirname(@file)).save_to_next_block @document
    @eof = @lines.empty?
    @look_ahead = 0
  end
  nil
end

def prepare_lines data, opts = {}

def prepare_lines data, opts = {}
  result = super
  # QUESTION should this work for AsciiDoc table cell content? Currently it does not.
  if @document && (@document.attributes.has_key? 'skip-front-matter')
    if (front_matter = skip_front_matter! result)
      @document.attributes['front-matter'] = front_matter * EOL
    end
  end
  if opts.fetch :condense, true
    result.shift && @lineno += 1 while (first = result[0]) && first.empty?
    result.pop while (last = result[-1]) && last.empty?
  end
  if (indent = opts.fetch(:indent, nil))
    Parser.reset_block_indent! result, indent.to_i
  end
  result
end

def preprocess_conditional_inclusion directive, target, delimiter, text

returns a Boolean indicating whether the cursor should be advanced

ifndef directives, and for the conditional expression for the ifeval directive.
Used for a single-line conditional block in the case of the ifdef or
text - The text associated with this directive (occurring between the square brackets)
can be defined or undefined.
attributes must be defined or undefined, ',' means any of the attributes
delimiter - The conditional delimiter for multiple attributes ('+' means all
used in the condition (blank in the case of the ifeval directive)
target - The target, which is the name of one or more attributes that are
directive - The conditional inclusion directive (ifdef, ifndef, ifeval, endif)

available content is found.
satisfied and continue preprocessing recursively until the next line of
blocks. If the Reader is not skipping, mark whether the condition is
simply track the open and close delimiters of any nested conditional
endif) under the cursor. If the Reader is currently skipping content, then
Preprocess the conditional inclusion directive (ifdef, ifndef, ifeval,

Internal: Preprocess the directive (macro) to conditionally include content.
def preprocess_conditional_inclusion directive, target, delimiter, text
  # must have a target before brackets if ifdef or ifndef
  # must not have text between brackets if endif
  # don't honor match if it doesn't meet this criteria
  # QUESTION should we warn for these bogus declarations?
  if ((directive == 'ifdef' || directive == 'ifndef') && target.empty?) ||
      (directive == 'endif' && text)
    return false
  end
  # attributes are case insensitive
  target = target.downcase
  if directive == 'endif'
    stack_size = @conditional_stack.size
    if stack_size > 0
      pair = @conditional_stack[-1]
      if target.empty? || target == pair[:target]
        @conditional_stack.pop
        @skipping = @conditional_stack.empty? ? false : @conditional_stack[-1][:skipping]
      else
        warn "asciidoctor: ERROR: #{line_info}: mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[]"
      end
    else
      warn "asciidoctor: ERROR: #{line_info}: unmatched macro: endif::#{target}[]"
    end
    return true
  end
  skip = false
  unless @skipping
    # QUESTION any way to wrap ifdef & ifndef logic up together?
    case directive
    when 'ifdef'
      case delimiter
      when nil
        # if the attribute is undefined, then skip
        skip = !@document.attributes.has_key?(target)
      when ','
        # if any attribute is defined, then don't skip
        skip = !target.split(',').detect {|name| @document.attributes.has_key? name }
      when '+'
        # if any attribute is undefined, then skip
        skip = target.split('+').detect {|name| !@document.attributes.has_key? name }
      end
    when 'ifndef'
      case delimiter
      when nil
        # if the attribute is defined, then skip
        skip = @document.attributes.has_key?(target)
      when ','
        # if any attribute is undefined, then don't skip
        skip = !target.split(',').detect {|name| !@document.attributes.has_key? name }
      when '+'
        # if any attribute is defined, then skip
        skip = target.split('+').detect {|name| @document.attributes.has_key? name }
      end
    when 'ifeval'
      # the text in brackets must match an expression
      # don't honor match if it doesn't meet this criteria
      if !target.empty? || !(expr_match = EvalExpressionRx.match(text.strip))
        return false
      end
      lhs = resolve_expr_val expr_match[1]
      # regex enforces a restrict set of math-related operations
      op = expr_match[2]
      rhs = resolve_expr_val expr_match[3]
      skip = !(lhs.send op.to_sym, rhs)
    end
  end
  # conditional inclusion block
  if directive == 'ifeval' || !text
    @skipping = true if skip
    @conditional_stack << {:target => target, :skip => skip, :skipping => @skipping}
  # single line conditional inclusion
  else
    unless @skipping || skip
      # FIXME slight hack to skip past conditional line
      # but keep our synthetic line marked as processed
      conditional_line = peek_line true
      replace_line text.rstrip
      unshift conditional_line
      return true
    end
  end
  true
end

def preprocess_include raw_target, raw_attributes

returns a Boolean indicating whether the line under the cursor has changed.

target slot of the include::[] macro
target - The name of the source document to include as specified in the

If none of the above apply, emit the include directive line verbatim.

of the Array of source data.
stack size, normalize the target path and read the lines onto the beginning
Otherwise, if the max depth is greater than 0, and is not exceeded by the

attributes to that processor and expect an Array of String lines in return.
Otherwise, if an include processor is specified pass the target and

directive line is emitted verbatim.
If SafeMode is SECURE or greater, the directive is ignore and the include

are as follows:
Preprocess the directive to include the target document. The scenarios

Internal: Preprocess the directive (macro) to include the target document.
def preprocess_include raw_target, raw_attributes
  if (target = @document.sub_attributes raw_target, :attribute_missing => 'drop-line').empty?
    if @document.attributes.fetch('attribute-missing', Compliance.attribute_missing) == 'skip'
      replace_line %(Unresolved directive in #{@path} - include::#{raw_target}[#{raw_attributes}])
      true
    else
      advance
      true
    end
  # assume that if an include processor is given, the developer wants
  # to handle when and how to process the include
  elsif include_processors? &&
      (extension = @include_processor_extensions.find {|candidate| candidate.instance.handles? target })
    advance
    # FIXME parse attributes if requested by extension
    extension.process_method[@document, self, target, AttributeList.new(raw_attributes).parse]
    true
  # if running in SafeMode::SECURE or greater, don't process this directive
  # however, be friendly and at least make it a link to the source document
  elsif @document.safe >= SafeMode::SECURE
    # FIXME we don't want to use a link macro if we are in a verbatim context
    replace_line %(link:#{target}[])
    true
  elsif (abs_maxdepth = @maxdepth[:abs]) > 0 && @include_stack.size >= abs_maxdepth
    warn %(asciidoctor: ERROR: #{line_info}: maximum include depth of #{@maxdepth[:rel]} exceeded)
    false
  elsif abs_maxdepth > 0
    if ::RUBY_ENGINE_OPAL
      # NOTE resolves uri relative to currently loaded document
      # NOTE we defer checking if file exists and catch the 404 error if it does not
      # TODO only use this logic if env-browser is set
      target_type = :file
      include_file = path = if @include_stack.empty?
        ::Dir.pwd == @document.base_dir ? target : (::File.join @dir, target)
      else
        ::File.join @dir, target
      end
    elsif target.include?(':') && UriSniffRx =~ target
      unless @document.attributes.has_key? 'allow-uri-read'
        replace_line %(link:#{target}[])
        return true
      end
      target_type = :uri
      include_file = path = target
      if @document.attributes.has_key? 'cache-uri'
        # caching requires the open-uri-cached gem to be installed
        # processing will be automatically aborted if these libraries can't be opened
        Helpers.require_library 'open-uri/cached', 'open-uri-cached'
      elsif !::RUBY_ENGINE_OPAL
        # autoload open-uri
        ::OpenURI
      end
    else
      target_type = :file
      # include file is resolved relative to dir of current include, or base_dir if within original docfile
      include_file = @document.normalize_system_path(target, @dir, nil, :target_name => 'include file')
      unless ::File.file? include_file
        warn "asciidoctor: WARNING: #{line_info}: include file not found: #{include_file}"
        replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}])
        return true
      end
      #path = @document.relative_path include_file
      path = PathResolver.new.relative_path include_file, @document.base_dir
    end
    inc_lines = nil
    tags = nil
    attributes = {}
    if !raw_attributes.empty?
      # QUESTION should we use @document.parse_attribues?
      attributes = AttributeList.new(raw_attributes).parse
      if attributes.has_key? 'lines'
        inc_lines = []
        attributes['lines'].split(DataDelimiterRx).each do |linedef|
          if linedef.include?('..')
            from, to = linedef.split('..').map(&:to_i)
            if to == -1
              inc_lines << from
              inc_lines << 1.0/0.0
            else
              inc_lines.concat ::Range.new(from, to).to_a
            end
          else
            inc_lines << linedef.to_i
          end
        end
        inc_lines = inc_lines.sort.uniq
      elsif attributes.has_key? 'tag'
        tags = [attributes['tag']].to_set
      elsif attributes.has_key? 'tags'
        tags = attributes['tags'].split(DataDelimiterRx).uniq.to_set
      end
    end
    if !inc_lines.nil?
      if !inc_lines.empty?
        selected = []
        inc_line_offset = 0
        inc_lineno = 0
        begin
          open(include_file, 'r') do |f|
            f.each_line do |l|
              inc_lineno += 1
              take = inc_lines[0]
              if take.is_a?(::Float) && take.infinite?
                selected.push l
                inc_line_offset = inc_lineno if inc_line_offset == 0
              else
                if f.lineno == take
                  selected.push l
                  inc_line_offset = inc_lineno if inc_line_offset == 0
                  inc_lines.shift
                end
                break if inc_lines.empty?
              end
            end
          end
        rescue
          warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file})
          replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}])
          return true
        end
        advance
        # FIXME not accounting for skipped lines in reader line numbering
        push_include selected, include_file, path, inc_line_offset, attributes
      end
    elsif !tags.nil?
      if !tags.empty?
        selected = []
        inc_line_offset = 0
        inc_lineno = 0
        active_tag = nil
        tags_found = ::Set.new
        begin
          open(include_file, 'r') do |f|
            f.each_line do |l|
              inc_lineno += 1
              # must force encoding here since we're performing String operations on line
              l.force_encoding(::Encoding::UTF_8) if FORCE_ENCODING
              l = l.rstrip
              if active_tag
                if l.end_with?(%(end::#{active_tag}[])) && TagDirectiveRx =~ l
                  active_tag = nil
                else
                  selected.push l unless l.end_with?('[]') && TagDirectiveRx =~ l
                  inc_line_offset = inc_lineno if inc_line_offset == 0
                end
              else
                tags.each do |tag|
                  if l.end_with?(%(tag::#{tag}[])) && TagDirectiveRx =~ l
                    active_tag = tag
                    tags_found << tag
                    break
                  end
                end
              end
            end
          end
        rescue
          warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file})
          replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}])
          return true
        end
        unless (missing_tags = tags.to_a - tags_found.to_a).empty?
          warn "asciidoctor: WARNING: #{line_info}: tag#{missing_tags.size > 1 ? 's' : nil} '#{missing_tags * ','}' not found in include #{target_type}: #{include_file}"
        end
        advance
        # FIXME not accounting for skipped lines in reader line numbering
        push_include selected, include_file, path, inc_line_offset, attributes
      end
    else
      begin
        advance
        push_include open(include_file, 'r') {|f| f.read }, include_file, path, 1, attributes
      rescue
        warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file})
        replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}])
        return true
      end
    end
    true
  else
    false
  end
end

def process_line line

def process_line line
  return line unless @process_lines
  if line.empty?
    @look_ahead += 1
    return ''
  end
  # NOTE highly optimized
  if line.end_with?(']') && !line.start_with?('[') && line.include?('::')
    if line.include?('if') && (match = ConditionalDirectiveRx.match(line))
      # if escaped, mark as processed and return line unescaped
      if line.start_with?('\\')
        @unescape_next_line = true
        @look_ahead += 1
        line[1..-1]
      else
        if preprocess_conditional_inclusion(*match.captures)
          # move the pointer past the conditional line
          advance
          # treat next line as uncharted territory
          nil
        else
          # the line was not a valid conditional line
          # mark it as visited and return it
          @look_ahead += 1
          line
        end
      end
    elsif @skipping
      advance
      nil
    elsif ((escaped = line.start_with?('\\include::')) || line.start_with?('include::')) && (match = IncludeDirectiveRx.match(line))
      # if escaped, mark as processed and return line unescaped
      if escaped
        @unescape_next_line = true
        @look_ahead += 1
        line[1..-1]
      else
        # QUESTION should we strip whitespace from raw attributes in Substitutors#parse_attributes? (check perf)
        if preprocess_include match[1], match[2].strip
          # peek again since the content has changed
          nil
        else
          # the line was not a valid include line and is unchanged
          # mark it as visited and return it
          @look_ahead += 1
          line
        end
      end
    else
      # NOTE optimization to inline super
      @look_ahead += 1
      line
    end
  elsif @skipping
    advance
    nil
  else
    # NOTE optimization to inline super
    @look_ahead += 1
    line
  end
end

def push_include data, file = nil, path = nil, lineno = 1, attributes = {}

Returns nothing

reader.push_include data, file, path
data = IO.read file
file = File.expand_path path
path = 'partial.adoc'

Examples

read from the target specified.
This method is typically used in an IncludeProcessor to add source

based on the file, document-relative path and line information given.
Public: Push source onto the front of the reader and switch the context
def push_include data, file = nil, path = nil, lineno = 1, attributes = {}
  @include_stack << [@lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines]
  if file
    @file = file
    @dir = File.dirname file
    # only process lines in AsciiDoc files
    @process_lines = ASCIIDOC_EXTENSIONS[::File.extname(file)]
  else
    @file = nil
    @dir = '.' # right?
    # we don't know what file type we have, so assume AsciiDoc
    @process_lines = true
  end
  @path = if path
    @includes << Helpers.rootname(path)
    path
  else
    '<stdin>'
  end
  @lineno = lineno
  if attributes.has_key? 'depth'
    depth = attributes['depth'].to_i
    depth = 1 if depth <= 0
    @maxdepth = {:abs => (@include_stack.size - 1) + depth, :rel => depth}
  end
  # effectively fill the buffer
  if (@lines = prepare_lines data, :normalize => true, :condense => false, :indent => attributes['indent']).empty?
    pop_include
  else
    # FIXME we eventually want to handle leveloffset without affecting the lines
    if attributes.has_key? 'leveloffset'
      @lines.unshift ''
      @lines.unshift %(:leveloffset: #{attributes['leveloffset']})
      @lines.push ''
      if (old_leveloffset = @document.attr 'leveloffset')
        @lines.push %(:leveloffset: #{old_leveloffset})
      else
        @lines.push ':leveloffset!:'
      end
      # compensate for these extra lines
      @lineno -= 2
    end
    # FIXME kind of a hack
    #Document::AttributeEntry.new('infile', @file).save_to_next_block @document
    #Document::AttributeEntry.new('indir', @dir).save_to_next_block @document
    @eof = false
    @look_ahead = 0
  end
  nil
end

def resolve_expr_val(str)

Returns The value of the expression, coerced to the appropriate type

# => "value"
resolve_expr_val(expr)
expr = '"{name}"'
@document.attributes['name'] = 'value'

# => 2
resolve_expr_val(expr)
expr = '2'

# => nil
resolve_expr_val(expr)
expr = '{undefined}'

# => ""
resolve_expr_val(expr)
expr = '"{undefined}"'

# => "\"value"
resolve_expr_val(expr)
expr = '"value'

# => "value"
resolve_expr_val(expr)
expr = '"value"'

Examples

Private: Resolve the value of one side of the expression
def resolve_expr_val(str)
  val = str
  type = nil
  if val.start_with?('"') && val.end_with?('"') ||
      val.start_with?('\'') && val.end_with?('\'')
    type = :string
    val = val[1...-1]
  end
  # QUESTION should we substitute first?
  if val.include? '{'
    val = @document.sub_attributes val
  end
  unless type == :string
    if val.empty?
      val = nil
    elsif val.strip.empty?
      val = ' '
    elsif val == 'true'
      val = true
    elsif val == 'false'
      val = false
    elsif val.include?('.')
      val = val.to_f
    else
      # fallback to coercing to integer, since we
      # require string values to be explicitly quoted
      val = val.to_i
    end
  end
  val
end

def shift

just implement the logic there?
also, we now have the field in the super class, so perhaps
TODO Document this override
def shift
  if @unescape_next_line
    @unescape_next_line = false
    super[1..-1]
  else
    super
  end
end

def skip_front_matter! data, increment_linenos = true

Private: Ignore front-matter, commonly used in static site generators
def skip_front_matter! data, increment_linenos = true
  front_matter = nil
  if data[0] == '---'
    original_data = data.dup
    front_matter = []
    data.shift
    @lineno += 1 if increment_linenos
    while !data.empty? && data[0] != '---'
      front_matter.push data.shift
      @lineno += 1 if increment_linenos
    end
    if data.empty?
      data.unshift(*original_data)
      @lineno = 0 if increment_linenos
      front_matter = nil
    else
      data.shift
      @lineno += 1 if increment_linenos
    end
  end
  front_matter
end

def to_s

def to_s
  %(#<#{self.class}@#{object_id} {path: #{@path.inspect}, line #: #{@lineno}, include depth: #{@include_stack.size}, include stack: [#{@include_stack.map {|inc| inc.to_s}.join ', '}]}>)
end