class Asciidoctor::Reader

Public: Methods for retrieving lines from AsciiDoc source files

def advance

Returns a Boolean indicating whether there was a line to discard.

Public: Advance to the next line by discarding the line at the front of the stack
def advance
  shift ? true : false
end

def cursor

def cursor
  Cursor.new @file, @dir, @path, @lineno
end

def cursor_at_line lineno

def cursor_at_line lineno
  Cursor.new @file, @dir, @path, lineno
end

def cursor_at_mark

def cursor_at_mark
  @mark ? Cursor.new(*@mark) : cursor
end

def cursor_at_prev_line

def cursor_at_prev_line
  Cursor.new @file, @dir, @path, @lineno - 1
end

def cursor_before_mark

def cursor_before_mark
  if @mark
    m_file, m_dir, m_path, m_lineno = @mark
    Cursor.new m_file, m_dir, m_path, m_lineno - 1
  else
    Cursor.new @file, @dir, @path, @lineno - 1
  end
end

def discard_save

Internal: Discard a previous saved state
def discard_save
  @saved = nil
end

def empty?

Returns true if there are no more lines to peek, otherwise false.

Public: Check whether this reader is empty (contains no lines)
def empty?
  if @lines.empty?
    @look_ahead = 0
    true
  else
    false
  end
end

def has_more_lines?

Returns True if there are more lines, False if there are not.

peek_line to determine if there is a next line available.
immediately returned the cached value. Otherwise, delegate to
If a previous call to this method resulted in a value of false,

Public: Check whether there are any lines left to read.
def has_more_lines?
  if @lines.empty?
    @look_ahead = 0
    false
  else
    true
  end
end

def initialize data = nil, cursor = nil, opts = {}

Public: Initialize the Reader object
def initialize data = nil, cursor = nil, opts = {}
  if !cursor
    @file = nil
    @dir = '.'
    @path = '<stdin>'
    @lineno = 1
  elsif ::String === cursor
    @file = cursor
    @dir, @path = ::File.split @file
    @lineno = 1
  else
    if (@file = cursor.file)
      @dir = cursor.dir || (::File.dirname @file)
      @path = cursor.path || (::File.basename @file)
    else
      @dir = cursor.dir || '.'
      @path = cursor.path || '<stdin>'
    end
    @lineno = cursor.lineno || 1
  end
  @lines = (@source_lines = prepare_lines data, opts).reverse
  @mark = nil
  @look_ahead = 0
  @process_lines = true
  @unescape_next_line = false
  @unterminated = nil
  @saved = nil
end

def line_info

Returns A String summary of the last line read

