class Asciidoctor::Reader
Public: Methods for retrieving lines from AsciiDoc source files
def advance
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
def discard_save @saved = nil end
def empty?
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?
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 = {}
def initialize data = nil, cursor = nil, opts = {} if !cursor @file = nil @dir = '.' @path = '<stdin>' @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call! elsif ::String === cursor @file = cursor @dir, @path = ::File.split @file @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call! 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 # IMPORTANT lineno assignment must proceed prepare_lines call! end @lines = prepare_lines data, opts @source_lines = @lines.drop 0 @mark = nil @look_ahead = 0 @process_lines = true @unescape_next_line = false @unterminated = nil @saved = nil end
def line_info
Public: Get information about the last line read, including file name and line number.
def line_info %(#{@path}: line #{@lineno}) end
def lines
Public: Get a copy of the remaining Array of String lines managed by this Reader
def lines @lines.drop 0 end
def mark
def mark @mark = @file, @dir, @path, @lineno end
def next_line_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 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 if direct || @look_ahead > 0 @unescape_next_line ? ((line = @lines[0]).slice 1, line.length) : @lines[0] elsif @lines.empty? @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 proxied lines (line = process_line @lines[0]) ? line : peek_line end end
def peek_lines num = nil, direct = false
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 = {}
: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]) trim_end = normalize == :chomp ? false : true ::Array === data ? (Helpers.prepare_source_array data, trim_end) : (Helpers.prepare_source_string data, trim_end) 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
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
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 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
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 while has_more_lines? lines << shift end lines end
def read_lines_until options = {}
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] complete = line_read = line_restored = nil shift if options[:skip_first_line] 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] unshift line line_restored = true end else unless skip_comments && (line.start_with? '//') && !(line.start_with? '///') result << line line_read = true end 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
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
def restore_save if @saved @saved.each do |name, val| instance_variable_set name, val end @saved = nil end end
def save
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
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.shift end
def skip_blank_lines
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
=> ["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
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
def source @source_lines.join LF end
def string
def string @lines.join LF end
def terminate
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
def unshift line @lineno -= 1 @look_ahead += 1 @lines.unshift line end
def unshift_all lines
def unshift_all lines @lineno -= lines.size @look_ahead += lines.size @lines.unshift(*lines) end
def unshift_line line_to_restore
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
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 nil end