Class: Asciidoctor::Reader

Inherits:
Object
  • Object
show all
Includes:
Logging
Defined in:
lib/asciidoctor/reader.rb

Overview

Methods for retrieving lines from AsciiDoc source files

Direct Known Subclasses

PreprocessorReader

Defined Under Namespace

Classes: Cursor

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Logging

#logger, #message_with_context

Constructor Details

#initialize(data = nil, cursor = nil, opts = {}) ⇒ Reader

Initialize the Reader object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/asciidoctor/reader.rb', line 42

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

Instance Attribute Details

#dirObject (readonly)



26
27
28
# File 'lib/asciidoctor/reader.rb', line 26

def dir
  @dir
end

#fileObject (readonly)



25
26
27
# File 'lib/asciidoctor/reader.rb', line 25

def file
  @file
end

#linenoObject (readonly)

Get the 1-based offset of the current line.



30
31
32
# File 'lib/asciidoctor/reader.rb', line 30

def lineno
  @lineno
end

#pathObject (readonly)



27
28
29
# File 'lib/asciidoctor/reader.rb', line 27

def path
  @path
end

#process_linesObject

Control whether lines are processed using Reader#process_line on first visit (default: true)



36
37
38
# File 'lib/asciidoctor/reader.rb', line 36

def process_lines
  @process_lines
end

#source_linesObject (readonly)

Get the document source as a String Array of lines.



33
34
35
# File 'lib/asciidoctor/reader.rb', line 33

def source_lines
  @source_lines
end

#unterminatedObject

Indicates that the end of the reader was reached with a delimited block still open.



39
40
41
# File 'lib/asciidoctor/reader.rb', line 39

def unterminated
  @unterminated
end

Instance Method Details

#advanceObject

Advance to the next line by discarding the line at the front of the stack

Returns:

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



214
215
216
# File 'lib/asciidoctor/reader.rb', line 214

def advance
  shift ? true : false
end

#cursorObject



479
480
481
# File 'lib/asciidoctor/reader.rb', line 479

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

#cursor_at_line(lineno) ⇒ Object



483
484
485
# File 'lib/asciidoctor/reader.rb', line 483

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

#cursor_at_markObject



487
488
489
# File 'lib/asciidoctor/reader.rb', line 487

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

#cursor_at_prev_lineObject



500
501
502
# File 'lib/asciidoctor/reader.rb', line 500

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

#cursor_before_markObject



491
492
493
494
495
496
497
498
# File 'lib/asciidoctor/reader.rb', line 491

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

#empty?Boolean Also known as: eof?

Check whether this reader is empty (contains no lines)

Returns:

  • (Boolean)

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



91
92
93
94
95
96
97
98
# File 'lib/asciidoctor/reader.rb', line 91

def empty?
  if @lines.empty?
    @look_ahead = 0
    true
  else
    false
  end
end

#has_more_lines?True

Check whether there are any lines left to read.

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

Returns:

  • (True)

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



79
80
81
82
83
84
85
86
# File 'lib/asciidoctor/reader.rb', line 79

def has_more_lines?
  if @lines.empty?
    @look_ahead = 0
    false
  else
    true
  end
end

#line_infoA

Get information about the last line read, including file name and line number.

Returns:

  • (A)

    String summary of the last line read



511
512
513
# File 'lib/asciidoctor/reader.rb', line 511

