"Fossies" - the Fresh Open Source Software Archive

Member "asciidoctor-2.0.10/lib/asciidoctor/reader.rb" (1 Jun 2019, 49393 Bytes) of package /linux/www/asciidoctor-2.0.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "reader.rb": 2.0.5_vs_2.0.6.

    1 # frozen_string_literal: true
    2 module Asciidoctor
    3 # Public: Methods for retrieving lines from AsciiDoc source files
    4 class Reader
    5   include Logging
    6 
    7   class Cursor
    8     attr_reader :file, :dir, :path, :lineno
    9 
   10     def initialize file, dir = nil, path = nil, lineno = 1
   11       @file, @dir, @path, @lineno = file, dir, path, lineno
   12     end
   13 
   14     def advance num
   15       @lineno += num
   16     end
   17 
   18     def line_info
   19       %(#{@path}: line #{@lineno})
   20     end
   21 
   22     alias to_s line_info
   23   end
   24 
   25   attr_reader :file
   26   attr_reader :dir
   27   attr_reader :path
   28 
   29   # Public: Get the 1-based offset of the current line.
   30   attr_reader :lineno
   31 
   32   # Public: Get the document source as a String Array of lines.
   33   attr_reader :source_lines
   34 
   35   # Public: Control whether lines are processed using Reader#process_line on first visit (default: true)
   36   attr_accessor :process_lines
   37 
   38   # Public: Indicates that the end of the reader was reached with a delimited block still open.
   39   attr_accessor :unterminated
   40 
   41   # Public: Initialize the Reader object
   42   def initialize data = nil, cursor = nil, opts = {}
   43     if !cursor
   44       @file = nil
   45       @dir = '.'
   46       @path = '<stdin>'
   47       @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
   48     elsif ::String === cursor
   49       @file = cursor
   50       @dir, @path = ::File.split @file
   51       @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
   52     else
   53       if (@file = cursor.file)
   54         @dir = cursor.dir || (::File.dirname @file)
   55         @path = cursor.path || (::File.basename @file)
   56       else
   57         @dir = cursor.dir || '.'
   58         @path = cursor.path || '<stdin>'
   59       end
   60       @lineno = cursor.lineno || 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
   61     end
   62     @lines = prepare_lines data, opts
   63     @source_lines = @lines.drop 0
   64     @mark = nil
   65     @look_ahead = 0
   66     @process_lines = true
   67     @unescape_next_line = false
   68     @unterminated = nil
   69     @saved = nil
   70   end
   71 
   72   # Public: Check whether there are any lines left to read.
   73   #
   74   # If a previous call to this method resulted in a value of false,
   75   # immediately returned the cached value. Otherwise, delegate to
   76   # peek_line to determine if there is a next line available.
   77   #
   78   # Returns True if there are more lines, False if there are not.
   79   def has_more_lines?
   80     if @lines.empty?
   81       @look_ahead = 0
   82       false
   83     else
   84       true
   85     end
   86   end
   87 
   88   # Public: Check whether this reader is empty (contains no lines)
   89   #
   90   # Returns true if there are no more lines to peek, otherwise false.
   91   def empty?
   92     if @lines.empty?
   93       @look_ahead = 0
   94       true
   95     else
   96       false
   97     end
   98   end
   99   alias eof? empty?
  100 
  101   # Public: Peek at the next line and check if it's empty (i.e., whitespace only)
  102   #
  103   # This method Does not consume the line from the stack.
  104   #
  105   # Returns True if the there are no more lines or if the next line is empty
  106   def next_line_empty?
  107     peek_line.nil_or_empty?
  108   end
  109 
  110   # Public: Peek at the next line of source data. Processes the line if not
  111   # already marked as processed, but does not consume it.
  112   #
  113   # This method will probe the reader for more lines. If there is a next line
  114   # that has not previously been visited, the line is passed to the
  115   # Reader#process_line method to be initialized. This call gives
  116   # sub-classes the opportunity to do preprocessing. If the return value of
  117   # the Reader#process_line is nil, the data is assumed to be changed and
  118   # Reader#peek_line is invoked again to perform further processing.
  119   #
  120   # If has_more_lines? is called immediately before peek_line, the direct flag
  121   # is implicitly true (since the line is flagged as visited).
  122   #
  123   # direct  - A Boolean flag to bypasses the check for more lines and immediately
  124   #           returns the first element of the internal @lines Array. (default: false)
  125   #
  126   # Returns the next line of the source data as a String if there are lines remaining.
  127   # Returns nothing if there is no more data.
  128   def peek_line direct = false
  129     if direct || @look_ahead > 0
  130       @unescape_next_line ? ((line = @lines[0]).slice 1, line.length) : @lines[0]
  131     elsif @lines.empty?
  132       @look_ahead = 0
  133       nil
  134     else
  135       # FIXME the problem with this approach is that we aren't
  136       # retaining the modified line (hence the @unescape_next_line tweak)
  137       # perhaps we need a stack of proxied lines
  138       (line = process_line @lines[0]) ? line : peek_line
  139     end
  140   end
  141 
  142   # Public: Peek at the next multiple lines of source data. Processes the lines if not
  143   # already marked as processed, but does not consume them.
  144   #
  145   # This method delegates to Reader#read_line to process and collect the line, then
  146   # restores the lines to the stack before returning them. This allows the lines to
  147   # be processed and marked as such so that subsequent reads will not need to process
  148   # the lines again.
  149   #
  150   # num    - The positive Integer number of lines to peek or nil to peek all lines (default: nil).
  151   # direct - A Boolean indicating whether processing should be disabled when reading lines (default: false).
  152   #
  153   # Returns A String Array of the next multiple lines of source data, or an empty Array
  154   # if there are no more lines in this Reader.
  155   def peek_lines num = nil, direct = false
  156     old_look_ahead = @look_ahead
  157     result = []
  158     (num || MAX_INT).times do
  159       if (line = direct ? shift : read_line)
  160         result << line
  161       else
  162         @lineno -= 1 if direct
  163         break
  164       end
  165     end
  166 
  167     unless result.empty?
  168       unshift_all result
  169       @look_ahead = old_look_ahead if direct
  170     end
  171 
  172     result
  173   end
  174 
  175   # Public: Get the next line of source data. Consumes the line returned.
  176   #
  177   # Returns the String of the next line of the source data if data is present.
  178   # Returns nothing if there is no more data.
  179   def read_line
  180     # has_more_lines? triggers preprocessor
  181     shift if @look_ahead > 0 || has_more_lines?
  182   end
  183 
  184   # Public: Get the remaining lines of source data.
  185   #
  186   # This method calls Reader#read_line repeatedly until all lines are consumed
  187   # and returns the lines as a String Array. This method differs from
  188   # Reader#lines in that it processes each line in turn, hence triggering
  189   # any preprocessors implemented in sub-classes.
  190   #
  191   # Returns the lines read as a String Array
  192   def read_lines
  193     lines = []
  194     # has_more_lines? triggers preprocessor
  195     while has_more_lines?
  196       lines << shift
  197     end
  198     lines
  199   end
  200   alias readlines read_lines
  201 
  202   # Public: Get the remaining lines of source data joined as a String.
  203   #
  204   # Delegates to Reader#read_lines, then joins the result.
  205   #
  206   # Returns the lines read joined as a String
  207   def read
  208     read_lines.join LF
  209   end
  210 
  211   # Public: Advance to the next line by discarding the line at the front of the stack
  212   #
  213   # Returns a Boolean indicating whether there was a line to discard.
  214   def advance
  215     shift ? true : false
  216   end
  217 
  218   # Public: Push the String line onto the beginning of the Array of source data.
  219   #
  220   # A line pushed on the reader using this method is not processed again. The
  221   # method assumes the line was previously retrieved from the reader or does
  222   # not otherwise contain preprocessor directives. Therefore, it is marked as
  223   # processed immediately.
  224   #
  225   # line_to_restore - the line to restore onto the stack
  226   #
  227   # Returns nothing.
  228   def unshift_line line_to_restore
  229     unshift line_to_restore
  230     nil
  231   end
  232   alias restore_line unshift_line
  233 
  234   # Public: Push an Array of lines onto the front of the Array of source data.
  235   #
  236   # Lines pushed on the reader using this method are not processed again. The
  237   # method assumes the lines were previously retrieved from the reader or do
  238   # not otherwise contain preprocessor directives. Therefore, they are marked
  239   # as processed immediately.
  240   #
  241   # Returns nothing.
  242   def unshift_lines lines_to_restore
  243     unshift_all lines_to_restore
  244     nil
  245   end
  246   alias restore_lines unshift_lines
  247 
  248   # Public: Replace the next line with the specified line.
  249   #
  250   # Calls Reader#advance to consume the current line, then calls
  251   # Reader#unshift to push the replacement onto the top of the
  252   # line stack.
  253   #
  254   # replacement - The String line to put in place of the next line (i.e., the line at the cursor).
  255   #
  256   # Returns true.
  257   def replace_next_line replacement
  258     shift
  259     unshift replacement
  260     true
  261   end
  262   # deprecated
  263   alias replace_line replace_next_line
  264 
  265   # Public: Skip blank lines at the cursor.
  266   #
  267   # Examples
  268   #
  269   #   reader.lines
  270   #   => ["", "", "Foo", "Bar", ""]
  271   #   reader.skip_blank_lines
  272   #   => 2
  273   #   reader.lines
  274   #   => ["Foo", "Bar", ""]
  275   #
  276   # Returns the [Integer] number of lines skipped or nothing if all lines have
  277   # been consumed (even if lines were skipped by this method).
  278   def skip_blank_lines
  279     return if empty?
  280 
  281     num_skipped = 0
  282     # optimized code for shortest execution path
  283     while (next_line = peek_line)
  284       if next_line.empty?
  285         shift
  286         num_skipped += 1
  287       else
  288         return num_skipped
  289       end
  290     end
  291   end
  292 
  293   # Public: Skip consecutive comment lines and block comments.
  294   #
  295   # Examples
  296   #   @lines
  297   #   => ["// foo", "bar"]
  298   #
  299   #   comment_lines = skip_comment_lines
  300   #   => nil
  301   #
  302   #   @lines
  303   #   => ["bar"]
  304   #
  305   # Returns nothing
  306   def skip_comment_lines
  307     return if empty?
  308 
  309     while (next_line = peek_line) && !next_line.empty?
  310       if next_line.start_with? '//'
  311         if next_line.start_with? '///'
  312           if (ll = next_line.length) > 3 && next_line == '/' * ll
  313             read_lines_until terminator: next_line, skip_first_line: true, read_last_line: true, skip_processing: true, context: :comment
  314           else
  315             break
  316           end
  317         else
  318           shift
  319         end
  320       else
  321         break
  322       end
  323     end
  324 
  325     nil
  326   end
  327 
  328   # Public: Skip consecutive comment lines and return them.
  329   #
  330   # This method assumes the reader only contains simple lines (no blocks).
  331   def skip_line_comments
  332     return [] if empty?
  333 
  334     comment_lines = []
  335     # optimized code for shortest execution path
  336     while (next_line = peek_line) && !next_line.empty?
  337       if (next_line.start_with? '//')
  338         comment_lines << shift
  339       else
  340         break
  341       end
  342     end
  343 
  344     comment_lines
  345   end
  346 
  347   # Public: Advance to the end of the reader, consuming all remaining lines
  348   #
  349   # Returns nothing.
  350   def terminate
  351     @lineno += @lines.size
  352     @lines.clear
  353     @look_ahead = 0
  354     nil
  355   end
  356 
  357   # Public: Return all the lines from `@lines` until we (1) run out them,
  358   #   (2) find a blank line with `break_on_blank_lines: true`, or (3) find
  359   #   a line for which the given block evals to true.
  360   #
  361   # options - an optional Hash of processing options:
  362   #           * :terminator may be used to specify the contents of the line
  363   #               at which the reader should stop
  364   #           * :break_on_blank_lines may be used to specify to break on
  365   #               blank lines
  366   #           * :break_on_list_continuation may be used to specify to break
  367   #               on a list continuation line
  368   #           * :skip_first_line may be used to tell the reader to advance
  369   #               beyond the first line before beginning the scan
  370   #           * :preserve_last_line may be used to specify that the String
  371   #               causing the method to stop processing lines should be
  372   #               pushed back onto the `lines` Array.
  373   #           * :read_last_line may be used to specify that the String
  374   #               causing the method to stop processing lines should be
  375   #               included in the lines being returned
  376   #           * :skip_line_comments may be used to look for and skip
  377   #               line comments
  378   #           * :skip_processing is used to disable line (pre)processing
  379   #               for the duration of this method
  380   #
  381   # Returns the Array of lines forming the next segment.
  382   #
  383   # Examples
  384   #
  385   #   data = [
  386   #     "First line\n",
  387   #     "Second line\n",
  388   #     "\n",
  389   #     "Third line\n",
  390   #   ]
  391   #   reader = Reader.new data, nil, normalize: true
  392   #
  393   #   reader.read_lines_until
  394   #   => ["First line", "Second line"]
  395   def read_lines_until options = {}
  396     result = []
  397     if @process_lines && options[:skip_processing]
  398       @process_lines = false
  399       restore_process_lines = true
  400     end
  401     if (terminator = options[:terminator])
  402       start_cursor = options[:cursor] || cursor
  403       break_on_blank_lines = false
  404       break_on_list_continuation = false
  405     else
  406       break_on_blank_lines = options[:break_on_blank_lines]
  407       break_on_list_continuation = options[:break_on_list_continuation]
  408     end
  409     skip_comments = options[:skip_line_comments]
  410     complete = line_read = line_restored = nil
  411     shift if options[:skip_first_line]
  412     while !complete && (line = read_line)
  413       complete = while true
  414         break true if terminator && line == terminator
  415         # QUESTION: can we get away with line.empty? here?
  416         break true if break_on_blank_lines && line.empty?
  417         if break_on_list_continuation && line_read && line == LIST_CONTINUATION
  418           options[:preserve_last_line] = true
  419           break true
  420         end
  421         break true if block_given? && (yield line)
  422         break false
  423       end
  424       if complete
  425         if options[:read_last_line]
  426           result << line
  427           line_read = true
  428         end
  429         if options[:preserve_last_line]
  430           unshift line
  431           line_restored = true
  432         end
  433       else
  434         unless skip_comments && (line.start_with? '//') && !(line.start_with? '///')
  435           result << line
  436           line_read = true
  437         end
  438       end
  439     end
  440     if restore_process_lines
  441       @process_lines = true
  442       @look_ahead -= 1 if line_restored && !terminator
  443     end
  444     if terminator && terminator != line && (context = options.fetch :context, terminator)
  445       start_cursor = cursor_at_mark if start_cursor == :at_mark
  446       logger.warn message_with_context %(unterminated #{context} block), source_location: start_cursor
  447       @unterminated = true
  448     end
  449     result
  450   end
  451 
  452   # Internal: Shift the line off the stack and increment the lineno
  453   #
  454   # This method can be used directly when you've already called peek_line
  455   # and determined that you do, in fact, want to pluck that line off the stack.
  456   # Use read_line if the line hasn't (or many not have been) visited yet.
  457   #
  458   # Returns The String line at the top of the stack
  459   def shift
  460     @lineno += 1
  461     @look_ahead -= 1 unless @look_ahead == 0
  462     @lines.shift
  463   end
  464 
  465   # Internal: Restore the line to the stack and decrement the lineno
  466   def unshift line
  467     @lineno -= 1
  468     @look_ahead += 1
  469     @lines.unshift line
  470   end
  471 
  472   # Internal: Restore the lines to the stack and decrement the lineno
  473   def unshift_all lines
  474     @lineno -= lines.size
  475     @look_ahead += lines.size
  476     @lines.unshift(*lines)
  477   end
  478 
  479   def cursor
  480     Cursor.new @file, @dir, @path, @lineno
  481   end
  482 
  483   def cursor_at_line lineno
  484     Cursor.new @file, @dir, @path, lineno
  485   end
  486 
  487   def cursor_at_mark
  488     @mark ? Cursor.new(*@mark) : cursor
  489   end
  490 
  491   def cursor_before_mark
  492     if @mark
  493       m_file, m_dir, m_path, m_lineno = @mark
  494       Cursor.new m_file, m_dir, m_path, m_lineno - 1
  495     else
  496       Cursor.new @file, @dir, @path, @lineno - 1
  497     end
  498   end
  499 
  500   def cursor_at_prev_line
  501     Cursor.new @file, @dir, @path, @lineno - 1
  502   end
  503 
  504   def mark
  505     @mark = @file, @dir, @path, @lineno
  506   end
  507 
  508   # Public: Get information about the last line read, including file name and line number.
  509   #
  510   # Returns A String summary of the last line read
  511   def line_info
  512     %(#{@path}: line #{@lineno})
  513   end
  514 
  515   # Public: Get a copy of the remaining Array of String lines managed by this Reader
  516   #
  517   # Returns A copy of the String Array of lines remaining in this Reader
  518   def lines
  519     @lines.drop 0
  520   end
  521 
  522   # Public: Get a copy of the remaining lines managed by this Reader joined as a String
  523   def string
  524     @lines.join LF
  525   end
  526 
  527   # Public: Get the source lines for this Reader joined as a String
  528   def source
  529     @source_lines.join LF
  530   end
  531 
  532   # Internal: Save the state of the reader at cursor
  533   def save
  534     @saved = {}.tap do |accum|
  535       instance_variables.each do |name|
  536         unless name == :@saved || name == :@source_lines
  537           accum[name] = ::Array === (val = instance_variable_get name) ? (val.drop 0) : val
  538         end
  539       end
  540     end
  541     nil
  542   end
  543 
  544   # Internal: Restore the state of the reader at cursor
  545   def restore_save
  546     if @saved
  547       @saved.each do |name, val|
  548         instance_variable_set name, val
  549       end
  550       @saved = nil
  551     end
  552   end
  553 
  554   # Internal: Discard a previous saved state
  555   def discard_save
  556     @saved = nil
  557   end
  558 
  559   def to_s
  560     %(#<#{self.class}@#{object_id} {path: #{@path.inspect}, line: #{@lineno}}>)
  561   end
  562 
  563   private
  564 
  565   # Internal: Prepare the source data for parsing.
  566   #
  567   # Converts the source data into an Array of lines ready for parsing. If the +:normalize+ option is set, this method
  568   # coerces the encoding of each line to UTF-8 and strips trailing whitespace, including the newline. (This whitespace
  569   # cleaning is very important to how Asciidoctor works). Subclasses may choose to perform additional preparation.
  570   #
  571   # data - A String Array or String of source data to be normalized.
  572   # opts - A Hash of options to control how lines are prepared.
  573   #        :normalize - Enables line normalization, which coerces the encoding to UTF-8 and removes trailing whitespace
  574   #        (optional, default: false).
  575   #
  576   # Returns A String Array of source lines. If the source data is an Array, this method returns a copy.
  577   def prepare_lines data, opts = {}
  578     if opts[:normalize]
  579       ::Array === data ? (Helpers.prepare_source_array data) : (Helpers.prepare_source_string data)
  580     elsif ::Array === data
  581       data.drop 0
  582     elsif data
  583       data.split LF, -1
  584     else
  585       []
  586     end
  587   rescue
  588     if (::Array === data ? data.join : data.to_s).valid_encoding?
  589       raise
  590     else
  591       raise ::ArgumentError, 'source is either binary or contains invalid Unicode data'
  592     end
  593   end
  594 
  595   # Internal: Processes a previously unvisited line
  596   #
  597   # By default, this method marks the line as processed
  598   # by incrementing the look_ahead counter and returns
  599   # the line unmodified.
  600   #
  601   # Returns The String line the Reader should make available to the next
  602   # invocation of Reader#read_line or nil if the Reader should drop the line,
  603   # advance to the next line and process it.
  604   def process_line line
  605     @look_ahead += 1 if @process_lines
  606     line
  607   end
  608 end
  609 
  610 # Public: Methods for retrieving lines from AsciiDoc source files, evaluating preprocessor
  611 # directives as each line is read off the Array of lines.
  612 class PreprocessorReader < Reader
  613   attr_reader :include_stack
  614 
  615   # Public: Initialize the PreprocessorReader object
  616   def initialize document, data = nil, cursor = nil, opts = {}
  617     @document = document
  618     super data, cursor, opts
  619     if (default_include_depth = (document.attributes['max-include-depth'] || 64).to_i) > 0
  620       # track absolute max depth, current max depth for comparing to include stack size, and relative max depth for reporting
  621       @maxdepth = { abs: default_include_depth, curr: default_include_depth, rel: default_include_depth }
  622     else
  623       # if @maxdepth is not set, built-in include functionality is disabled
  624       @maxdepth = nil
  625     end
  626     @include_stack = []
  627     @includes = document.catalog[:includes]
  628     @skipping = false
  629     @conditional_stack = []
  630     @include_processor_extensions = nil
  631   end
  632 
  633   # (see Reader#has_more_lines?)
  634   def has_more_lines?
  635     peek_line ? true : false
  636   end
  637 
  638   # (see Reader#empty?)
  639   def empty?
  640     peek_line ? false : true
  641   end
  642   alias eof? empty?
  643 
  644   # Public: Override the Reader#peek_line method to pop the include
  645   # stack if the last line has been reached and there's at least
  646   # one include on the stack.
  647   #
  648   # Returns the next line of the source data as a String if there are lines remaining
  649   # in the current include context or a parent include context.
  650   # Returns nothing if there are no more lines remaining and the include stack is empty.
  651   def peek_line direct = false
  652     if (line = super)
  653       line
  654     elsif @include_stack.empty?
  655       nil
  656     else
  657       pop_include
  658       peek_line direct
  659     end
  660   end
  661 
  662   # Public: Push source onto the front of the reader and switch the context
  663   # based on the file, document-relative path and line information given.
  664   #
  665   # This method is typically used in an IncludeProcessor to add source
  666   # read from the target specified.
  667   #
  668   # Examples
  669   #
  670   #    path = 'partial.adoc'
  671   #    file = File.expand_path path
  672   #    data = File.read file
  673   #    reader.push_include data, file, path
  674   #
  675   # Returns this Reader object.
  676   def push_include data, file = nil, path = nil, lineno = 1, attributes = {}
  677     @include_stack << [@lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines]
  678     if (@file = file)
  679       # NOTE if file is not a string, assume it's a URI
  680       if ::String === file
  681         @dir = ::File.dirname file
  682       elsif RUBY_ENGINE_OPAL
  683         @dir = ::URI.parse ::File.dirname(file = file.to_s)
  684       else
  685         # NOTE this intentionally throws an error if URI has no path
  686         (@dir = file.dup).path = (dir = ::File.dirname file.path) == '/' ? '' : dir
  687         file = file.to_s
  688       end
  689       @path = (path ||= ::File.basename file)
  690       # only process lines in AsciiDoc files
  691       if (@process_lines = file.end_with?(*ASCIIDOC_EXTENSIONS.keys))
  692         @includes[path.slice 0, (path.rindex '.')] = attributes['partial-option'] ? nil : true
  693       end
  694     else
  695       @dir = '.'
  696       # we don't know what file type we have, so assume AsciiDoc
  697       @process_lines = true
  698       if (@path = path)
  699         @includes[Helpers.rootname path] = attributes['partial-option'] ? nil : true
  700       else
  701         @path = '<stdin>'
  702       end
  703     end
  704 
  705     @lineno = lineno
  706 
  707     if @maxdepth && (attributes.key? 'depth')
  708       if (rel_maxdepth = attributes['depth'].to_i) > 0
  709         if (curr_maxdepth = @include_stack.size + rel_maxdepth) > (abs_maxdepth = @maxdepth[:abs])
  710           # if relative depth exceeds absolute max depth, effectively ignore relative depth request
  711           curr_maxdepth = rel_maxdepth = abs_maxdepth
  712         end
  713         @maxdepth = { abs: abs_maxdepth, curr: curr_maxdepth, rel: rel_maxdepth }
  714       else
  715         @maxdepth = { abs: @maxdepth[:abs], curr: @include_stack.size, rel: 0 }
  716       end
  717     end
  718 
  719     # effectively fill the buffer
  720     if (@lines = prepare_lines data, normalize: true, condense: false, indent: attributes['indent']).empty?
  721       pop_include
  722     else
  723       # FIXME we eventually want to handle leveloffset without affecting the lines
  724       if attributes.key? 'leveloffset'
  725         @lines.unshift ''
  726         @lines.unshift %(:leveloffset: #{attributes['leveloffset']})
  727         @lines << ''
  728         if (old_leveloffset = @document.attr 'leveloffset')
  729           @lines << %(:leveloffset: #{old_leveloffset})
  730         else
  731           @lines << ':leveloffset!:'
  732         end
  733         # compensate for these extra lines
  734         @lineno -= 2
  735       end
  736 
  737       # FIXME kind of a hack
  738       #Document::AttributeEntry.new('infile', @file).save_to_next_block @document
  739       #Document::AttributeEntry.new('indir', @dir).save_to_next_block @document
  740       @look_ahead = 0
  741     end
  742     self
  743   end
  744 
  745   def include_depth
  746     @include_stack.size
  747   end
  748 
  749   # Public: Reports whether pushing an include on the include stack exceeds the max include depth.
  750   #
  751   # Returns nil if no max depth is set and includes are disabled (max-include-depth=0), false if the current max depth
  752   # will not be exceeded, and the relative max include depth if the current max depth will be exceed.
  753   def exceeds_max_depth?
  754     @maxdepth && @include_stack.size >= @maxdepth[:curr] && @maxdepth[:rel]
  755   end
  756   alias exceeded_max_depth? exceeds_max_depth?
  757 
  758   # TODO Document this override
  759   # also, we now have the field in the super class, so perhaps
  760   # just implement the logic there?
  761   def shift
  762     if @unescape_next_line
  763       @unescape_next_line = false
  764       (line = super).slice 1, line.length
  765     else
  766       super
  767     end
  768   end
  769 
  770   def include_processors?
  771     if @include_processor_extensions.nil?
  772       if @document.extensions? && @document.extensions.include_processors?
  773         !!(@include_processor_extensions = @document.extensions.include_processors)
  774       else
  775         @include_processor_extensions = false
  776       end
  777     else
  778       @include_processor_extensions != false
  779     end
  780   end
  781 
  782   def create_include_cursor file, path, lineno
  783     if ::String === file
  784       dir = ::File.dirname file
  785     elsif RUBY_ENGINE_OPAL
  786       dir = ::File.dirname(file = file.to_s)
  787     else
  788       dir = (dir = ::File.dirname file.path) == '' ? '/' : dir
  789       file = file.to_s
  790     end
  791     Cursor.new file, dir, path, lineno
  792   end
  793 
  794   def to_s
  795     %(#<#{self.class}@#{object_id} {path: #{@path.inspect}, line: #{@lineno}, include depth: #{@include_stack.size}, include stack: [#{@include_stack.map {|inc| inc.to_s }.join ', '}]}>)
  796   end
  797 
  798   private
  799 
  800   def prepare_lines data, opts = {}
  801     result = super
  802 
  803     # QUESTION should this work for AsciiDoc table cell content? Currently it does not.
  804     if @document && @document.attributes['skip-front-matter']
  805       if (front_matter = skip_front_matter! result)
  806         @document.attributes['front-matter'] = front_matter.join LF
  807       end
  808     end
  809 
  810     if opts.fetch :condense, true
  811       result.shift && @lineno += 1 while (first = result[0]) && first.empty?
  812       result.pop while (last = result[-1]) && last.empty?
  813     end
  814 
  815     Parser.adjust_indentation! result, opts[:indent].to_i, (@document.attr 'tabsize').to_i if opts[:indent]
  816 
  817     result
  818   end
  819 
  820   def process_line line
  821     return line unless @process_lines
  822 
  823     if line.empty?
  824       @look_ahead += 1
  825       return line
  826     end
  827 
  828     # NOTE highly optimized
  829     if line.end_with?(']') && !line.start_with?('[') && line.include?('::')
  830       if (line.include? 'if') && ConditionalDirectiveRx =~ line
  831         # if escaped, mark as processed and return line unescaped
  832         if $1 == '\\'
  833           @unescape_next_line = true
  834           @look_ahead += 1
  835           line.slice 1, line.length
  836         elsif preprocess_conditional_directive $2, $3, $4, $5
  837           # move the pointer past the conditional line
  838           shift
  839           # treat next line as uncharted territory
  840           nil
  841         else
  842           # the line was not a valid conditional line
  843           # mark it as visited and return it
  844           @look_ahead += 1
  845           line
  846         end
  847       elsif @skipping
  848         shift
  849         nil
  850       elsif (line.start_with? 'inc', '\\inc') && IncludeDirectiveRx =~ line
  851         # if escaped, mark as processed and return line unescaped
  852         if $1 == '\\'
  853           @unescape_next_line = true
  854           @look_ahead += 1
  855           line.slice 1, line.length
  856         # QUESTION should we strip whitespace from raw attributes in Substitutors#parse_attributes? (check perf)
  857         elsif preprocess_include_directive $2, $3
  858           # peek again since the content has changed
  859           nil
  860         else
  861           # the line was not a valid include line and is unchanged
  862           # mark it as visited and return it
  863           @look_ahead += 1
  864           line
  865         end
  866       else
  867         # NOTE optimization to inline super
  868         @look_ahead += 1
  869         line
  870       end
  871     elsif @skipping
  872       shift
  873       nil
  874     else
  875       # NOTE optimization to inline super
  876       @look_ahead += 1
  877       line
  878     end
  879   end
  880 
  881   # Internal: Preprocess the directive to conditionally include or exclude content.
  882   #
  883   # Preprocess the conditional directive (ifdef, ifndef, ifeval, endif) under
  884   # the cursor. If Reader is currently skipping content, then simply track the
  885   # open and close delimiters of any nested conditional blocks. If Reader is
  886   # not skipping, mark whether the condition is satisfied and continue
  887   # preprocessing recursively until the next line of available content is
  888   # found.
  889   #
  890   # keyword   - The conditional inclusion directive (ifdef, ifndef, ifeval, endif)
  891   # target    - The target, which is the name of one or more attributes that are
  892   #             used in the condition (blank in the case of the ifeval directive)
  893   # delimiter - The conditional delimiter for multiple attributes ('+' means all
  894   #             attributes must be defined or undefined, ',' means any of the attributes
  895   #             can be defined or undefined.
  896   # text      - The text associated with this directive (occurring between the square brackets)
  897   #             Used for a single-line conditional block in the case of the ifdef or
  898   #             ifndef directives, and for the conditional expression for the ifeval directive.
  899   #
  900   # Returns a Boolean indicating whether the cursor should be advanced
  901   def preprocess_conditional_directive keyword, target, delimiter, text
  902     # attributes are case insensitive
  903     target = target.downcase unless (no_target = target.empty?)
  904 
  905     if keyword == 'endif'
  906       if text
  907         logger.error message_with_context %(malformed preprocessor directive - text not permitted: endif::#{target}[#{text}]), source_location: cursor
  908       elsif @conditional_stack.empty?
  909         logger.error message_with_context %(unmatched preprocessor directive: endif::#{target}[]), source_location: cursor
  910       elsif no_target || target == (pair = @conditional_stack[-1])[:target]
  911         @conditional_stack.pop
  912         @skipping = @conditional_stack.empty? ? false : @conditional_stack[-1][:skipping]
  913       else
  914         logger.error message_with_context %(mismatched preprocessor directive: endif::#{target}[], expected endif::#{pair[:target]}[]), source_location: cursor
  915       end
  916       return true
  917     elsif @skipping
  918       skip = false
  919     else
  920       # QUESTION any way to wrap ifdef & ifndef logic up together?
  921       case keyword
  922       when 'ifdef'
  923         if no_target
  924           logger.error message_with_context %(malformed preprocessor directive - missing target: ifdef::[#{text}]), source_location: cursor
  925           return true
  926         end
  927         case delimiter
  928         when ','
  929           # skip if no attribute is defined
  930           skip = target.split(',', -1).none? {|name| @document.attributes.key? name }
  931         when '+'
  932           # skip if any attribute is undefined
  933           skip = target.split('+', -1).any? {|name| !@document.attributes.key? name }
  934         else
  935           # if the attribute is undefined, then skip
  936           skip = !@document.attributes.key?(target)
  937         end
  938       when 'ifndef'
  939         if no_target
  940           logger.error message_with_context %(malformed preprocessor directive - missing target: ifndef::[#{text}]), source_location: cursor
  941           return true
  942         end
  943         case delimiter
  944         when ','
  945           # skip if any attribute is defined
  946           skip = target.split(',', -1).any? {|name| @document.attributes.key? name }
  947         when '+'
  948           # skip if all attributes are defined
  949           skip = target.split('+', -1).all? {|name| @document.attributes.key? name }
  950         else
  951           # if the attribute is defined, then skip
  952           skip = @document.attributes.key?(target)
  953         end
  954       when 'ifeval'
  955         if no_target
  956           # the text in brackets must match a conditional expression
  957           if text && EvalExpressionRx =~ text.strip
  958             lhs = $1
  959             op = $2
  960             rhs = $3
  961             # regex enforces a restricted set of math-related operations (==, !=, <=, >=, <, >)
  962             skip = ((resolve_expr_val lhs).send op, (resolve_expr_val rhs)) ? false : true
  963           else
  964             logger.error message_with_context %(malformed preprocessor directive - #{text ? 'invalid expression' : 'missing expression'}: ifeval::[#{text}]), source_location: cursor
  965             return true
  966           end
  967         else
  968           logger.error message_with_context %(malformed preprocessor directive - target not permitted: ifeval::#{target}[#{text}]), source_location: cursor
  969           return true
  970         end
  971       end
  972     end
  973 
  974     # conditional inclusion block
  975     if keyword == 'ifeval' || !text
  976       @skipping = true if skip
  977       @conditional_stack << { target: target, skip: skip, skipping: @skipping }
  978     # single line conditional inclusion
  979     else
  980       unless @skipping || skip
  981         replace_next_line text.rstrip
  982         # HACK push dummy line to stand in for the opening conditional directive that's subsequently dropped
  983         unshift ''
  984         # NOTE force line to be processed again if it looks like an include directive
  985         # QUESTION should we just call preprocess_include_directive here?
  986         @look_ahead -= 1 if text.start_with? 'include::'
  987       end
  988     end
  989 
  990     true
  991   end
  992 
  993   # Internal: Preprocess the directive to include lines from another document.
  994   #
  995   # Preprocess the directive to include the target document. The scenarios
  996   # are as follows:
  997   #
  998   # If SafeMode is SECURE or greater, the directive is ignore and the include
  999   # directive line is emitted verbatim.
 1000   #
 1001   # Otherwise, if an include processor is specified pass the target and
 1002   # attributes to that processor and expect an Array of String lines in return.
 1003   #
 1004   # Otherwise, if the max depth is greater than 0, and is not exceeded by the
 1005   # stack size, normalize the target path and read the lines onto the beginning
 1006   # of the Array of source data.
 1007   #
 1008   # If none of the above apply, emit the include directive line verbatim.
 1009   #
 1010   # target   - The unsubstituted String name of the target document to include as specified in the
 1011   #            target slot of the include directive.
 1012   # attrlist - An attribute list String, which is the text between the square brackets of the
 1013   #            include directive.
 1014   #
 1015   # Returns a [Boolean] indicating whether the line under the cursor was changed. To skip over the
 1016   # directive, call shift and return true.
 1017   def preprocess_include_directive target, attrlist
 1018     doc = @document
 1019     if ((expanded_target = target).include? ATTR_REF_HEAD) &&
 1020         (expanded_target = doc.sub_attributes target, attribute_missing: ((attr_missing = doc.attributes['attribute-missing'] || Compliance.attribute_missing) == 'warn' ? 'drop-line' : attr_missing)).empty?
 1021       if attr_missing == 'drop-line' && (doc.sub_attributes target + ' ', attribute_missing: 'drop-line', drop_line_severity: :ignore).empty?
 1022         logger.info { message_with_context %(include dropped due to missing attribute: include::#{target}[#{attrlist}]), source_location: cursor }
 1023         shift
 1024         true
 1025       elsif (doc.parse_attributes attrlist, [], sub_input: true)['optional-option']
 1026         logger.info { message_with_context %(optional include dropped #{attr_missing == 'warn' && (doc.sub_attributes target + ' ', attribute_missing: 'drop-line', drop_line_severity: :ignore).empty? ? 'due to missing attribute' : 'because resolved target is blank'}: include::#{target}[#{attrlist}]), source_location: cursor }
 1027         shift
 1028         true
 1029       else
 1030         logger.warn message_with_context %(include dropped #{attr_missing == 'warn' && (doc.sub_attributes target + ' ', attribute_missing: 'drop-line', drop_line_severity: :ignore).empty? ? 'due to missing attribute' : 'because resolved target is blank'}: include::#{target}[#{attrlist}]), source_location: cursor
 1031         # QUESTION should this line include target or expanded_target (or escaped target?)
 1032         replace_next_line %(Unresolved directive in #{@path} - include::#{target}[#{attrlist}])
 1033       end
 1034     elsif include_processors? && (ext = @include_processor_extensions.find {|candidate| candidate.instance.handles? expanded_target })
 1035       shift
 1036       # FIXME parse attributes only if requested by extension
 1037       ext.process_method[doc, self, expanded_target, (doc.parse_attributes attrlist, [], sub_input: true)]
 1038       true
 1039     # if running in SafeMode::SECURE or greater, don't process this directive
 1040     # however, be friendly and at least make it a link to the source document
 1041     elsif doc.safe >= SafeMode::SECURE
 1042       # FIXME we don't want to use a link macro if we are in a verbatim context
 1043       replace_next_line %(link:#{expanded_target}[])
 1044     elsif @maxdepth
 1045       if @include_stack.size >= @maxdepth[:curr]
 1046         logger.error message_with_context %(maximum include depth of #{@maxdepth[:rel]} exceeded), source_location: cursor
 1047         return
 1048       end
 1049 
 1050       parsed_attrs = doc.parse_attributes attrlist, [], sub_input: true
 1051       inc_path, target_type, relpath = resolve_include_path expanded_target, attrlist, parsed_attrs
 1052       if target_type == :file
 1053         reader = ::File.method :open
 1054         read_mode = FILE_READ_MODE
 1055       elsif target_type == :uri
 1056         reader = ::OpenURI.method :open_uri
 1057         read_mode = URI_READ_MODE
 1058       else
 1059         # NOTE if target_type is not set, inc_path is a boolean to skip over (false) or reevaluate (true) the current line
 1060         return inc_path
 1061       end
 1062 
 1063       inc_linenos = inc_tags = nil
 1064       if attrlist
 1065         if parsed_attrs.key? 'lines'
 1066           inc_linenos = []
 1067           (split_delimited_value parsed_attrs['lines']).each do |linedef|
 1068             if linedef.include? '..'
 1069               from, _, to = linedef.partition '..'
 1070               inc_linenos += (to.empty? || (to = to.to_i) < 0) ? [from.to_i, 1.0/0.0] : (from.to_i..to).to_a
 1071             else
 1072               inc_linenos << linedef.to_i
 1073             end
 1074           end
 1075           inc_linenos = inc_linenos.empty? ? nil : inc_linenos.sort.uniq
 1076         elsif parsed_attrs.key? 'tag'
 1077           unless (tag = parsed_attrs['tag']).empty? || tag == '!'
 1078             inc_tags = (tag.start_with? '!') ? { (tag.slice 1, tag.length) => false } : { tag => true }
 1079           end
 1080         elsif parsed_attrs.key? 'tags'
 1081           inc_tags = {}
 1082           (split_delimited_value parsed_attrs['tags']).each do |tagdef|
 1083             if tagdef.start_with? '!'
 1084               inc_tags[tagdef.slice 1, tagdef.length] = false
 1085             else
 1086               inc_tags[tagdef] = true
 1087             end unless tagdef.empty? || tagdef == '!'
 1088           end
 1089           inc_tags = nil if inc_tags.empty?
 1090         end
 1091       end
 1092 
 1093       if inc_linenos
 1094         inc_lines, inc_offset, inc_lineno = [], nil, 0
 1095         begin
 1096           reader.call inc_path, read_mode do |f|
 1097             select_remaining = nil
 1098             f.each_line do |l|
 1099               inc_lineno += 1
 1100               if select_remaining || (::Float === (select = inc_linenos[0]) && (select_remaining = select.infinite?))
 1101                 # NOTE record line where we started selecting
 1102                 inc_offset ||= inc_lineno
 1103                 inc_lines << l
 1104               else
 1105                 if select == inc_lineno
 1106                   # NOTE record line where we started selecting
 1107                   inc_offset ||= inc_lineno
 1108                   inc_lines << l
 1109                   inc_linenos.shift
 1110                 end
 1111                 break if inc_linenos.empty?
 1112               end
 1113             end
 1114           end
 1115         rescue
 1116           logger.error message_with_context %(include #{target_type} not readable: #{inc_path}), source_location: cursor
 1117           return replace_next_line %(Unresolved directive in #{@path} - include::#{expanded_target}[#{attrlist}])
 1118         end
 1119         shift
 1120         # FIXME not accounting for skipped lines in reader line numbering
 1121         if inc_offset
 1122           parsed_attrs['partial-option'] = ''
 1123           push_include inc_lines, inc_path, relpath, inc_offset, parsed_attrs
 1124         end
 1125       elsif inc_tags
 1126         inc_lines, inc_offset, inc_lineno, tag_stack, tags_used, active_tag = [], nil, 0, [], ::Set.new, nil
 1127         if inc_tags.key? '**'
 1128           if inc_tags.key? '*'
 1129             select = base_select = inc_tags.delete '**'
 1130             wildcard = inc_tags.delete '*'
 1131           else
 1132             select = base_select = wildcard = inc_tags.delete '**'
 1133           end
 1134         else
 1135           select = base_select = !(inc_tags.value? true)
 1136           wildcard = inc_tags.delete '*'
 1137         end
 1138         begin
 1139           reader.call inc_path, read_mode do |f|
 1140             dbl_co, dbl_sb = '::', '[]'
 1141             f.each_line do |l|
 1142               inc_lineno += 1
 1143               if (l.include? dbl_co) && (l.include? dbl_sb) && TagDirectiveRx =~ l
 1144                 this_tag = $2
 1145                 if $1 # end tag
 1146                   if this_tag == active_tag
 1147                     tag_stack.pop
 1148                     active_tag, select = tag_stack.empty? ? [nil, base_select] : tag_stack[-1]
 1149                   elsif inc_tags.key? this_tag
 1150                     include_cursor = create_include_cursor inc_path, expanded_target, inc_lineno
 1151                     if (idx = tag_stack.rindex {|key, _| key == this_tag })
 1152                       idx == 0 ? tag_stack.shift : (tag_stack.delete_at idx)
 1153                       logger.warn message_with_context %(mismatched end tag (expected '#{active_tag}' but found '#{this_tag}') at line #{inc_lineno} of include #{target_type}: #{inc_path}), source_location: cursor, include_location: include_cursor
 1154                     else
 1155                       logger.warn message_with_context %(unexpected end tag '#{this_tag}' at line #{inc_lineno} of include #{target_type}: #{inc_path}), source_location: cursor, include_location: include_cursor
 1156                     end
 1157                   end
 1158                 elsif inc_tags.key? this_tag
 1159                   tags_used << this_tag
 1160                   # QUESTION should we prevent tag from being selected when enclosing tag is excluded?
 1161                   tag_stack << [(active_tag = this_tag), (select = inc_tags[this_tag]), inc_lineno]
 1162                 elsif !wildcard.nil?
 1163                   select = active_tag && !select ? false : wildcard
 1164                   tag_stack << [(active_tag = this_tag), select, inc_lineno]
 1165                 end
 1166               elsif select
 1167                 # NOTE record the line where we started selecting
 1168                 inc_offset ||= inc_lineno
 1169                 inc_lines << l
 1170               end
 1171             end
 1172           end
 1173         rescue
 1174           logger.error message_with_context %(include #{target_type} not readable: #{inc_path}), source_location: cursor
 1175           return replace_next_line %(Unresolved directive in #{@path} - include::#{expanded_target}[#{attrlist}])
 1176         end
 1177         unless tag_stack.empty?
 1178           tag_stack.each do |tag_name, _, tag_lineno|
 1179             logger.warn message_with_context %(detected unclosed tag '#{tag_name}' starting at line #{tag_lineno} of include #{target_type}: #{inc_path}), source_location: cursor, include_location: (create_include_cursor inc_path, expanded_target, tag_lineno)
 1180           end
 1181         end
 1182         unless (missing_tags = inc_tags.keys - tags_used.to_a).empty?
 1183           logger.warn message_with_context %(tag#{missing_tags.size > 1 ? 's' : ''} '#{missing_tags.join ', '}' not found in include #{target_type}: #{inc_path}), source_location: cursor
 1184         end
 1185         shift
 1186         if inc_offset
 1187           parsed_attrs['partial-option'] = '' unless base_select && wildcard && inc_tags.empty?
 1188           # FIXME not accounting for skipped lines in reader line numbering
 1189           push_include inc_lines, inc_path, relpath, inc_offset, parsed_attrs
 1190         end
 1191       else
 1192         begin
 1193           # NOTE read content before shift so cursor is only advanced if IO operation succeeds
 1194           inc_content = reader.call(inc_path, read_mode) {|f| f.read }
 1195           shift
 1196           push_include inc_content, inc_path, relpath, 1, parsed_attrs
 1197         rescue
 1198           logger.error message_with_context %(include #{target_type} not readable: #{inc_path}), source_location: cursor
 1199           return replace_next_line %(Unresolved directive in #{@path} - include::#{expanded_target}[#{attrlist}])
 1200         end
 1201       end
 1202       true
 1203     end
 1204   end
 1205 
 1206   # Internal: Resolve the target of an include directive.
 1207   #
 1208   # An internal method to resolve the target of an include directive. This method must return an
 1209   # Array containing the resolved (absolute) path of the target, the target type (:file or :uri),
 1210   # and the path of the target relative to the outermost document. Alternately, the method may
 1211   # return a boolean to halt processing of the include directive line and to indicate whether the
 1212   # cursor should be advanced beyond this line (true) or the line should be reprocessed (false).
 1213   #
 1214   # This method is overridden in Asciidoctor.js to resolve the target of an include in the browser
 1215   # environment.
 1216   #
 1217   # target     - A String containing the unresolved include target.
 1218   #              (Attribute references in target value have already been resolved).
 1219   # attrlist   - An attribute list String (i.e., the text between the square brackets).
 1220   # attributes - A Hash of attributes parsed from attrlist.
 1221   #
 1222   # Returns An Array containing the resolved (absolute) include path, the target type, and the path
 1223   # relative to the outermost document. May also return a boolean to halt processing of the include.
 1224   def resolve_include_path target, attrlist, attributes
 1225     doc = @document
 1226     if (Helpers.uriish? target) || (::String === @dir ? nil : (target = %(#{@dir}/#{target})))
 1227       return replace_next_line %(link:#{target}[#{attrlist}]) unless doc.attr? 'allow-uri-read'
 1228       if doc.attr? 'cache-uri'
 1229         # caching requires the open-uri-cached gem to be installed
 1230         # processing will be automatically aborted if these libraries can't be opened
 1231         Helpers.require_library 'open-uri/cached', 'open-uri-cached' unless defined? ::OpenURI::Cache
 1232       elsif !RUBY_ENGINE_OPAL
 1233         # autoload open-uri
 1234         ::OpenURI
 1235       end
 1236       [(::URI.parse target), :uri, target]
 1237     else
 1238       # include file is resolved relative to dir of current include, or base_dir if within original docfile
 1239       inc_path = doc.normalize_system_path target, @dir, nil, target_name: 'include file'
 1240       unless ::File.file? inc_path
 1241         if attributes['optional-option']
 1242           logger.info { message_with_context %(optional include dropped because include file not found: #{inc_path}), source_location: cursor }
 1243           shift
 1244           return true
 1245         else
 1246           logger.error message_with_context %(include file not found: #{inc_path}), source_location: cursor
 1247           return replace_next_line %(Unresolved directive in #{@path} - include::#{target}[#{attrlist}])
 1248         end
 1249       end
 1250       # NOTE relpath is the path relative to the root document (or base_dir, if set)
 1251       # QUESTION should we move relative_path method to Document
 1252       relpath = doc.path_resolver.relative_path inc_path, doc.base_dir
 1253       [inc_path, :file, relpath]
 1254     end
 1255   end
 1256 
 1257   def pop_include
 1258     if @include_stack.size > 0
 1259       @lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines = @include_stack.pop
 1260       # FIXME kind of a hack
 1261       #Document::AttributeEntry.new('infile', @file).save_to_next_block @document
 1262       #Document::AttributeEntry.new('indir', ::File.dirname(@file)).save_to_next_block @document
 1263       @look_ahead = 0
 1264       nil
 1265     end
 1266   end
 1267 
 1268   # Private: Split delimited value on comma (if found), otherwise semi-colon
 1269   def split_delimited_value val
 1270     (val.include? ',') ? (val.split ',') : (val.split ';')
 1271   end
 1272 
 1273   # Private: Ignore front-matter, commonly used in static site generators
 1274   def skip_front_matter! data, increment_linenos = true
 1275     front_matter = nil
 1276     if data[0] == '---'
 1277       original_data = data.drop 0
 1278       data.shift
 1279       front_matter = []
 1280       @lineno += 1 if increment_linenos
 1281       while !data.empty? && data[0] != '---'
 1282         front_matter << data.shift
 1283         @lineno += 1 if increment_linenos
 1284       end
 1285 
 1286       if data.empty?
 1287         data.unshift(*original_data)
 1288         @lineno = 0 if increment_linenos
 1289         front_matter = nil
 1290       else
 1291         data.shift
 1292         @lineno += 1 if increment_linenos
 1293       end
 1294     end
 1295 
 1296     front_matter
 1297   end
 1298 
 1299   # Private: Resolve the value of one side of the expression
 1300   #
 1301   # Examples
 1302   #
 1303   #   expr = '"value"'
 1304   #   resolve_expr_val expr
 1305   #   # => "value"
 1306   #
 1307   #   expr = '"value'
 1308   #   resolve_expr_val expr
 1309   #   # => "\"value"
 1310   #
 1311   #   expr = '"{undefined}"'
 1312   #   resolve_expr_val expr
 1313   #   # => ""
 1314   #
 1315   #   expr = '{undefined}'
 1316   #   resolve_expr_val expr
 1317   #   # => nil
 1318   #
 1319   #   expr = '2'
 1320   #   resolve_expr_val expr
 1321   #   # => 2
 1322   #
 1323   #   @document.attributes['name'] = 'value'
 1324   #   expr = '"{name}"'
 1325   #   resolve_expr_val expr
 1326   #   # => "value"
 1327   #
 1328   # Returns The value of the expression, coerced to the appropriate type
 1329   def resolve_expr_val val
 1330     if ((val.start_with? '"') && (val.end_with? '"')) ||
 1331         ((val.start_with? '\'') && (val.end_with? '\''))
 1332       quoted = true
 1333       val = val.slice 1, (val.length - 1)
 1334     else
 1335       quoted = false
 1336     end
 1337 
 1338     # QUESTION should we substitute first?
 1339     # QUESTION should we also require string to be single quoted (like block attribute values?)
 1340     val = @document.sub_attributes val, attribute_missing: 'drop' if val.include? ATTR_REF_HEAD
 1341 
 1342     if quoted
 1343       val
 1344     elsif val.empty?
 1345       nil
 1346     elsif val == 'true'
 1347       true
 1348     elsif val == 'false'
 1349       false
 1350     elsif val.rstrip.empty?
 1351       ' '
 1352     elsif val.include? '.'
 1353       val.to_f
 1354     else
 1355       # fallback to coercing to integer, since we
 1356       # require string values to be explicitly quoted
 1357       val.to_i
 1358     end
 1359   end
 1360 end
 1361 end