Public: Get information about the last line read, including file name and line number.
def line_info
  %(#{@path}: line #{@lineno})
end

def lines

Returns A copy of the String Array of lines remaining in this Reader

Public: Get a copy of the remaining Array of String lines managed by this Reader
def lines
  @lines.reverse
end

def mark

def mark
  @mark = @file, @dir, @path, @lineno
end

def next_line_empty?

Returns True if the there are no more lines or if the next line is empty

This method Does not consume the line from the stack.

Public: Peek at the next line and check if it's empty (i.e., whitespace only)
def next_line_empty?
  peek_line.nil_or_empty?
end

def peek_line direct = false

Returns nothing if there is no more data.
Returns the next line of the source data as a String if there are lines remaining.

returns the first element of the internal @lines Array. (default: false)
direct - A Boolean flag to bypasses the check for more lines and immediately

is implicitly true (since the line is flagged as visited).
If has_more_lines? is called immediately before peek_line, the direct flag

Reader#peek_line is invoked again to perform further processing.
the Reader#process_line is nil, the data is assumed to be changed and
sub-classes the opportunity to do preprocessing. If the return value of
Reader#process_line method to be initialized. This call gives
that has not previously been visited, the line is passed to the
This method will probe the reader for more lines. If there is a next line

already marked as processed, but does not consume it.
Public: Peek at the next line of source data. Processes the line if not
def peek_line direct = false
  while true
    next_line = @lines[-1]
    if direct || @look_ahead > 0
      return @unescape_next_line ? (next_line.slice 1, next_line.length) : next_line
    elsif next_line
      # FIXME the problem with this approach is that we aren't
      # retaining the modified line (hence the @unescape_next_line tweak)
      # perhaps we need a stack of proxied lines
      if (line = process_line next_line)
        return line
      end
    else
      @look_ahead = 0
      return
    end
  end
end

def peek_lines num = nil, direct = false

if there are no more lines in this Reader.
Returns A String Array of the next multiple lines of source data, or an empty Array

direct - A Boolean indicating whether processing should be disabled when reading lines (default: false).
num - The positive Integer number of lines to peek or nil to peek all lines (default: nil).

the lines again.
be processed and marked as such so that subsequent reads will not need to process
restores the lines to the stack before returning them. This allows the lines to
This method delegates to Reader#read_line to process and collect the line, then

already marked as processed, but does not consume them.
Public: Peek at the next multiple lines of source data. Processes the lines if not
def peek_lines num = nil, direct = false
  old_look_ahead = @look_ahead
  result = []
  (num || MAX_INT).times do
    if (line = direct ? shift : read_line)
      result << line
    else
      @lineno -= 1 if direct
      break
    end
  end
  unless result.empty?
    unshift_all result
    @look_ahead = old_look_ahead if direct
  end
  result
end

def prepare_lines data, opts = {}

Returns A String Array of source lines. If the source data is an Array, this method returns a copy.

:rstrip removes all trailing whitespace; :chomp removes trailing newline only (optional, not set).
:normalize - Enables line normalization, which coerces the encoding to UTF-8 and removes trailing whitespace;
opts - A Hash of options to control how lines are prepared.
data - A String Array or String of source data to be normalized.

cleaning is very important to how Asciidoctor works). Subclasses may choose to perform additional preparation.
coerces the encoding of each line to UTF-8 and strips trailing whitespace, including the newline. (This whitespace
Converts the source data into an Array of lines ready for parsing. If the +:normalize+ option is set, this method

Internal: Prepare the source data for parsing.
def prepare_lines data, opts = {}
  if (normalize = opts[:normalize])
    ::Array === data ? (Helpers.prepare_source_array data, normalize != :chomp) : (Helpers.prepare_source_string data, normalize != :chomp)
  elsif ::Array === data
    data.drop 0
  elsif data
    data.chomp.split LF, -1
  else
    []
  end
rescue
  if (::Array === data ? data.join : data.to_s).valid_encoding?
    raise
  else
    raise ::ArgumentError, 'source is either binary or contains invalid Unicode data'
  end
end

def process_line line

advance to the next line and process it.
invocation of Reader#read_line or nil if the Reader should drop the line,
Returns The String line the Reader should make available to the next

the line unmodified.
by incrementing the look_ahead counter and returns
By default, this method marks the line as processed

Internal: Processes a previously unvisited line
def process_line line
  @look_ahead += 1 if @process_lines
  line
end

def read

Returns the lines read joined as a String

Delegates to Reader#read_lines, then joins the result.

Public: Get the remaining lines of source data joined as a String.
def read
  read_lines.join LF
end

def read_line

Returns nothing if there is no more data.
Returns the String of the next line of the source data if data is present.

Public: Get the next line of source data. Consumes the line returned.
def read_line
  # has_more_lines? triggers preprocessor
  shift if @look_ahead > 0 || has_more_lines?
end

def read_lines

Returns the lines read as a String Array

any preprocessors implemented in sub-classes.
Reader#lines in that it processes each line in turn, hence triggering
and returns the lines as a String Array. This method differs from
This method calls Reader#read_line repeatedly until all lines are consumed

Public: Get the remaining lines of source data.
def read_lines
  lines = []
  # has_more_lines? triggers preprocessor
  lines << shift while has_more_lines?
  lines
end

def read_lines_until options = {}

=> ["First line", "Second line"]
reader.read_lines_until

reader = Reader.new data, nil, normalize: true
]
"Third line\n",
"\n",
"Second line\n",
"First line\n",
data = [

Examples

Returns the Array of lines forming the next segment.

for the duration of this method
* :skip_processing is used to disable line (pre)processing
line comments
* :skip_line_comments may be used to look for and skip
included in the lines being returned
causing the method to stop processing lines should be
* :read_last_line may be used to specify that the String
pushed back onto the `lines` Array.
causing the method to stop processing lines should be
* :preserve_last_line may be used to specify that the String
beyond the first line before beginning the scan
* :skip_first_line may be used to tell the reader to advance
on a list continuation line
* :break_on_list_continuation may be used to specify to break
blank lines
* :break_on_blank_lines may be used to specify to break on
at which the reader should stop
* :terminator may be used to specify the contents of the line
options - an optional Hash of processing options:

a line for which the given block evals to true.
(2) find a blank line with `break_on_blank_lines: true`, or (3) find
Public: Return all the lines from `@lines` until we (1) run out them,
def read_lines_until options = {}
  result = []
  if @process_lines && options[:skip_processing]
    @process_lines = false
    restore_process_lines = true
  end
  if (terminator = options[:terminator])
    start_cursor = options[:cursor] || cursor
    break_on_blank_lines = false
    break_on_list_continuation = false
  else
    break_on_blank_lines = options[:break_on_blank_lines]
    break_on_list_continuation = options[:break_on_list_continuation]
  end
  skip_comments = options[:skip_line_comments]
  line_read = line_restored = nil
  shift if options[:skip_first_line]
  while (line = read_line)
    if terminator ? line == terminator : ((break_on_blank_lines && line.empty?) ||
        (break_on_list_continuation && line_read && line == LIST_CONTINUATION && (options[:preserve_last_line] = true)) ||
        (block_given? && (yield line)))
      result << line if options[:read_last_line]
      if options[:preserve_last_line]
        unshift line
        line_restored = true
      end
      break
    end
    unless skip_comments && (line.start_with? '//') && !(line.start_with? '///')
      result << line
      line_read = true
    end
  end
  if restore_process_lines
    @process_lines = true
    @look_ahead -= 1 if line_restored && !terminator
  end
  if terminator && terminator != line && (context = options.fetch :context, terminator)
    start_cursor = cursor_at_mark if start_cursor == :at_mark
    logger.warn message_with_context %(unterminated #{context} block), source_location: start_cursor
    @unterminated = true
  end
  result
end

def replace_next_line replacement

Returns true.

replacement - The String line to put in place of the next line (i.e., the line at the cursor).

line stack.
Reader#unshift to push the replacement onto the top of the
Calls Reader#advance to consume the current line, then calls

Public: Replace the next line with the specified line.
def replace_next_line replacement
  shift
  unshift replacement
  true
end

def restore_save

Internal: Restore the state of the reader at cursor
def restore_save
  if @saved
    @saved.each do |name, val|
      instance_variable_set name, val
    end
    @saved = nil
  end
end

def save

Internal: Save the state of the reader at cursor
def save
  @saved = {}.tap do |accum|
    instance_variables.each do |name|
      unless name == :@saved || name == :@source_lines
        accum[name] = ::Array === (val = instance_variable_get name) ? (val.drop 0) : val
      end
    end
  end
  nil
end

def shift

Returns The String line at the top of the stack

Use read_line if the line hasn't (or many not have been) visited yet.
and determined that you do, in fact, want to pluck that line off the stack.
This method can be used directly when you've already called peek_line

Internal: Shift the line off the stack and increment the lineno
def shift
  @lineno += 1
  @look_ahead -= 1 unless @look_ahead == 0
  @lines.pop
end

def skip_blank_lines

been consumed (even if lines were skipped by this method).
Returns the [Integer] number of lines skipped or nothing if all lines have

=> ["Foo", "Bar", ""]
reader.lines
=> 2
reader.skip_blank_lines
=> ["", "", "Foo", "Bar", ""]
reader.lines

Examples

Public: Skip blank lines at the cursor.
def skip_blank_lines
  return if empty?
  num_skipped = 0
  # optimized code for shortest execution path
  while (next_line = peek_line)
    if next_line.empty?
      shift
      num_skipped += 1
    else
      return num_skipped
    end
  end
end

def skip_comment_lines

Returns nothing

=> ["bar"]
@lines

=> nil
comment_lines = skip_comment_lines

=> ["// foo", "bar"]
@lines
Examples

Public: Skip consecutive comment lines and block comments.
def skip_comment_lines
  return if empty?
  while (next_line = peek_line) && !next_line.empty?
    if next_line.start_with? '//'
      if next_line.start_with? '///'
        if (ll = next_line.length) > 3 && next_line == '/' * ll
          read_lines_until terminator: next_line, skip_first_line: true, read_last_line: true, skip_processing: true, context: :comment
        else
          break
        end
      else
        shift
      end
    else
      break
    end
  end
  nil
end

def skip_line_comments

This method assumes the reader only contains simple lines (no blocks).

Public: Skip consecutive comment lines and return them.
def skip_line_comments
  return [] if empty?
  comment_lines = []
  # optimized code for shortest execution path
  while (next_line = peek_line) && !next_line.empty?
    if next_line.start_with? '//'
      comment_lines << shift
    else
      break
    end
  end
  comment_lines
end

def source

Public: Get the source lines for this Reader joined as a String
def source
  @source_lines.join LF
end

def string

Public: Get a copy of the remaining lines managed by this Reader joined as a String
def string
  @lines.reverse.join LF
end

def terminate

Returns nothing.

Public: Advance to the end of the reader, consuming all remaining lines
def terminate
  @lineno += @lines.size
  @lines.clear
  @look_ahead = 0
  nil
end

def to_s

def to_s
  %(#<#{self.class}@#{object_id} {path: #{@path.inspect}, line: #{@lineno}}>)
end

def unshift line

Internal: Restore the line to the stack and decrement the lineno
def unshift line
  @lineno -= 1
  @look_ahead += 1
  @lines.push line
  nil
end

def unshift_all lines_to_restore

Internal: Restore the lines to the stack and decrement the lineno
def unshift_all lines_to_restore
  @lineno -= lines_to_restore.size
  @look_ahead += lines_to_restore.size
  if lines_to_restore.respond_to? :reverse
    @lines.push(*lines_to_restore.reverse)
  else
    lines_to_restore.reverse_each {|it| @lines.push it }
  end
  nil
end

def unshift_all lines_to_restore

Internal: Restore the lines to the stack and decrement the lineno
def unshift_all lines_to_restore
  @lineno -= lines_to_restore.size
  @look_ahead += lines_to_restore.size
  @lines.push(*lines_to_restore.reverse)
  nil
end

def unshift_line line_to_restore

Returns nothing.

line_to_restore - the line to restore onto the stack

processed immediately.
not otherwise contain preprocessor directives. Therefore, it is marked as
method assumes the line was previously retrieved from the reader or does
A line pushed on the reader using this method is not processed again. The

Public: Push the String line onto the beginning of the Array of source data.
def unshift_line line_to_restore
  unshift line_to_restore
  nil
end

def unshift_lines lines_to_restore

Returns nothing.

as processed immediately.
not otherwise contain preprocessor directives. Therefore, they are marked
method assumes the lines were previously retrieved from the reader or do
Lines pushed on the reader using this method are not processed again. The

Public: Push an Array of lines onto the front of the Array of source data.
def unshift_lines lines_to_restore
  unshift_all lines_to_restore
end