def line_info
  %(#{@path}: line #{@lineno})
end

#linesA

Get a copy of the remaining Array of String lines managed by this Reader

Returns:

  • (A)

    copy of the String Array of lines remaining in this Reader



518
519
520
# File 'lib/asciidoctor/reader.rb', line 518

def lines
  @lines.drop 0
end

#markObject



504
505
506
# File 'lib/asciidoctor/reader.rb', line 504

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

#next_line_empty?True

Peek at the next line and check if it’s empty (i.e., whitespace only)

This method Does not consume the line from the stack.

Returns:

  • (True)

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



106
107
108
# File 'lib/asciidoctor/reader.rb', line 106

def next_line_empty?
  peek_line.nil_or_empty?
end

#peek_line(direct = false) ⇒ Object

Peek at the next line of source data. Processes the line if not already marked as processed, but does not consume it.

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

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

Parameters:

  • direct (defaults to: false)

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

Returns:

  • the next line of the source data as a String if there are lines remaining.

  • nothing if there is no more data.



128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/asciidoctor/reader.rb', line 128

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

#peek_lines(num = nil, direct = false) ⇒ A

Peek at the next multiple lines of source data. Processes the lines if not already marked as processed, but does not consume them.

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

Parameters:

  • num (defaults to: nil)

    The positive Integer number of lines to peek or nil to peek all lines (default: nil).

  • direct (defaults to: false)

    A Boolean indicating whether processing should be disabled when reading lines (default: false).

Returns:

  • (A)

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



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/asciidoctor/reader.rb', line 155

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

#readObject

Get the remaining lines of source data joined as a String.

Delegates to Reader#read_lines, then joins the result.

Returns:

  • the lines read joined as a String



207
208
209
# File 'lib/asciidoctor/reader.rb', line 207

def read
  read_lines.join LF
end

#read_lineObject

Get the next line of source data. Consumes the line returned.

Returns:

  • the String of the next line of the source data if data is present.

  • nothing if there is no more data.



179
180
181
182
# File 'lib/asciidoctor/reader.rb', line 179

def read_line
  # has_more_lines? triggers preprocessor
  shift if @look_ahead > 0 || has_more_lines?
end

#read_linesObject Also known as: readlines

Get the remaining lines of source data.

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

Returns:

  • the lines read as a String Array



192
193
194
195
196
197
198
199
# File 'lib/asciidoctor/reader.rb', line 192

def read_lines
  lines = []
  # has_more_lines? triggers preprocessor
  while has_more_lines?
    lines << shift
  end
  lines
end

#read_lines_until(options = {}) ⇒ Object

Return all the lines from ‘@lines` until we (1) run out them,

(2) find a blank line with `break_on_blank_lines: true`, or (3) find
a line for which the given block evals to true.

Examples:

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

Parameters:

  • options (defaults to: {})

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

Returns:

  • the Array of lines forming the next segment.



395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/asciidoctor/reader.rb', line 395

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

#replace_next_line(replacement) ⇒ Object Also known as: replace_line

Replace the next line with the specified line.

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

Parameters:

  • replacement

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

Returns:

  • true.



257
258
259
260
261
# File 'lib/asciidoctor/reader.rb', line 257

def replace_next_line replacement
  shift
  unshift replacement
  true
end

#skip_blank_linesInteger

Skip blank lines at the cursor.

Examples:

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

Returns:

  • (Integer)

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



278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/asciidoctor/reader.rb', line 278

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

#skip_comment_linesvoid

This method returns an undefined value.

Skip consecutive comment lines and block comments.

Examples:

@lines
=> ["// foo", "bar"]
comment_lines = skip_comment_lines
=> nil
@lines
=> ["bar"]


306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/asciidoctor/reader.rb', line 306

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

#skip_line_commentsObject

Skip consecutive comment lines and return them.

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



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/asciidoctor/reader.rb', line 331

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

#sourceObject

Get the source lines for this Reader joined as a String



528
529
530
# File 'lib/asciidoctor/reader.rb', line 528

def source
  @source_lines.join LF
end

#stringObject

Get a copy of the remaining lines managed by this Reader joined as a String



523
524
525
# File 'lib/asciidoctor/reader.rb', line 523

def string
  @lines.join LF
end

#terminatevoid

This method returns an undefined value.

Advance to the end of the reader, consuming all remaining lines



350
351
352
353
354
355
# File 'lib/asciidoctor/reader.rb', line 350

def terminate
  @lineno += @lines.size
  @lines.clear
  @look_ahead = 0
  nil
end

#to_sObject



559
560
561
# File 'lib/asciidoctor/reader.rb', line 559

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

#unshift_line(line_to_restore) ⇒ void Also known as: restore_line

This method returns an undefined value.

Push the String line onto the beginning of the Array of source data.

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

Parameters:

  • line_to_restore

    the line to restore onto the stack



228
229
230
231
# File 'lib/asciidoctor/reader.rb', line 228

def unshift_line line_to_restore
  unshift line_to_restore
  nil
end

#unshift_lines(lines_to_restore) ⇒ void Also known as: restore_lines

This method returns an undefined value.

Push an Array of lines onto the front of the Array of source data.

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



242
243
244
245
# File 'lib/asciidoctor/reader.rb', line 242

def unshift_lines lines_to_restore
  unshift_all lines_to_restore
  nil
end