class Asciidoctor::Reader

Public: Methods for retrieving lines from AsciiDoc source files

def advance direct = true

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

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

Public: Advance to the next line by discarding the line at the front of the stack
def advance direct = true
  !!read_line(direct)
end

def cursor

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

def eof?

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

Public: Check whether this reader is empty (contains no lines)
def eof?
  !has_more_lines?
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?
  !(@eof || (@eof = peek_line.nil?))
end

def initialize data = nil, cursor = nil, opts = {:normalize => false}

Public: Initialize the Reader object
def initialize data = nil, cursor = nil, opts = {:normalize => false}
  if !cursor
    @file = @dir = nil
    @path = '<stdin>'
    @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
  elsif cursor.is_a? ::String
    @file = cursor
    @dir, @path = ::File.split @file
    @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
  else
    @file = cursor.file
    @dir = cursor.dir
    @path = cursor.path || '<stdin>'
    if @file
      unless @dir
        # REVIEW might to look at this assignment closer
        @dir = ::File.dirname @file
        @dir = nil if @dir == '.' # right?
      end
      unless cursor.path
        @path = ::File.basename @file
      end
    end
    @lineno = cursor.lineno || 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
  end
  @lines = data ? (prepare_lines data, opts) : []
  @source_lines = @lines.dup
  @eof = @lines.empty?
  @look_ahead = 0
  @process_lines = true
  @unescape_next_line = false
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.dup
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 nil 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

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-classess 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
  if direct || @look_ahead > 0
    @unescape_next_line ? @lines[0][1..-1] : @lines[0]
  elsif @eof || @lines.empty?
    @eof = true
    @look_ahead = 0
    nil
  else
    # 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 proxy lines
    if !(line = process_line @lines[0])
      peek_line
    else
      line
    end
  end
end

def peek_lines num = 1, direct = true

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
num - The Integer number of lines to peek.

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 = 1, direct = true
  old_look_ahead = @look_ahead
  result = []
  num.times do
    if (line = read_line direct)
      result << line
    else
      break
    end
  end
  unless result.empty?
    result.reverse_each {|line| unshift line }
    @look_ahead = old_look_ahead if direct
  end
  result
end

def prepare_lines data, opts = {}

Returns The String lines extracted from the data

opts - A Hash of options to control what cleansing is done
data - A String Array of input data to be normalized

Any leading or trailing blank lines are also removed.

works.
whitespace substitution is very important to how Asciidoctor
the source data and appends a LF (i.e., Unix endline). This
This method strips whitespace from the end of every line of

Internal: Prepare the lines from the provided data
def prepare_lines data, opts = {}
  if data.is_a? ::String
    if opts[:normalize]
      Helpers.normalize_lines_from_string data
    else
      data.split EOL
    end
  else
    if opts[:normalize]
      Helpers.normalize_lines_array data
    else
      data.dup
    end
  end
end

def prev_line_info

def prev_line_info
  %(#{@path}: line #{@lineno - 1})
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 * EOL
end

def read_line direct = false

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

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

Public: Get the next line of source data. Consumes the line returned.
def read_line direct = false
  if direct || @look_ahead > 0 || has_more_lines?
    shift
  else
    nil
  end
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 = []
  while has_more_lines?
    lines << shift
  end
  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.

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
blank lines
* :break_on_blank_lines may be used to specify to break on
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 = []
  advance if options[:skip_first_line]
  if @process_lines && options[:skip_processing]
    @process_lines = false
    restore_process_lines = true
  else
    restore_process_lines = false
  end
  if (terminator = options[:terminator])
    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_line_comments = options[:skip_line_comments]
  line_read = false
  line_restored = false
  
  complete = false
  while !complete && (line = read_line)
    complete = while true
      break true if terminator && line == terminator
      # QUESTION: can we get away with line.empty? here?
      break true if break_on_blank_lines && line.empty?
      if break_on_list_continuation && line_read && line == LIST_CONTINUATION
        options[:preserve_last_line] = true
        break true
      end
      break true if block_given? && (yield line)
      break false
    end
    if complete
      if options[:read_last_line]
        result << line
        line_read = true
      end
      if options[:preserve_last_line]
        restore_line line
        line_restored = true
      end
    else
      unless skip_line_comments && line.start_with?('//') && CommentLineRx =~ line
        result << line
        line_read = true
      end
    end
  end
  if restore_process_lines
    @process_lines = true
    @look_ahead -= 1 if line_restored && !terminator
  end
  result
end

def replace_line replacement

Returns nothing.

replacement - The String line to put in place of 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 current line with the specified line.
def replace_line replacement
  advance
  unshift replacement
  nil
end

def shift

Returns The String line at the top of the stack

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.shift
end

def skip_blank_lines

Returns an Integer of the number of lines skipped

=> ["Foo", "Bar", ""]
@lines

=> 2
skip_blank_lines

=> ["", "", "Foo", "Bar", ""]
@lines

Examples

Public: Strip off leading blank lines in the Array of lines.
def skip_blank_lines
  return 0 if eof?
  num_skipped = 0
  # optimized code for shortest execution path
  while (next_line = peek_line)
    if next_line.empty?
      advance
      num_skipped += 1
    else
      return num_skipped
    end
  end
  num_skipped
end

def skip_comment_lines opts = {}

Returns the Array of lines that were skipped

=> ["bar"]
@lines

=> ["// foo"]
comment_lines = skip_comment_lines

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

Public: Skip consecutive lines containing line comments and return them.
def skip_comment_lines opts = {}
  return [] if eof?
  comment_lines = []
  include_blank_lines = opts[:include_blank_lines]
  while (next_line = peek_line)
    if include_blank_lines && next_line.empty?
      comment_lines << shift
    elsif (commentish = next_line.start_with?('//')) && (match = CommentBlockRx.match(next_line))
      comment_lines << shift
      comment_lines.push(*(read_lines_until(:terminator => match[0], :read_last_line => true, :skip_processing => true)))
    elsif commentish && CommentLineRx =~ next_line
      comment_lines << shift
    else
      break
    end
  end
  comment_lines
end

def skip_line_comments

Public: Skip consecutive lines that are line comments and return them.
def skip_line_comments
  return [] if eof?
  comment_lines = []
  # optimized code for shortest execution path
  while (next_line = peek_line)
    if CommentLineRx =~ next_line
      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 * EOL
end

def string

Public: Get a copy of the remaining lines managed by this Reader joined as a String
def string
  @lines * EOL
end

def terminate

Returns nothing.

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

def to_s

Returns A string summary of this reader, which contains the path and line information


Public: Get a summary of this Reader.
def to_s
  line_info
end

def unshift line

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

def unshift_line line_to_restore

returns nil

reader, it is marked as seen.
Since this line was (assumed to be) previously retrieved through 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 nil

reader, they are marked as seen.
Since these lines were (assumed to be) previously retrieved through the

Public: Push an Array of lines onto the front of the Array of source data.
def unshift_lines lines_to_restore
  # QUESTION is it faster to use unshift(*lines_to_restore)?
  lines_to_restore.reverse_each {|line| unshift line }
  nil
end