diff options
| author | Dan Allen <dan.j.allen@gmail.com> | 2013-08-17 22:09:44 -0600 |
|---|---|---|
| committer | Dan Allen <dan.j.allen@gmail.com> | 2013-08-21 16:17:45 -0600 |
| commit | 8dc9e1c796d8466c540a484625d0919da85ebf27 (patch) | |
| tree | 890b6080f7d8d3b3b8f4aab3fe1899cfdb9b464e | |
| parent | 7e86094b58e9f9e04f65cb70b05206d7668aa3b8 (diff) | |
resolves #575, #572 and #581 refactor reader to track include stack
- split the Reader into Reader and PreprocessorReader
- maintain an internal stack of include contexts
- report file names with line numbers
- rework reader tests
- rename several reader methods to be more intuitive
- optimize operations in the readers
| -rw-r--r-- | asciidoctor.gemspec | 3 | ||||
| -rw-r--r-- | lib/asciidoctor/document.rb | 22 | ||||
| -rw-r--r-- | lib/asciidoctor/lexer.rb | 102 | ||||
| -rw-r--r-- | lib/asciidoctor/reader.rb | 1185 | ||||
| -rw-r--r-- | lib/asciidoctor/table.rb | 15 | ||||
| -rw-r--r-- | test/document_test.rb | 24 | ||||
| -rw-r--r-- | test/fixtures/basic-docinfo.xml | 6 | ||||
| -rw-r--r-- | test/fixtures/child-include.adoc | 3 | ||||
| -rw-r--r-- | test/fixtures/parent-include.adoc | 5 | ||||
| -rw-r--r-- | test/lexer_test.rb | 6 | ||||
| -rw-r--r-- | test/lists_test.rb | 4 | ||||
| -rw-r--r-- | test/reader_test.rb | 1711 | ||||
| -rw-r--r-- | test/tables_test.rb | 2 | ||||
| -rw-r--r-- | test/test_helper.rb | 9 | ||||
| -rw-r--r-- | test/text_test.rb | 12 |
15 files changed, 1784 insertions, 1325 deletions
diff --git a/asciidoctor.gemspec b/asciidoctor.gemspec index 2d5ede13..92772d4f 100644 --- a/asciidoctor.gemspec +++ b/asciidoctor.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| ## Rake build (see the validate task) s.name = 'asciidoctor' s.version = '0.1.4.preview.4' - s.date = '2013-08-12' + s.date = '2013-08-21' s.rubyforge_project = 'asciidoctor' s.summary = 'A native Ruby AsciiDoc syntax processor and publishing toolchain' @@ -63,6 +63,7 @@ EOS lib/asciidoctor/backends/_stylesheets.rb lib/asciidoctor/backends/base_template.rb lib/asciidoctor/backends/docbook45.rb + lib/asciidoctor/backends/docbook5.rb lib/asciidoctor/backends/html5.rb lib/asciidoctor/block.rb lib/asciidoctor/callouts.rb diff --git a/lib/asciidoctor/document.rb b/lib/asciidoctor/document.rb index 9d8966d0..99e3103b 100644 --- a/lib/asciidoctor/document.rb +++ b/lib/asciidoctor/document.rb @@ -25,9 +25,12 @@ class Document < AbstractBlock end def save_to(block_attributes) - block_attributes[:attribute_entries] ||= [] - block_attributes[:attribute_entries] << self + (block_attributes[:attribute_entries] ||= []) << self end + + #def save_to_next_block(document) + # (document.attributes[:pending_attribute_entries] ||= []) << self + #end end # Public A read-only integer value indicating the level of security that @@ -198,7 +201,7 @@ class Document < AbstractBlock @attribute_overrides['embedded'] = @options[:header_footer] ? nil : '' # the only way to set the include-depth attribute is via the document options - # 10 is the AsciiDoc default, though currently Asciidoctor only supports 1 level + # 10 is the AsciiDoc default @attribute_overrides['include-depth'] ||= 10 # the only way to enable uri reads is via the document options, disabled by default @@ -282,6 +285,9 @@ class Document < AbstractBlock @attributes['doctype'] ||= DEFAULT_DOCTYPE update_backend_attributes + @attributes['indir'] = @attributes['docdir'] + @attributes['infile'] = @attributes['docfile'] + # dynamic intrinstic attribute values now = Time.new @attributes['localdate'] ||= now.strftime('%Y-%m-%d') @@ -301,9 +307,11 @@ class Document < AbstractBlock if @parent_document # don't need to do the extra processing within our own document - @reader = Reader.new(data, self) + # FIXME how are line numbers being tracked in this case?!?! + @reader = Reader.new data else - @reader = Reader.new(data, self, true, &block) + # TODO review the docfile logic here + @reader = PreprocessorReader.new self, data, ((@attributes.has_key? 'docfile') ? File.basename(@attributes['docfile']) : nil) end # Now parse the lines in the reader into blocks @@ -401,12 +409,12 @@ class Document < AbstractBlock # Make the raw source for the Document available. def source - @reader.source.join if @reader + @reader.source if @reader end # Make the raw source lines for the Document available. def source_lines - @reader.source if @reader + @reader.source_lines if @reader end def doctype diff --git a/lib/asciidoctor/lexer.rb b/lib/asciidoctor/lexer.rb index b13f78b0..f288d956 100644 --- a/lib/asciidoctor/lexer.rb +++ b/lib/asciidoctor/lexer.rb @@ -138,7 +138,7 @@ class Lexer document.attributes['mantitle'] = document.sub_attributes(m[1].rstrip.downcase) document.attributes['manvolnum'] = m[2].strip else - warn "asciidoctor: ERROR: line #{reader.lineno}: malformed manpage title" + warn "asciidoctor: ERROR: #{reader.prev_line_info}: malformed manpage title" end reader.skip_blank_lines @@ -146,7 +146,7 @@ class Lexer if is_next_line_section?(reader, {}) name_section = initialize_section(reader, document, {}) if name_section.level == 1 - name_section_buffer = reader.grab_lines_until(:break_on_blank_lines => true).join.tr_s("\n ", ' ') + name_section_buffer = reader.take_lines_until(:break_on_blank_lines => true).join.tr_s("\n ", ' ') if (m = name_section_buffer.match(REGEXP[:manname_manpurpose])) document.attributes['manname'] = m[1] document.attributes['manpurpose'] = m[2] @@ -157,13 +157,13 @@ class Lexer document.attributes['outfilesuffix'] = ".#{document.attributes['manvolnum']}" end else - warn "asciidoctor: ERROR: line #{reader.lineno}: malformed name section body" + warn "asciidoctor: ERROR: #{reader.prev_line_info}: malformed name section body" end else - warn "asciidoctor: ERROR: line #{reader.lineno}: name section title must be at level 1" + warn "asciidoctor: ERROR: #{reader.prev_line_info}: name section title must be at level 1" end else - warn "asciidoctor: ERROR: line #{reader.lineno}: name section expected" + warn "asciidoctor: ERROR: #{reader.prev_line_info}: name section expected" end end @@ -265,9 +265,9 @@ class Lexer doctype = parent.document.doctype if next_level > current_level || (section.is_a?(Document) && next_level == 0) if next_level == 0 && doctype != 'book' - warn "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections" + warn "asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections" elsif !expected_next_levels.nil? && !expected_next_levels.include?(next_level) - warn "asciidoctor: WARNING: line #{reader.lineno + 1}: section title out of sequence: " + + warn "asciidoctor: WARNING: #{reader.line_info}: section title out of sequence: " + "expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, " + "got level #{next_level}" end @@ -276,7 +276,7 @@ class Lexer section << new_section else if next_level == 0 && doctype != 'book' - warn "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections" + warn "asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections" end # close this section (and break out of the nesting) to begin a new one break @@ -362,7 +362,7 @@ class Lexer end # QUESTION should we introduce a parsing context object? - this_line = reader.get_line + this_line = reader.read_line delimited_block = false block_context = nil terminator = nil @@ -383,7 +383,7 @@ class Lexer elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style) block_context = :admonition else - warn "asciidoctor: WARNING: line #{reader.lineno}: invalid style for #{block_context} block: #{style}" + warn "asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for #{block_context} block: #{style}" style = block_context.to_s end end @@ -474,7 +474,7 @@ class Lexer begin # might want to move this check to a validate method if match[1].to_i != expected_index - warn "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}" + warn "asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: callout list item index: expected #{expected_index} got #{match[1]}" end list_item = next_list_item(reader, block, match) expected_index += 1 @@ -484,7 +484,7 @@ class Lexer if !coids.empty? list_item.attributes['coids'] = coids else - warn "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{block.items.size}" + warn "asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: no callouts refer to list item #{block.items.size}" end end end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist]) @@ -550,7 +550,7 @@ class Lexer # advance to block parsing => break else - warn "asciidoctor: WARNING: line #{reader.lineno}: invalid style for paragraph: #{style}" + warn "asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for paragraph: #{style}" style = nil # continue to process paragraph end @@ -560,9 +560,9 @@ class Lexer # a literal paragraph is contiguous lines starting at least one space if style != 'normal' && this_line.match(REGEXP[:lit_par]) - # So we need to actually include this one in the grab_lines group + # So we need to actually include this one in the take_lines group reader.unshift_line this_line - lines = reader.grab_lines_until( + lines = reader.take_lines_until( :break_on_blank_lines => true, :break_on_list_continuation => true, :preserve_last_line => true) {|line| @@ -584,7 +584,7 @@ class Lexer # a paragraph is contiguous nonblank/noncontinuation lines else reader.unshift_line this_line - lines = reader.grab_lines_until( + lines = reader.take_lines_until( :break_on_blank_lines => true, :break_on_list_continuation => true, :preserve_last_line => true, @@ -602,7 +602,7 @@ class Lexer # were the only lines found if lines.empty? # call get_line since the reader preserved the last line - reader.get_line + reader.read_line return nil end @@ -638,6 +638,7 @@ class Lexer attributes['citetitle'] = citetitle unless citetitle.nil? # NOTE will only detect headings that are floating titles (not section titles) # TODO could assume a floating title when inside a block context + # FIXME Reader needs to be created w/ line info block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes) elsif !text_only && lines.size > 1 && first_line.start_with?('"') && lines.last.start_with?('-- ') && lines[-2].chomp.end_with?('"') @@ -707,7 +708,10 @@ class Lexer block = build_block(block_context, :compound, terminator, parent, reader, attributes) when :table - block_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true) + file = reader.file + path = reader.path + lineno = reader.lineno + block_reader = Reader.new reader.take_lines_until(:terminator => terminator, :skip_line_comments => true), file, path, lineno case terminator[0..0] when ',' attributes['format'] = 'csv' @@ -722,7 +726,7 @@ class Lexer else # this should only happen if there is a misconfiguration - raise "Unsupported block type #{block_context} at line #{reader.lineno}" + raise "Unsupported block type #{block_context} at #{reader.line_info}" end end end @@ -746,6 +750,12 @@ class Lexer end block.update_attributes(attributes) + #if document.attributes.has_key? :pending_attribute_entries + # document.attributes.delete(:pending_attribute_entries).each do |entry| + # entry.save_to block.attributes + # end + #end + # FIXME callout capabilities should be a setting on the block if block.context == :listing || block.context == :literal catalog_callouts(block.source, document) @@ -820,10 +830,10 @@ class Lexer if terminator.nil? if parse_as_content_model == :verbatim - lines = reader.grab_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true) + lines = reader.take_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true) else content_model = :simple if content_model == :compound - lines = reader.grab_lines_until( + lines = reader.take_lines_until( :break_on_blank_lines => true, :break_on_list_continuation => true, :preserve_last_line => true, @@ -834,7 +844,7 @@ class Lexer end block_reader = nil elsif parse_as_content_model != :compound - lines = reader.grab_lines_until(:terminator => terminator, :chomp_last_line => true) + lines = reader.take_lines_until(:terminator => terminator, :chomp_last_line => true) block_reader = nil # terminator is false when reader has already been prepared elsif terminator == false @@ -842,7 +852,10 @@ class Lexer block_reader = reader else lines = nil - block_reader = Reader.new reader.grab_lines_until(:terminator => terminator) + file = reader.file + path = reader.path + lineno = reader.lineno + block_reader = Reader.new reader.take_lines_until(:terminator => terminator), file, path, lineno end if content_model == :skip @@ -1050,15 +1063,18 @@ class Lexer end # first skip the line with the marker / term - reader.get_line - list_item_reader = Reader.new grab_lines_for_list_item(reader, list_type, sibling_trait, has_text) + reader.read_line + file = reader.file + path = reader.path + lineno = reader.lineno + list_item_reader = Reader.new take_lines_for_list_item(reader, list_type, sibling_trait, has_text), file, path, lineno if list_item_reader.has_more_lines? comment_lines = list_item_reader.consume_line_comments subsequent_line = list_item_reader.peek_line list_item_reader.unshift(*comment_lines) unless comment_lines.empty? if !subsequent_line.nil? - continuation_connects_first_block = (subsequent_line == "\n") + continuation_connects_first_block = (subsequent_line == ::Asciidoctor::EOL) # if there's no continuation connecting the first block, then # treat the lines as paragraph text (activated when has_text = false) if !continuation_connects_first_block && list_type != :dlist @@ -1108,7 +1124,7 @@ class Lexer # has_text - Whether the list item has text defined inline (always true except for labeled lists) # # Returns an Array of lines belonging to the current list item. - def self.grab_lines_for_list_item(reader, list_type, sibling_trait = nil, has_text = true) + def self.take_lines_for_list_item(reader, list_type, sibling_trait = nil, has_text = true) buffer = [] # three states for continuation: :inactive, :active & :frozen @@ -1126,7 +1142,7 @@ class Lexer detached_continuation = nil while reader.has_more_lines? - this_line = reader.get_line + this_line = reader.read_line # if we've arrived at a sibling item in this list, we've captured # the complete list item and can begin processing it @@ -1161,7 +1177,7 @@ class Lexer buffer << this_line # grab all the lines in the block, leaving the delimiters in place # we're being more strict here about the terminator, but I think that's a good thing - buffer.concat reader.grab_lines_until(:terminator => match.terminator, :grab_last_line => true) + buffer.concat reader.take_lines_until(:terminator => match.terminator, :take_last_line => true) continuation = :inactive else break @@ -1178,7 +1194,7 @@ class Lexer # list item will throw off the exit from it if this_line.match(REGEXP[:lit_par]) reader.unshift_line this_line - buffer.concat reader.grab_lines_until( + buffer.concat reader.take_lines_until( :preserve_last_line => true, :break_on_blank_lines => true, :break_on_list_continuation => true) {|line| @@ -1205,7 +1221,7 @@ class Lexer # advance to the next line of content if this_line.chomp.empty? reader.skip_blank_lines - this_line = reader.get_line + this_line = reader.read_line # if we hit eof or a sibling, stop reading break if this_line.nil? || is_sibling_list_item?(this_line, list_type, sibling_trait) end @@ -1221,7 +1237,7 @@ class Lexer # slurp up any literal paragraph offset by blank lines if this_line.match(REGEXP[:lit_par]) reader.unshift_line this_line - buffer.concat reader.grab_lines_until( + buffer.concat reader.take_lines_until( :preserve_last_line => true, :break_on_blank_lines => true, :break_on_list_continuation => true) {|line| @@ -1279,8 +1295,8 @@ class Lexer buffer.pop end - #warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.join}<BUFFER" - #warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.inspect}<BUFFER" + #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.join}<BUFFER" + #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.inspect}<BUFFER" buffer end @@ -1458,7 +1474,7 @@ class Lexer #-- # NOTE for efficiency, we don't reuse methods that check for a section title def self.parse_section_title(reader, document) - line1 = reader.get_line + line1 = reader.read_line sect_id = nil sect_title = nil sect_level = -1 @@ -1483,7 +1499,7 @@ class Lexer end sect_level = section_level line2 single_line = false - reader.get_line + reader.read_line end end if sect_level >= 0 @@ -1523,7 +1539,7 @@ class Lexer implicit_authors = nil if reader.has_more_lines? && !reader.next_line_empty? - author_metadata = process_authors reader.get_line + author_metadata = process_authors reader.read_line unless author_metadata.empty? # apply header subs and assign to document @@ -1547,7 +1563,7 @@ class Lexer rev_metadata = {} if reader.has_more_lines? && !reader.next_line_empty? - rev_line = reader.get_line + rev_line = reader.read_line if match = rev_line.match(REGEXP[:revision_info]) rev_metadata['revdate'] = match[2].strip rev_metadata['revnumber'] = match[1].rstrip unless match[1].nil? @@ -1737,7 +1753,7 @@ class Lexer next_line = reader.peek_line if (commentish = next_line.start_with?('//')) && (match = next_line.match(REGEXP[:comment_blk])) terminator = match[0] - reader.grab_lines_until(:skip_first_line => true, :preserve_last_line => true, :terminator => terminator, :preprocess => false) + reader.take_lines_until(:skip_first_line => true, :preserve_last_line => true, :terminator => terminator, :preprocess => false) elsif commentish && next_line.match(REGEXP[:comment]) # do nothing, we'll skip it elsif !options[:text] && (match = next_line.match(REGEXP[:attr_entry])) @@ -1918,7 +1934,7 @@ class Lexer end if validate && expected != actual - # FIXME I need a reader reference or line number to report line number + # FIXME I need a reader reference or line number to report line number!! warn "asciidoctor: WARNING: list item index: expected #{expected}, got #{actual}" end @@ -1979,7 +1995,7 @@ class Lexer loop_idx = -1 while table_reader.has_more_lines? loop_idx += 1 - line = table_reader.get_line + line = table_reader.read_line if skipped == 0 && loop_idx.zero? && !attributes.has_key?('options') && !(next_line = table_reader.peek_line(false)).nil? && next_line.chomp.empty? @@ -2211,7 +2227,7 @@ class Lexer save_current = lambda { if collector.empty? if type != :style - warn "asciidoctor: WARNING:#{reader.nil? ? nil : " line #{reader.lineno}:"} invalid empty #{type} detected in style attribute" + warn "asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} invalid empty #{type} detected in style attribute" end else case type @@ -2220,7 +2236,7 @@ class Lexer parsed[type].push collector.join when :id if parsed.has_key? :id - warn "asciidoctor: WARNING:#{reader.nil? ? nil : " line #{reader.lineno}:"} multiple ids detected in style attribute" + warn "asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} multiple ids detected in style attribute" end parsed[type] = collector.join else diff --git a/lib/asciidoctor/reader.rb b/lib/asciidoctor/reader.rb index 572ac763..a51fbdb7 100644 --- a/lib/asciidoctor/reader.rb +++ b/lib/asciidoctor/reader.rb @@ -1,153 +1,250 @@ module Asciidoctor # Public: Methods for retrieving lines from AsciiDoc source files class Reader - - # Public: Get the document source as a String Array of lines. - attr_reader :source + attr_reader :lines + attr_reader :file + attr_reader :dir + attr_reader :path # Public: Get the 1-based offset of the current line. attr_reader :lineno - # Public: Initialize the Reader object. - # - # data - The Array of Strings holding the Asciidoc source document. The - # original instance of this Array is not modified (default: nil) - # document - The document with which this reader is associated. Used to access - # document attributes (default: nil) - # preprocess - A flag indicating whether to run the preprocessor on these lines. - # Only enable for the outer-most Reader. If this argument is true, - # a Document object must also be supplied. - # (default: false) - # block - A block that can be used to retrieve external Asciidoc - # data to include in this document. - # - # Examples - # - # data = File.readlines(filename) - # reader = Asciidoctor::Reader.new data - def initialize(data = nil, document = nil, preprocess = false, &block) - # TODO use Struct to track file/lineno info; track as file changes; offset for sub-readers - data = [] if data.nil? - @lineno = 0 - @next_line_preprocessed = false - @unescape_next_line = false - @conditionals_stack = [] - @skipping = false - @eof = false + # Public: Get the document source as a String Array of lines. + attr_reader :source_lines - if !preprocess - @lines = data.is_a?(String) ? data.lines.entries : data.dup - @preprocess_source = false - # document is not nil in the case of nested AsciiDoc document (in table cell) - unless document.nil? - @document = document - # preprocess first line, since we may not have hit it yet - # FIXME include handler (block) should be available to the peek_line call - @include_block = nil - peek_line true - @document = nil - end - elsif !data.empty? - # NOTE we assume document is not nil! - @document = document - @preprocess_source = true - @include_block = block_given? ? block : nil - normalize_data(data.is_a?(String) ? data.lines : data) + # Public: Initialize the Reader object + def initialize data = nil, file = nil, path = nil, lineno = 1 + if file.nil? + @file = @dir = nil + @path = path + #elsif file.is_a? Reader + # @file = file.file + # @dir = File.dirname @file + # @dir = nil if @dir == '.' # right? + # @path = file.path + # lineno = file.lineno else - @lines = [] - @preprocess_source = false + @file = file + @dir = File.dirname @file + @dir = nil if @dir == '.' # right? + @path = path || File.basename(@file) end + @lineno = lineno # IMPORTANT lineno assignment must proceed prepare_lines call! + @lines = data.nil? ? [] : (prepare_lines data) + @source_lines = @lines.dup + @eof = @lines.empty? + @look_ahead = 0 + @process_lines = true + @unescape_next_line = false + end - @source = @lines.dup + # Protected: Prepare the lines from the provided data + # + # This method strips whitespace from the end of every line of + # the source data and appends a LF (i.e., Unix endline). This + # whitespace substitution is very important to how Asciidoctor + # works. + # + # Any leading or trailing blank lines are also removed. + # + # The normalized lines are assigned to the @lines instance variable. + # + # data - A String Array of input data to be normalized + # opts - A Hash of options to control what cleansing is done + # + # Returns The String lines extracted from the data + def prepare_lines data, opts = {} + data.is_a?(String) ? data.each_line.to_a : data.dup end - # Public: Get a copy of the remaining Array of String lines parsed from the source - def lines - @lines.nil? ? nil : @lines.dup + # Protected: Processes a previously unvisited line + # + # By default, this method marks the line as processed + # by incrementing the look_ahead counter and returns + # the line unmodified. + # + # Returns The String line the Reader should make available to the next + # invocation of Reader#read_line or nil if the Reader should drop the line, + # advance to the next line and process it. + def process_line line + @look_ahead += 1 if @process_lines + line end # Public: Check whether there are any lines left to read. # - # If preprocessing is enabled for this Reader, and there are lines remaining, - # the next line is preprocessed before checking whether there are more lines. + # 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 @lines is empty, or false otherwise. - def has_more_lines?(preprocess = nil) - if @eof || (@eof = @lines.empty?) - false - elsif (preprocess.nil? && @preprocess_source || preprocess) && !@next_line_preprocessed - preprocess_next_line.nil? ? false : !@lines.empty? + # Returns True if there are more lines, False if there are not. + def has_more_lines? + !(@eof || (@eof = peek_line.nil?)) + end + + # Public: 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 + def next_line_empty? + (line = peek_line).nil? || line.chomp.empty? + end + + # Public: Get the next line of source data. Does not consume the line returned. + # + # 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#preprocess_line method to be initialized. This call gives + # sub-classess 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. + # + # direct - A Boolean flag to bypasses the check for more lines and immediately + # returns the first element of the internal @lines Array. (default: false) + # + # Returns a String dup of the next line of the source data if data is present. + # Returns nil if there is no more data. + def peek_line direct = false + if @look_ahead > 0 + @unescape_next_line ? @lines.first[1..-1].dup : @lines.first.dup + elsif @eof || @lines.empty? + @eof = true + @look_ahead = 0 + nil else - true + # 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.first).nil? + peek_line + else + line.dup + end end end - # Public: Check whether this reader is empty (contains no lines) + # TODO document & test me! + def peek_lines num = 1 + result = [] + (1..num).each do + if has_more_lines? + result << read_line + else + break + end + end + + restore_lines result unless result.empty? + + result + end + + # Public: Get the next line of source data. Consumes the line returned. # - # If preprocessing is enabled for this Reader, and there are lines remaining, - # the next line is preprocessed before checking whether there are more lines. + # direct - A Boolean flag to bypasses the check for more lines and immediately + # returns the first element of the internal @lines Array. (default: false) # - # Returns true if @lines is empty, otherwise false. - def empty? - !has_more_lines? + # Returns the String of the next line of the source data if data is present. + # Returns nil if there is no more data. + def read_line direct = false + if direct || @look_ahead > 0 || has_more_lines? + shift + else + nil + end end + alias :get_line :read_line - # Private: Strip off leading blank lines in the Array of lines. + # Public: Get the remaining lines of source data. # - # Examples + # 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. # - # @lines - # => ["\n", "\t\n", "Foo\n", "Bar\n", "\n"] + # Returns the lines read as a String Array + def read_lines + lines = [] + while has_more_lines? + lines << read_line + end + lines + end + alias :readlines :read_lines + alias :get_lines :read_lines + + # Public: Get the remaining lines of source data joined as a String. # - # skip_blank_lines - # => 2 + # Delegates to Reader#read_lines, then joins the result. # - # @lines - # => ["Foo\n", "Bar\n"] + # Returns the lines read joined as a String + def read + read_lines.join + end + + # Public: Advance to the next line by discarding the line at the front of the stack # - # Returns an Integer of the number of lines skipped - def skip_blank_lines - skipped = 0 - # optimized code for shortest execution path - while !(next_line = get_line).nil? - if next_line.chomp.empty? - skipped += 1 - else - unshift_line next_line - break - end - end + # direct - A Boolean flag to bypasses the check for more lines and immediately + # returns the first element of the internal @lines Array. (default: true) + # + # returns a Boolean indicating whether there was a line to discard. + def advance direct = true + !(read_line direct).nil? + end - skipped + # Public: Push the String line onto the beginning of the Array of source data. + # + # Since this line was (assumed to be) previously retrieved through the + # reader, it is marked as seen. + # + # returns nil + def unshift_line line_to_restore + unshift line_to_restore + nil end + alias :restore_line :unshift_line - # Public: Consume consecutive lines containing line- or block-level comments. + # Public: Push an Array of lines onto the front of the Array of source data. # - # Returns the Array of lines that were consumed + # Since these lines were (assumed to be) previously retrieved through the + # reader, they are marked as seen. + # + # Returns nil + 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 + alias :restore_lines :unshift_lines + + # Public: Consume consecutive lines containing line comments. # # Examples # @lines - # => ["// foo\n", "////\n", "foo bar\n", "////\n", "actual text\n"] + # => ["// foo\n", "bar\n"] # # comment_lines = consume_comments - # => ["// foo\n", "////\n", "foo bar\n", "////\n"] + # => ["// foo\n"] # # @lines - # => ["actual text\n"] - def consume_comments(options = {}) + # => ["bar\n"] + # + # Returns the Array of lines that were consumed + def consume_comments opts = {} + return [] if eof? + comment_lines = [] - preprocess = options.fetch(:preprocess, true) - while !(next_line = get_line(preprocess)).nil? - if options[:include_blank_lines] && next_line.chomp.empty? - comment_lines << next_line + include_blank_lines = opts[:include_blank_lines] + while (next_line = peek_line) + if include_blank_lines && next_line.chomp.empty? + comment_lines << read_line elsif (commentish = next_line.start_with?('//')) && (match = next_line.match(REGEXP[:comment_blk])) - comment_lines << next_line - comment_lines.push(*(grab_lines_until(:terminator => match[0], :grab_last_line => true, :preprocess => false))) + comment_lines << read_line + comment_lines.push(*(take_lines_until(:terminator => match[0], :take_last_line => true, :raw => true))) elsif commentish && next_line.match(REGEXP[:comment]) - comment_lines << next_line + comment_lines << read_line else - # throw it back - unshift_line next_line break end end @@ -156,225 +253,323 @@ class Reader end alias :skip_comment_lines :consume_comments - # Ignore front-matter, commonly used in static site generators - def skip_front_matter(data, increment_linenos = true) - #if data.nil? - # data = @lines - # increment_linenos = true - #else - # increment_linenos = false - #end - - front_matter = nil - if data.size > 0 && data.first.chomp == '---' - original_data = data.dup - front_matter = [] - data.shift - @lineno += 1 if increment_linenos - while !data.empty? && data.first.chomp != '---' - front_matter.push data.shift - @lineno += 1 if increment_linenos - end + def consume_line_comments + return [] if eof? - if data.empty? - data.unshift(*original_data) - @lineno = 0 if increment_linenos - front_matter = nil + comment_lines = [] + # optimized code for shortest execution path + while (next_line = peek_line) + if next_line.match(REGEXP[:comment]) + comment_lines << read_line else - data.shift - @lineno += 1 if increment_linenos + break end end - front_matter + comment_lines end + alias :skip_lines_comments :consume_comments - # Public: Consume consecutive lines containing line comments. - # - # Returns the Array of lines that were consumed + # Private: Strip off leading blank lines in the Array of lines. # # Examples + # # @lines - # => ["// foo\n", "bar\n"] + # => ["\n", "\t\n", "Foo\n", "Bar\n", "\n"] # - # comment_lines = consume_comments - # => ["// foo\n"] + # skip_blank_lines + # => 2 # # @lines - # => ["bar\n"] - def consume_line_comments - comment_lines = [] + # => ["Foo\n", "Bar\n"] + # + # Returns an Integer of the number of lines skipped + def skip_blank_lines + return 0 if eof? + + num_skipped = 0 # optimized code for shortest execution path - while !(next_line = get_line).nil? - if next_line.match(REGEXP[:comment]) - comment_lines << next_line + while (next_line = peek_line) + if next_line.chomp.empty? + advance + num_skipped += 1 else - unshift_line next_line - break + return num_skipped end end - comment_lines + num_skipped end - # Public: Get the next line of source data. Consumes the line returned. - # - # preprocess - A Boolean flag indicating whether to evaluate preprocessing - # directives (macros) before reading line (default: true) + # Public: Advance to the end of the reader, consuming all remaining lines # - # Returns the String of the next line of the source data if data is present. - # Returns nil if there is no more data. - def get_line(preprocess = true) - if @eof || (@eof = @lines.empty?) - @next_line_preprocessed = true - nil - elsif preprocess && @preprocess_source && - !@next_line_preprocessed && preprocess_next_line.nil? - @next_line_preprocessed = true - nil - else - @lineno += 1 - @next_line_preprocessed = false - if @unescape_next_line - @unescape_next_line = false - @lines.shift[1..-1] - else - @lines.shift - end - end + # Returns nothing. + def terminate + @lineno += @lines.size + @lines.clear + @eof = true + @look_ahead = 0 + nil end - # Public: Get the remaining lines of source data by calling - # Reader#get_line until all lines are consumed. - # - # preprocess - A Boolean flag indicating whether to evaluate preprocessing - # directives (macros) before reading line (default: true) + # Public: Check whether this reader is empty (contains no lines) # - # Returns the lines read as a String Array - def get_lines(preprocess = true) - lines = nil - while has_more_lines? preprocess - (lines ||= []) << get_line(preprocess) - end - @eof = true - lines + # Returns true if there are no more lines to peek, otherwise false. + def eof? + !has_more_lines? end + alias :empty? :eof? - # Public: Advance to the next line by discarding the line at the front of the stack + # Public: 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. # - # Removes the line at the front of the stack without any processing. + # options - an optional Hash of processing options: + # * :break_on_blank_lines may be used to specify to break on + # blank lines + # * :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. + # * :take_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 # - # returns a boolean indicating whether there was a line to discard - def advance - @next_line_preprocessed = false - # we assume that we're advancing over a line of known content - if @eof || (@eof = @lines.empty?) - false + # Returns the Array of lines forming the next segment. + # + # Examples + # + # reader = Reader.new ["First paragraph\n", "Second paragraph\n", + # "Open block\n", "\n", "Can have blank lines\n", + # "--\n", "\n", "In a different segment\n"] + # + # reader.take_lines_until + # => ["First paragraph\n", "Second paragraph\n", "Open block\n"] + def take_lines_until options = {} + result = [] + advance if options[:skip_first_line] + if @process_lines && options[:raw] + @process_lines = false + reset_process_lines = true else - @lineno += 1 - @lines.shift - true + reset_process_lines = false end - end - # Public: Advance to the end of the reader, consuming all remaining lines - # - # Returns nothing. - def terminate - while !@eof - advance + has_block = block_given? + if (terminator = (options.fetch :terminator, nil)) + break_on_blank_lines = false + break_on_list_continuation = false + chomp_last_line = options.fetch :chomp_last_line, false + else + break_on_blank_lines = options[:break_on_blank_lines] + break_on_list_continuation = options[:break_on_list_continuation] + chomp_last_line = break_on_blank_lines + end + skip_line_comments = options[:skip_line_comments] + taken = false + + while (line = read_line) + finish = while true + break true if terminator && line.chomp == terminator + # QUESTION: can we get away with line.chomp.empty? here? + break true if break_on_blank_lines && line.chomp.empty? + if break_on_list_continuation && taken && line.chomp == LIST_CONTINUATION + options[:preserve_last_line] = true + break true + end + break true if has_block && (yield line) + break false + end + + if finish + if options[:take_last_line] + result << line + taken = true + end + restore_line line if options[:preserve_last_line] + break + end + + unless skip_line_comments && line.match(REGEXP[:comment]) + result << line + taken = true + end + end + + if chomp_last_line && taken + result[-1] = result.last.chomp end + + @process_lines = true if reset_process_lines + result + end + alias :grab_lines_until :take_lines_until + + # Protected: Shift the line off the stack and increment the lineno + def shift + @lineno += 1 + @look_ahead -= 1 unless @look_ahead == 0 + @lines.shift end - # Public: Return whether the next line is empty. Does not consume the line returned. + # Protected: Restore the line to the stack and decrement the lineno + def unshift line + @lineno -= 1 + @look_ahead += 1 + @eof = false + @lines.unshift line + end + + # Public: Get information about the last line read, including file name and line number. # - # preprocess - A Boolean flag indicating whether to evaluate preprocessing - # directives (macros) before reading line (default: true) + # Returns A String summary of the last line read + def line_info + %(#{@path}: line #{@lineno}) + end + alias :next_line_info :line_info + + def prev_line_info + %(#{@path}: line #{@lineno - 1}) + end + + # Public: Get a copy of the remaining Array of String lines managed by this Reader # - # Returns a Boolean indicating whether the next line of the source data is empty. - # Returns true if there is no more data. - def next_line_empty?(preprocess = true) - if !preprocess - @eof || (@eof = @lines.empty?) ? true : @lines.first.chomp.empty? - elsif has_more_lines? true - @lines.first.chomp.empty? - else - true - end + # Returns A copy of the String Array of lines remaining in this Reader + def lines + @lines.dup end - # Public: Get the next line of source data. Does not consume the line returned. + def source + @source_lines.join + end + + # Public: Get a summary of this Reader. # - # preprocess - A Boolean flag indicating whether to evaluate preprocessing - # directives (macros) before reading line (default: true) # - # Returns a String dup of the next line of the source data if data is present. - # Returns nil if there is no more data. - def peek_line(preprocess = true) - if !preprocess - # QUESTION do we need to dup? - @eof || (@eof = @lines.empty?) ? nil : @lines.first.dup - elsif has_more_lines? true - # QUESTION do we need to dup? - @lines.first.dup + # Returns A string summary of this reader, which contains the path and line information + def to_s + line_info + end +end + +# Public: Methods for retrieving lines from AsciiDoc source files, evaluating preprocessor +# directives as each line is read off the Array of lines. +class PreprocessorReader < Reader + attr_reader :include_stack + attr_reader :includes + + # Public: Initialize the PreprocessorReader object + def initialize document, data = nil, file = nil, path = nil + @document = document + super data, file, path + include_depth_default = document.attributes.fetch('include-depth', 10).to_i + include_depth_default = 0 if include_depth_default < 0 + # track both absolute depth for comparing to size of include stack and relative depth for reporting + @maxdepth = {:abs => include_depth_default, :rel => include_depth_default} + @include_stack = [] + @includes = (document.references[:includes] ||= []) + @skipping = false + @conditional_stack = [] + end + + def prepare_lines data, opts = {} + if data.is_a?(String) + if ::Asciidoctor::FORCE_ENCODING + result = data.each_line.map {|line| "#{line.rstrip.force_encoding ::Encoding::UTF_8}#{::Asciidoctor::EOL}" } + else + result = data.each_line.map {|line| "#{line.rstrip}#{::Asciidoctor::EOL}" } + end else - nil + if ::Asciidoctor::FORCE_ENCODING + result = data.map {|line| "#{line.rstrip.force_encoding ::Encoding::UTF_8}#{::Asciidoctor::EOL}" } + else + result = data.map {|line| "#{line.rstrip}#{::Asciidoctor::EOL}" } + end end - end - # TODO document & test me! - def peek_lines(number = 1) - lines = [] - idx = 0 - (1..number).each do - if @preprocess_source && !@next_line_preprocessed - advanced = preprocess_next_line - break if advanced.nil? || @eof || (@eof = @lines.empty?) - idx = 0 if advanced + # QUESTION should this work for AsciiDoc table cell content? Currently it does not. + unless @document.nil? || !(@document.attributes.has_key? 'skip-front-matter') + if (front_matter = skip_front_matter! result) + @document.attributes['front-matter'] = front_matter.join.chomp end - break if idx >= @lines.size - # QUESTION do we need to dup? - lines << @lines[idx].dup - idx += 1 end - lines + + # QUESTION should we chomp last line? (with or without the condense flag?) + if opts.fetch(:condense, true) + result.shift && @lineno += 1 while !(first = result.first).nil? && first == ::Asciidoctor::EOL + result.pop while !(last = result.last).nil? && last == ::Asciidoctor::EOL + end + + if (indent = opts.fetch(:indent, nil)) + Lexer.reset_block_indent! result, indent.to_i + end + + result end - # Internal: Preprocess the next line until the cursor is at a line of content - # - # Evaluate preprocessor macros on the next line, continuing to do so until - # the cursor arrives at a line of included content. That line is marked as - # preprocessed so that preprocessing is not performed multiple times. - # - # returns a Boolean indicating whether the cursor advanced, or nil if there - # are no more lines available. - def preprocess_next_line - # this return could be happening from a recursive call - return nil if @eof || (next_line = @lines.first).nil? - if next_line.include?('::') && (next_line.include?('if') || next_line.include?('endif')) && (match = next_line.match(REGEXP[:ifdef_macro])) - if next_line.start_with? '\\' - @next_line_preprocessed = true + def process_line line + return line unless @process_lines + + if line.chomp.empty? + @look_ahead += 1 + return '' + end + + found_colon = line.include?('::') + if found_colon && line.include?('if') && (match = line.match(REGEXP[:ifdef_macro])) + # if escaped, mark as processed and return line unescaped + if line.start_with? '\\' @unescape_next_line = true - false + @look_ahead += 1 + line[1..-1] else - preprocess_conditional_inclusion(*match.captures) + if preprocess_conditional_inclusion(*match.captures) + # move the pointer past the conditional line + advance + # treat next line as uncharted territory + nil + else + # the line was not a valid conditional line + # mark it as visited and return it + @look_ahead += 1 + line + end end elsif @skipping advance - # skip over comment blocks, we don't want to process directives in them - skip_comment_lines :include_blank_lines => true, :preprocess => false - preprocess_next_line.nil? ? nil : true - elsif next_line.include?('include::') && match = next_line.match(REGEXP[:include_macro]) - if next_line.start_with? '\\' - @next_line_preprocessed = true + nil + elsif found_colon && line.include?('include::') && (match = line.match(REGEXP[:include_macro])) + # if escaped, mark as processed and return line unescaped + if line.start_with? '\\' @unescape_next_line = true - false + @look_ahead += 1 + line[1..-1] else - preprocess_include(match[1], match[2].strip) + # QUESTION should we strip whitespace from raw attributes in Substituters#parse_attributes? (check perf) + if preprocess_include match[1], match[2].strip + # peek again since the content has changed + nil + else + # the line was not a valid include line and is unchanged + # mark it as visited and return it + @look_ahead += 1 + line + end end else - @next_line_preprocessed = true - false + super + end + end + + def peek_line direct = false + if (line = super) + line + elsif !@include_stack.empty? + pop_include + peek_line direct + else + nil end end @@ -397,37 +592,36 @@ class Reader # Used for a single-line conditional block in the case of the ifdef or # ifndef directives, and for the conditional expression for the ifeval directive. # - # returns a Boolean indicating whether the cursor advanced, or nil if there - # are no more lines available. - def preprocess_conditional_inclusion(directive, target, delimiter, text) + # returns a Boolean indicating whether the cursor should be advanced + def preprocess_conditional_inclusion directive, target, delimiter, text # must have a target before brackets if ifdef or ifndef # must not have text between brackets if endif # don't honor match if it doesn't meet this criteria + # QUESTION should we warn for these bogus declarations? if ((directive == 'ifdef' || directive == 'ifndef') && target.empty?) || (directive == 'endif' && !text.nil?) - @next_line_preprocessed = true return false end if directive == 'endif' - stack_size = @conditionals_stack.size + stack_size = @conditional_stack.size if stack_size > 0 - pair = @conditionals_stack.last + pair = @conditional_stack.last if target.empty? || target == pair[:target] - @conditionals_stack.pop - @skipping = @conditionals_stack.empty? ? false : @conditionals_stack.last[:skipping] + @conditional_stack.pop + @skipping = @conditional_stack.empty? ? false : @conditional_stack.last[:skipping] else - warn "asciidoctor: ERROR: line #{@lineno + 1}: mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[]" + warn "asciidoctor: ERROR: #{line_info}: mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[]" end else - warn "asciidoctor: ERROR: line #{@lineno + 1}: unmatched macro: endif::#{target}[]" + warn "asciidoctor: ERROR: #{line_info}: unmatched macro: endif::#{target}[]" end - advance - return preprocess_next_line.nil? ? nil : true + return true end skip = false - if !@skipping + unless @skipping + # QUESTION any way to wrap ifdef & ifndef logic up together? case directive when 'ifdef' case delimiter @@ -457,32 +651,36 @@ class Reader # the text in brackets must match an expression # don't honor match if it doesn't meet this criteria if !target.empty? || !(expr_match = text.strip.match(REGEXP[:eval_expr])) - @next_line_preprocessed = true return false end - lhs = resolve_expr_val(expr_match[1]) + lhs = resolve_expr_val expr_match[1] + # regex enforces a restrict set of math-related operations op = expr_match[2] - rhs = resolve_expr_val(expr_match[3]) + rhs = resolve_expr_val expr_match[3] - skip = !lhs.send(op.to_sym, rhs) + skip = !(lhs.send op.to_sym, rhs) end end - advance - # single line conditional inclusion - if directive != 'ifeval' && !text.nil? - if !@skipping && !skip - unshift_line "#{text.rstrip}\n" - return true - end + # conditional inclusion block + if directive == 'ifeval' || text.nil? + @skipping = true if skip + @conditional_stack << {:target => target, :skip => skip, :skipping => @skipping} + # single line conditional inclusion else - if !@skipping && skip - @skipping = true + unless @skipping || skip + # FIXME this should be simpler! + # slight hack to skip past conditional line + # but keep our synthetic line marked as processed + conditional_line = read_line true + unshift "#{text.rstrip}#{::Asciidoctor::EOL}" + unshift conditional_line + return true end - @conditionals_stack << {:target => target, :skip => skip, :skipping => @skipping} end - return preprocess_next_line.nil? ? nil : true + + true end # Internal: Preprocess the directive (macro) to include the target document. @@ -493,13 +691,12 @@ class Reader # If SafeMode is SECURE or greater, the directive is ignore and the include # directive line is emitted verbatim. # - # Otherwise, if an include handler is specified (currently controlled by a - # closure block), pass the target to that block and expect an Array of String - # lines in return. + # Otherwise, if an include processor is specified pass the target and + # attributes to that processor and expect an Array of String lines in return. # - # Otherwise, if the include-depth attribute is greater than 0, normalize the - # target path and read the lines onto the beginning of the Array of source - # data. + # Otherwise, if the max depth is greater than 0, and is not exceeded by the + # stack size, normalize the target path and read the lines onto the beginning + # of the Array of source data. # # If none of the above apply, emit the include directive line verbatim. # @@ -508,50 +705,55 @@ class Reader # # returns a Boolean indicating whether the line under the cursor has changed. # - - # FIXME includes bork line numbers - def preprocess_include(target, raw_attributes) - target = @document.sub_attributes target + # TODO extensions need to be able to communicate line numbers + def preprocess_include target, raw_attributes + target = @document.sub_attributes target, :attribute_missing => 'drop-line' if target.empty? - advance - @next_line_preprocessed = false - false + if @document.attributes.fetch('attribute-missing', COMPLIANCE[:attribute_missing]) == 'skip' + false + else + advance + true + end # if running in SafeMode::SECURE or greater, don't process this directive # however, be friendly and at least make it a link to the source document elsif @document.safe >= SafeMode::SECURE - @lines[0] = "link:#{target}[#{target}]\n" - @next_line_preprocessed = true - false - # assume that if a block is given, the developer wants - # to handle when and how to process the include, even - # if the include-depth attribute is 0 - elsif @include_block - advance - @lines.unshift(*normalize_include_data(@include_block.call(target, @document))) - # FIXME currently we're not checking the upper bound of the include depth - elsif @document.attributes.fetch('include-depth', 0).to_i > 0 advance + unshift "link:#{target}[]#{::Asciidoctor::EOL}" + # TODO make creating the output target a helper method + #output_target = %(#{File.join(File.dirname(target), File.basename(target, File.extname(target)))}#{@document.attributes['outfilesuffix']}) + #unshift "link:#{output_target}[]#{::Asciidoctor::EOL}" + true + elsif @maxdepth[:abs] > 0 && @include_stack.size >= @maxdepth[:abs] + warn %(asciidoctor: WARNING: #{line_info}: maximum include depth of #{@maxdepth[:rel]} exceeded) + false + # FIXME Insert IncludeProcessor logic here once merged + elsif @maxdepth[:abs] > 0 if target.include?(':') && target.match(REGEXP[:uri_sniff]) unless @document.attributes.has_key? 'allow-uri-read' - @lines[0] = "link:#{target}[#{target}]\n" - @next_line_preprocessed = true - return false + advance + unshift "link:#{target}[]#{::Asciidoctor::EOL}" + return true end target_type = :uri - include_file = target + include_file = path = target if @document.attributes.has_key? 'cache-uri' - # NOTE caching requires the open-uri-cached gem to be installed + # caching requires the open-uri-cached gem to be installed + # processing will be automatically aborted if these libraries can't be opened Helpers.require_library 'open-uri/cached', 'open-uri-cached' else Helpers.require_library 'open-uri' end - # FIXME should stop processing include if required library cannot be loaded; check for -1 return value? else target_type = :file @document.references[:includes] << Helpers.rootname(target) - include_file = @document.normalize_system_path(target, nil, nil, :target_name => 'include file') + # include file is resolved relative to current include context + include_file = @document.normalize_system_path(target, @dir, nil, :target_name => 'include file') + path = include_file[(@document.base_dir.length + 1)..-1] if !File.file?(include_file) - warn "asciidoctor: WARNING: line #{@lineno}: include file not found: #{include_file}" + warn "asciidoctor: WARNING: #{line_info}: include file not found: #{include_file}" + advance return true end end @@ -560,6 +762,7 @@ class Reader tags = nil attributes = {} if !raw_attributes.empty? + # QUESTION should we use @document.parse_attribues? attributes = AttributeList.new(raw_attributes).parse if attributes.has_key? 'lines' inc_lines = [] @@ -584,40 +787,53 @@ class Reader if !inc_lines.nil? if !inc_lines.empty? selected = [] + inc_line_offset = 0 + inc_lineno = 0 begin open(include_file) do |f| f.each_line do |l| + inc_lineno += 1 take = inc_lines.first if take.is_a?(Float) && take.infinite? selected.push l + inc_line_offset = inc_lineno if inc_line_offset == 0 else if f.lineno == take selected.push l - inc_lines.shift + inc_line_offset = inc_lineno if inc_line_offset == 0 + inc_lines.shift end break if inc_lines.empty? end end end rescue - warn "asciidoctor: WARNING: line #{@lineno}: include #{target_type} not readable: #{include_file}" + warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}" + advance return true end - @lines.unshift(*normalize_include_data(selected, attributes['indent'])) unless selected.empty? + advance + # FIXME not accounting for skipped lines in reader line numbering + push_include selected, include_file, path, inc_line_offset, attributes end elsif !tags.nil? if !tags.empty? selected = [] + inc_line_offset = 0 + inc_lineno = 0 active_tag = nil begin open(include_file) do |f| f.each_line do |l| + inc_lineno += 1 + # must force encoding here since we're performing String operations on line l.force_encoding(::Encoding::UTF_8) if ::Asciidoctor::FORCE_ENCODING if !active_tag.nil? if l.include?("end::#{active_tag}[]") active_tag = nil else - selected.push "#{l.rstrip}\n" + selected.push l + inc_line_offset = inc_lineno if inc_line_offset == 0 end else tags.each do |tag| @@ -630,184 +846,167 @@ class Reader end end rescue - warn "asciidoctor: WARNING: line #{@lineno}: include #{target_type} not readable: #{include_file}" + warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}" + advance return true end - #@lines.unshift(*selected) unless selected.empty? - @lines.unshift(*normalize_include_data(selected, attributes['indent'])) unless selected.empty? + advance + # FIXME not accounting for skipped lines in reader line numbering + push_include selected, include_file, path, inc_line_offset, attributes end else begin - @lines.unshift(*normalize_include_data(open(include_file) {|f| f.readlines }, attributes['indent'])) + advance + push_include open(include_file) {|f| f.read }, include_file, path, 1, attributes rescue - warn "asciidoctor: WARNING: line #{@lineno}: include #{target_type} not readable: #{include_file}" + warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}" + advance return true end end true else - @next_line_preprocessed = true false end end - # Public: Push the String line onto the beginning of the Array of source data. - # - # Since this line was (assumed to be) previously retrieved through the - # reader, it is marked as preprocessed. - # - # returns nil - def unshift_line(line) - @lines.unshift line - @next_line_preprocessed = true - @eof = false - @lineno -= 1 - nil - end - - # Public: Push Array of lines onto the front of the Array of source data, unless `lines` has no non-nil values. - # - # Returns nil - def unshift(*new_lines) - size = new_lines.size - if size > 0 - @lines.unshift(*new_lines) - # assume that what we are putting back on is already processed for directives - @next_line_preprocessed = true + def push_include data, file = nil, path = nil, lineno = 1, attributes = {} + # FIXME change this to File.basename(File.rootname(file)) or File.baserootname(file) + @includes << File.join(File.dirname(file), File.basename(file, File.extname(file))) + @include_stack << [@lines, @file, @path, @lineno, @maxdepth] + @file = file + @dir = File.dirname file + @path = path + @lineno = lineno + if attributes.has_key? 'depth' + depth = attributes['depth'].to_i + depth = 1 if depth <= 0 + @maxdepth = {:abs => (@include_stack.size - 1) + depth, :rel => depth} + end + # effectively fill the buffer + @lines = prepare_lines data, :condense => false, :indent => attributes['indent'] + # FIXME kind of a hack + #Document::AttributeEntry.new('infile', @file).save_to_next_block @document + #Document::AttributeEntry.new('indir', File.dirname(@file)).save_to_next_block @document + if @lines.empty? + pop_include + else @eof = false - @lineno -= size + @look_ahead = 0 end - nil end - # Public: Chomp the String on the last line if this reader contains at least one line - # - # Delegates to chomp! - # - # Returns nil - def chomp_last! - @lines.last.chomp! unless @eof || (@eof = @lines.empty?) - nil + def pop_include + if @include_stack.size > 0 + @lines, @file, @path, @lineno, @maxdepth = @include_stack.pop + @dir = @file.nil? ? nil : File.dirname(@file) + # FIXME kind of a hack + #Document::AttributeEntry.new('infile', @file).save_to_next_block @document + #Document::AttributeEntry.new('indir', File.dirname(@file)).save_to_next_block @document + @eof = @lines.empty? + @look_ahead = 0 + end end - # Public: 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. - # - # options - an optional Hash of processing options: - # * :break_on_blank_lines may be used to specify to break on - # blank lines - # * :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. - # * :grab_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 - # - # Returns the Array of lines forming the next segment. - # - # Examples - # - # reader = Reader.new ["First paragraph\n", "Second paragraph\n", - # "Open block\n", "\n", "Can have blank lines\n", - # "--\n", "\n", "In a different segment\n"] - # - # reader.grab_lines_until - # => ["First paragraph\n", "Second paragraph\n", "Open block\n"] - def grab_lines_until(options = {}, &block) - buffer = [] + def include_depth + @include_stack.size + end - advance if options[:skip_first_line] - # very hot code - # save options to locals for minor optimizations - if options.has_key? :terminator - terminator = options[:terminator] - break_on_blank_lines = false - break_on_list_continuation = false - chomp_last_line = options[:chomp_last_line] || false + # TODO Document this override + # also, we now have the field in the super class, so perhaps + # just implement the logic there? + def shift + if @unescape_next_line + @unescape_next_line = false + super[1..-1] else - terminator = nil - break_on_blank_lines = options[:break_on_blank_lines] - break_on_list_continuation = options[:break_on_list_continuation] - chomp_last_line = break_on_blank_lines + super end - skip_line_comments = options[:skip_line_comments] - preprocess = options.fetch(:preprocess, true) - buffer_empty = true - while !(this_line = get_line(preprocess)).nil? - # effectively a no-args lamba, but much faster - finish = while true - break true if terminator && this_line.chomp == terminator - break true if break_on_blank_lines && this_line.strip.empty? - if break_on_list_continuation && !buffer_empty && this_line.chomp == LIST_CONTINUATION - options[:preserve_last_line] = true - break true - end - break true if block && yield(this_line) - break false - end + end - if finish - if options[:grab_last_line] - buffer << this_line - buffer_empty = false - end - # QUESTION should we dup this_line when restoring?? - unshift_line this_line if options[:preserve_last_line] - break + # Private: Ignore front-matter, commonly used in static site generators + def skip_front_matter! data, increment_linenos = true + #if data.nil? + # data = @lines + # increment_linenos = true + #else + # increment_linenos = false + #end + + front_matter = nil + if data.size > 0 && data.first.chomp == '---' + original_data = data.dup + front_matter = [] + data.shift + @lineno += 1 if increment_linenos + while !data.empty? && data.first.chomp != '---' + front_matter.push data.shift + @lineno += 1 if increment_linenos end - unless skip_line_comments && this_line.match(REGEXP[:comment]) - buffer << this_line - buffer_empty = false + if data.empty? + data.unshift(*original_data) + @lineno = 0 if increment_linenos + front_matter = nil + else + data.shift + @lineno += 1 if increment_linenos end end - # should we dup the line before chopping? - buffer.last.chomp! if chomp_last_line && !buffer_empty - buffer + front_matter end - # Public: Convert a string to a legal attribute name. + # Private: Resolve the value of one side of the expression # - # name - The String holding the Asciidoc attribute name. + # Examples # - # Returns a String with the legal name. + # expr = '"value"' + # resolve_expr_val(expr) + # # => "value" # - # Examples + # expr = '"value' + # resolve_expr_val(expr) + # # => "\"value" # - # sanitize_attribute_name('Foo Bar') - # => 'foobar' + # expr = '"{undefined}"' + # resolve_expr_val(expr) + # # => "" # - # sanitize_attribute_name('foo') - # => 'foo' + # expr = '{undefined}' + # resolve_expr_val(expr) + # # => nil # - # sanitize_attribute_name('Foo 3 #-Billy') - # => 'foo3-billy' - def sanitize_attribute_name(name) - Lexer.sanitize_attribute_name(name) - end - - # Private: Resolve the value of one side of the expression + # expr = '2' + # resolve_expr_val(expr) + # # => 2 + # + # @document.attributes['name'] = 'value' + # expr = '"{name}"' + # resolve_expr_val(expr) + # # => "value" + # + # Returns The value of the expression, coerced to the appropriate type def resolve_expr_val(str) val = str type = nil if val.start_with?('"') && val.end_with?('"') || val.start_with?('\'') && val.end_with?('\'') - type = :s - val = val[1..-2] + type = :string + val = val[1...-1] end + # QUESTION should we substitute first? if val.include? '{' val = @document.sub_attributes val end - if type != :s + unless type == :string if val.empty? val = nil + elsif val.strip.empty? + val = ' ' elsif val == 'true' val = true elsif val == 'false' @@ -815,6 +1014,8 @@ class Reader elsif val.include?('.') val = val.to_f else + # fallback to coercing to integer, since we + # require string values to be explicitly quoted val = val.to_i end end @@ -822,75 +1023,9 @@ class Reader val end - # Private: Normalize raw input read from an include directive - # - # This method strips whitespace from the end of every line of - # the source data and appends a LF (i.e., Unix endline). This - # whitespace substitution is very important to how Asciidoctor - # works. - # - # Any leading or trailing blank lines are also removed. (DISABLED) - # - # data - A String Array of input data to be normalized - # - # returns the processed lines - #- - # FIXME this shares too much in common w/ normalize_data; combine - # in a shared function - def normalize_include_data(data, indent = nil) - if ::Asciidoctor::FORCE_ENCODING - result = data.map {|line| "#{line.rstrip.force_encoding(::Encoding::UTF_8)}\n" } - else - result = data.map {|line| "#{line.rstrip}\n" } - end - - # QUESTION: how should this be activated? (defering for now) - # QUESTION: should we save the front matter read from an include file somewhere? - #if !@document.nil? && @document.attributes.has_key?('skip-front-matter') - # skip_front_matter result - #end - - unless indent.nil? - Lexer.reset_block_indent! result, indent.to_i - end - - result - end - - # Private: Normalize raw input, used for the outermost Reader. - # - # This method strips whitespace from the end of every line of - # the source data and appends a LF (i.e., Unix endline). This - # whitespace substitution is very important to how Asciidoctor - # works. - # - # Any leading or trailing blank lines are also removed. - # - # The normalized lines are assigned to the @lines instance variable. - # - # data - A String Array of input data to be normalized - # - # returns nothing - def normalize_data(data) - # normalize line ending to LF (purging occurrences of CRLF) - # this rstrip is *very* important to how Asciidoctor works - - if ::Asciidoctor::FORCE_ENCODING - @lines = data.map {|line| "#{line.rstrip.force_encoding(::Encoding::UTF_8)}\n" } - else - @lines = data.map {|line| "#{line.rstrip}\n" } - end - - # QUESTION should this work for AsciiDoc table cell content? If so, it won't hit here - if !@document.nil? && @document.attributes.has_key?('skip-front-matter') - if (front_matter = skip_front_matter(@lines)) - @document.attributes['front-matter'] = front_matter.join.chomp - end - end - - @lines.shift && @lineno += 1 while !@lines.first.nil? && @lines.first.chomp.empty? - @lines.pop while !@lines.last.nil? && @lines.last.chomp.empty? - nil + def to_s + %(#{self.class.name} [path: #{@path}, line #: #{@lineno}, include depth: #{@include_stack.size}, include stack: [#{@include_stack.map {|include| include.to_s}.join ', '}]]) end end + end diff --git a/lib/asciidoctor/table.rb b/lib/asciidoctor/table.rb index d194fe50..bb14c98a 100644 --- a/lib/asciidoctor/table.rb +++ b/lib/asciidoctor/table.rb @@ -210,7 +210,19 @@ class Table::Cell < AbstractNode # FIXME hide doctitle from nested document; temporary workaround to fix # nested document seeing doctitle and assuming it has its own document title parent_doctitle = @document.attributes.delete('doctitle') - @inner_document = Document.new(@text, :header_footer => false, :parent => @document) + # NOTE we need to process the first line of content as it may not have been processed + # the included content cannot expect to match conditional terminators in the remaining + # lines of table cell content, it must be self-contained logic + inner_document_lines = @text.each_line.to_a + unless inner_document_lines.empty? || !inner_document_lines.first.include?('::') + unprocessed_lines = inner_document_lines[0..0] + processed_lines = PreprocessorReader.new(@document, unprocessed_lines).readlines + if processed_lines != unprocessed_lines + inner_document_lines.shift + inner_document_lines.unshift *processed_lines + end + end + @inner_document = Document.new(inner_document_lines, :header_footer => false, :parent => @document) @document.attributes['doctitle'] = parent_doctitle unless parent_doctitle.nil? end end @@ -410,6 +422,7 @@ class Table::ParserContext if format == 'psv' cell_spec = take_cell_spec if cell_spec.nil? + # FIXME need a reader reference to report line info warn 'asciidoctor: ERROR: table missing leading separator, recovering automatically' cell_spec = {} repeat = 1 diff --git a/test/document_test.rb b/test/document_test.rb index 148565a5..90a2c981 100644 --- a/test/document_test.rb +++ b/test/document_test.rb @@ -1390,4 +1390,28 @@ asciidoctor - converts AsciiDoc source files to HTML, DocBook and other formats assert_xpath '//*[@id="content"]/*[@class="sect1"]/h2[text()="SYNOPSIS"]', output, 1 end end + + context 'Secure Asset Path' do + test 'allows us to specify a path relative to the current dir' do + doc = Asciidoctor::Document.new + legit_path = Dir.pwd + '/foo' + assert_equal legit_path, doc.normalize_asset_path(legit_path) + end + + test 'keeps naughty absolute paths from getting outside' do + naughty_path = "#{disk_root}etc/passwd" + doc = Asciidoctor::Document.new + secure_path = doc.normalize_asset_path(naughty_path) + assert naughty_path != secure_path + assert_match(/^#{doc.base_dir}/, secure_path) + end + + test 'keeps naughty relative paths from getting outside' do + naughty_path = 'safe/ok/../../../../../etc/passwd' + doc = Asciidoctor::Document.new + secure_path = doc.normalize_asset_path(naughty_path) + assert naughty_path != secure_path + assert_match(/^#{doc.base_dir}/, secure_path) + end + end end diff --git a/test/fixtures/basic-docinfo.xml b/test/fixtures/basic-docinfo.xml index df66e54a..6181e12f 100644 --- a/test/fixtures/basic-docinfo.xml +++ b/test/fixtures/basic-docinfo.xml @@ -1,4 +1,4 @@ -<copyright> -<year>2013</year> -<holder>Acme, Inc.</holder> +<copyright><!-- don't remove the indent! --> + <year>2013</year> + <holder>Acme, Inc.</holder> </copyright> diff --git a/test/fixtures/child-include.adoc b/test/fixtures/child-include.adoc new file mode 100644 index 00000000..d47011d3 --- /dev/null +++ b/test/fixtures/child-include.adoc @@ -0,0 +1,3 @@ +first line of child + +last line of child diff --git a/test/fixtures/parent-include.adoc b/test/fixtures/parent-include.adoc new file mode 100644 index 00000000..45aa82dc --- /dev/null +++ b/test/fixtures/parent-include.adoc @@ -0,0 +1,5 @@ +first line of parent + +include::child-include.adoc[] + +last line of parent diff --git a/test/lexer_test.rb b/test/lexer_test.rb index df78aff1..da2c8159 100644 --- a/test/lexer_test.rb +++ b/test/lexer_test.rb @@ -7,6 +7,12 @@ context "Lexer" do assert Asciidoctor::Lexer.is_section_title?('=== AsciiDoc Home Page') end + test 'sanitize attribute name' do + assert_equal 'foobar', Asciidoctor::Lexer.sanitize_attribute_name("Foo Bar") + assert_equal 'foo', Asciidoctor::Lexer.sanitize_attribute_name("foo") + assert_equal 'foo3-bar', Asciidoctor::Lexer.sanitize_attribute_name("Foo 3^ # - Bar[") + end + test "collect unnamed attribute" do attributes = {} line = 'quote' diff --git a/test/lists_test.rb b/test/lists_test.rb index 199a4603..7631007b 100644 --- a/test/lists_test.rb +++ b/test/lists_test.rb @@ -1089,7 +1089,7 @@ Lists assert_xpath '//ul/li[1]/*', output, 1 end - test "consecutive blocks in list continuation attach to list item" do + test 'consecutive blocks in list continuation attach to list item' do input = <<-EOS Lists ===== @@ -1106,7 +1106,7 @@ ____ + * Item two EOS - output = render_string input + output = render_embedded_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '//ul/li[1]/p', output, 1 diff --git a/test/reader_test.rb b/test/reader_test.rb index 1366c99a..975bfcb2 100644 --- a/test/reader_test.rb +++ b/test/reader_test.rb @@ -1,128 +1,257 @@ require 'test_helper' class ReaderTest < Test::Unit::TestCase - # setup for test - def setup - @src_data = File.readlines(sample_doc_path(:asciidoc_index)) - @reader = Asciidoctor::Reader.new @src_data - end + DIRNAME = File.dirname __FILE__ - context "has_more_lines?" do - test "returns false for empty document" do - assert !Asciidoctor::Reader.new.has_more_lines? - end + SAMPLE_DATA = <<-EOS.each_line.to_a +first line +second line +third line + EOS + + context 'Reader' do + context 'Prepare lines' do + test 'should prepare lines from Array data' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA, reader.lines + end - test "returns true with lines remaining" do - assert @reader.has_more_lines?, "Yo, didn't work" + test 'should prepare lines from String data' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA, reader.lines + end end - end - context "with source data loaded" do - test "get_line returns next line" do - assert_equal @src_data[0], @reader.get_line - end + context 'With empty data' do + test 'has_more_lines? should return false with empty data' do + assert !Asciidoctor::Reader.new.has_more_lines? + end - test "get_line consumes the line it returns" do - reader = Asciidoctor::Reader.new(["foo", "bar"]) - _ = reader.get_line - second = reader.get_line - assert_equal "bar", second - end + test 'empty? should return true with empty data' do + assert Asciidoctor::Reader.new.empty? + assert Asciidoctor::Reader.new.eof? + end - test "peek_line does not consume the line it returns" do - reader = Asciidoctor::Reader.new(["foo", "bar"]) - _ = reader.peek_line - second = reader.peek_line - assert_equal "foo", second - end + test 'next_line_empty? should return true with empty data' do + assert Asciidoctor::Reader.new.next_line_empty? + end - test "unshift puts line onto Reader instance for the next get_line" do - reader = Asciidoctor::Reader.new(["foo"]) - reader.unshift("bar") - assert_equal "bar", reader.get_line - assert_equal "foo", reader.get_line + test 'peek_line should return nil with empty data' do + assert_nil Asciidoctor::Reader.new.peek_line + end + + test 'peek_lines should return empty Array with empty data' do + assert_equal [], Asciidoctor::Reader.new.peek_lines + end + + test 'read_line should return nil with empty data' do + assert_nil Asciidoctor::Reader.new.read_line + assert_nil Asciidoctor::Reader.new.get_line + end + + test 'read_lines should return empty Array with empty data' do + assert_equal [], Asciidoctor::Reader.new.read_lines + assert_equal [], Asciidoctor::Reader.new.get_lines + end end - test 'get lines consumes all remaining lines' do - input = <<-EOS -line 1 -line 2 -line 3 - EOS - input_lines = input.lines.entries - reader = Asciidoctor::Reader.new input_lines - assert_equal input_lines, reader.lines - assert_equal 'line 1', reader.get_line.chomp - assert_equal input_lines[1..-1], reader.get_lines - assert !reader.has_more_lines? - assert_equal 3, reader.lineno + context 'With data' do + test 'has_more_lines? should return true if there are lines remaining' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert reader.has_more_lines? + end + + test 'empty? should return false if there are lines remaining' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert !reader.empty? + assert !reader.eof? + end + + test 'next_line_empty? should return false if next line is not blank' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert !reader.next_line_empty? + end + + test 'next_line_empty? should return true if next line is blank' do + reader = Asciidoctor::Reader.new ["\n", "second line\n"] + assert reader.next_line_empty? + end + + test 'peek_line should return next line if there are lines remaining' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA.first, reader.peek_line + end + + test 'peek_line should not consume line or increment line number' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA.first, reader.peek_line + assert_equal SAMPLE_DATA.first, reader.peek_line + assert_equal 1, reader.lineno + end + + test 'peek_line should return next lines if there are lines remaining' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA[0..1], reader.peek_lines(2) + end + + test 'peek_lines should not consume lines or increment line number' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA[0..1], reader.peek_lines(2) + assert_equal SAMPLE_DATA[0..1], reader.peek_lines(2) + assert_equal 1, reader.lineno + end + + test 'peek_lines should not invert order of lines' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA, reader.lines + reader.peek_lines 3 + assert_equal SAMPLE_DATA, reader.lines + end + + test 'read_line should return next line if there are lines remaining' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA.first, reader.read_line + end + + test 'read_line should consume next line and increment line number' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA[0], reader.read_line + assert_equal SAMPLE_DATA[1], reader.read_line + assert_equal 3, reader.lineno + end + + test 'advance should consume next line and return a Boolean indicating if a line was consumed' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert reader.advance + assert reader.advance + assert reader.advance + assert !reader.advance + end + + test 'read_lines should return all lines' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA, reader.read_lines + end + + test 'read should return all lines joined as String' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + assert_equal SAMPLE_DATA.join, reader.read + end + + test 'has_more_lines? should return false after read_lines is invoked' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + reader.read_lines + assert !reader.has_more_lines? + end + + test 'unshift puts line onto Reader as next line to read' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + reader.unshift "line zero\n" + assert_equal "line zero\n", reader.peek_line + assert_equal "line zero\n", reader.get_line + assert_equal 1, reader.lineno + end + + test 'terminate should consume all lines and update line number' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + reader.terminate + assert reader.eof? + assert_equal 4, reader.lineno + end + + test 'skip_blank_lines should skip blank lines' do + reader = Asciidoctor::Reader.new ["", "\n"].concat(SAMPLE_DATA) + reader.skip_blank_lines + assert_equal SAMPLE_DATA.first, reader.peek_line + end + + test 'lines should return remaining lines' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + reader.read_line + assert_equal SAMPLE_DATA[1..-1], reader.lines + end + + test 'source_lines should return copy of original data Array' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + reader.read_lines + assert_equal SAMPLE_DATA, reader.source_lines + end + + test 'source should return original data Array joined as String' do + reader = Asciidoctor::Reader.new SAMPLE_DATA + reader.read_lines + assert_equal SAMPLE_DATA.join, reader.source + end + end - test 'terminate should consume remaining lines' do - input = <<-EOS -line 1 -line 2 -line 3 - EOS - input_lines = input.lines.entries - reader = Asciidoctor::Reader.new input_lines - assert_equal input_lines, reader.lines - assert_equal 'line 1', reader.get_line.chomp - reader.terminate - assert !reader.has_more_lines? - assert_equal 3, reader.lineno + context 'Line context' do + test 'to_s should return file name and line number of current line' do + reader = Asciidoctor::Reader.new SAMPLE_DATA, 'sample.ad' + reader.read_line + assert_equal 'sample.ad: line 2', reader.to_s + end + + test 'line_info should return file name and line number of current line' do + reader = Asciidoctor::Reader.new SAMPLE_DATA, 'sample.ad' + reader.read_line + assert_equal 'sample.ad: line 2', reader.line_info + assert_equal 'sample.ad: line 2', reader.next_line_info + end + + test 'prev_line_info should return file name and line number of previous line read' do + reader = Asciidoctor::Reader.new SAMPLE_DATA, 'sample.ad' + reader.read_line + assert_equal 'sample.ad: line 1', reader.prev_line_info + end end - end - context "Grab lines" do - test "Grab until end" do - input = <<-EOS + context 'Take lines' do + test 'Take lines until end' do + lines = <<-EOS.each_line.to_a This is one paragraph. This is another paragraph. - EOS - - lines = input.lines.entries - reader = Asciidoctor::Reader.new(lines) - result = reader.grab_lines_until - assert_equal 3, result.size - assert_equal lines, result - assert !reader.has_more_lines? - assert reader.empty? - end + EOS + + reader = Asciidoctor::Reader.new lines + result = reader.take_lines_until + assert_equal 3, result.size + assert_equal lines, result + assert !reader.has_more_lines? + assert reader.eof? + end - test "Grab until blank line" do - input = <<-EOS + test 'Take lines until blank line' do + lines = <<-EOS.each_line.to_a This is one paragraph. This is another paragraph. - EOS + EOS - lines = input.lines.entries - reader = Asciidoctor::Reader.new(lines) - result = reader.grab_lines_until :break_on_blank_lines => true - assert_equal 1, result.size - assert_equal lines.first, result.first - assert_equal lines.last, reader.peek_line - end + reader = Asciidoctor::Reader.new lines + result = reader.take_lines_until :break_on_blank_lines => true + assert_equal 1, result.size + assert_equal lines.first.chomp, result.first + assert_equal lines.last, reader.peek_line + end - test "Grab until blank line preserving last line" do - input = <<-EOS + test 'Take lines until blank line preserving last line' do + lines = <<-EOS.each_line.to_a This is one paragraph. This is another paragraph. - EOS + EOS - lines = input.lines.entries - reader = Asciidoctor::Reader.new(lines) - result = reader.grab_lines_until :break_on_blank_lines => true, :preserve_last_line => true - assert_equal 1, result.size - assert_equal lines.first, result.first - assert_equal "\n", reader.peek_line - end + reader = Asciidoctor::Reader.new lines + result = reader.take_lines_until :break_on_blank_lines => true, :preserve_last_line => true + assert_equal 1, result.size + assert_equal lines.first.chomp, result.first + assert reader.next_line_empty? + end - test "Grab until condition" do - input = <<-EOS + test 'Take lines until condition is true' do + lines = <<-EOS.each_line.to_a -- This is one paragraph inside the block. @@ -130,19 +259,18 @@ This is another paragraph inside the block. -- This is a paragraph outside the block. - EOS - - lines = input.lines.entries - reader = Asciidoctor::Reader.new(lines) - reader.get_line - result = reader.grab_lines_until {|line| line.chomp == '--' } - assert_equal 3, result.size - assert_equal lines[1, 3], result - assert_equal "\n", reader.peek_line - end + EOS + + reader = Asciidoctor::Reader.new lines + reader.read_line + result = reader.take_lines_until {|line| line.chomp == '--' } + assert_equal 3, result.size + assert_equal lines[1, 3], result + assert reader.next_line_empty? + end - test "Grab until condition with last line" do - input = <<-EOS + test 'Take lines until condition is true, taking last line' do + lines = <<-EOS.each_line.to_a -- This is one paragraph inside the block. @@ -150,19 +278,18 @@ This is another paragraph inside the block. -- This is a paragraph outside the block. - EOS - - lines = input.lines.entries - reader = Asciidoctor::Reader.new(lines) - reader.get_line - result = reader.grab_lines_until(:grab_last_line => true) {|line| line.chomp == '--' } - assert_equal 4, result.size - assert_equal lines[1, 4], result - assert_equal "\n", reader.peek_line - end + EOS + + reader = Asciidoctor::Reader.new lines + reader.read_line + result = reader.take_lines_until(:take_last_line => true) {|line| line.chomp == '--' } + assert_equal 4, result.size + assert_equal lines[1, 4], result + assert reader.next_line_empty? + end - test "Grab until condition with last line and preserving last line" do - input = <<-EOS + test 'Take lines until condition is true, taking and preserving last line' do + lines = <<-EOS.each_line.to_a -- This is one paragraph inside the block. @@ -170,357 +297,530 @@ This is another paragraph inside the block. -- This is a paragraph outside the block. - EOS - - lines = input.lines.entries - reader = Asciidoctor::Reader.new(lines) - reader.get_line - result = reader.grab_lines_until(:grab_last_line => true, :preserve_last_line => true) {|line| line.chomp == '--' } - assert_equal 4, result.size - assert_equal lines[1, 4], result - assert_equal "--\n", reader.peek_line + EOS + + reader = Asciidoctor::Reader.new lines + reader.read_line + result = reader.take_lines_until(:take_last_line => true, :preserve_last_line => true) {|line| line.chomp == '--' } + assert_equal 4, result.size + assert_equal lines[1, 4], result + assert_equal "--\n", reader.peek_line + end end end - context 'Include Macro' do - test 'include macro is disabled by default and becomes a link' do - input = <<-EOS -include::include-file.asciidoc[] - EOS - para = block_from_string input, :attributes => { 'include-depth' => 0 } - assert_equal 1, para.lines.size - #assert_equal 'include::include-file.asciidoc[]', para.source - assert_equal 'link:include-file.asciidoc[include-file.asciidoc]', para.source + context 'PreprocessorReader' do + context 'Type hierarchy' do + test 'PreprocessorReader should extend from Reader' do + doc = Asciidoctor::Document.new + reader = Asciidoctor::PreprocessorReader.new doc + assert reader.is_a?(Asciidoctor::Reader) + end + + test 'PreprocessorReader should invoke or emulate Reader initializer' do + doc = Asciidoctor::Document.new + reader = Asciidoctor::PreprocessorReader.new doc, SAMPLE_DATA + assert_equal SAMPLE_DATA, reader.lines + assert_equal 1, reader.lineno + end end - test 'include macro is enabled when safe mode is less than SECURE' do - input = <<-EOS -include::fixtures/include-file.asciidoc[] - EOS + context 'Prepare lines' do + test 'should prepare and normalize lines from Array data' do + doc = Asciidoctor::Document.new + data = SAMPLE_DATA.map {|line| line.chomp} + data.unshift '' + data.push '' + reader = Asciidoctor::PreprocessorReader.new doc, data + assert_equal SAMPLE_DATA, reader.lines + end - doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)} - output = doc.render - assert_match(/included content/, output) - end + test 'should prepare and normalize lines from String data' do + doc = Asciidoctor::Document.new + data = SAMPLE_DATA.map {|line| line.chomp} + data.unshift ' ' + data.push ' ' + data_as_string = data * "\n" + reader = Asciidoctor::PreprocessorReader.new doc, data_as_string + assert_equal SAMPLE_DATA, reader.lines + end - test 'missing file referenced by include macro does not crash processor' do - input = <<-EOS -include::fixtures/no-such-file.ad[] + test 'should clean CRLF from end of lines' do + input = <<-EOS +source\r +with\r +CRLF\r +endlines\r EOS - begin - doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)} - assert_equal 0, doc.blocks.size - rescue - flunk 'include macro should not raise exception on missing file' + doc = Asciidoctor::Document.new + [input, input.lines, input.split("\n"), input.split("\n").join].each do |lines| + reader = Asciidoctor::PreprocessorReader.new doc, lines + reader.lines.each do |line| + assert !line.end_with?("\r"), "CRLF not properly cleaned for source lines: #{lines.inspect}" + assert !line.end_with?("\r\n"), "CRLF not properly cleaned for source lines: #{lines.inspect}" + assert line.end_with?("\n"), "CRLF not properly cleaned for source lines: #{lines.inspect}" + end + end end - end - test 'include macro can retrieve data from uri' do - input = <<-EOS -.... -include::https://raw.github.com/asciidoctor/asciidoctor/master/LICENSE[] -.... - EOS + test 'should not skip front matter by default' do + input = <<-EOS +--- +layout: post +title: Document Title +author: username +tags: [ first, second ] +--- += Document Title +Author Name + +preamble + EOS - output = render_embedded_string input, :safe => :safe, :attributes => {'allow-uri-read' => ''} - assert_match(/MIT/, output) + doc = Asciidoctor::Document.new + reader = Asciidoctor::PreprocessorReader.new doc, input + assert_equal '---', reader.peek_line.chomp end - test 'inaccessible uri referenced by include macro does not crash processor' do - input = <<-EOS -.... -include::http://127.0.0.1:0[] -.... - EOS + test 'should skip front matter if specified by skip-front-matter attribute' do + front_matter = %(layout: post +title: Document Title +author: username +tags: [ first, second ]) + input = <<-EOS +--- +#{front_matter} +--- += Document Title +Author Name - begin - output = render_embedded_string input, :safe => :safe, :attributes => {'allow-uri-read' => ''} - assert_css 'pre', output, 1 - assert_css 'pre *', output, 0 - rescue - flunk 'include macro should not raise exception on inaccessible uri' +preamble + EOS + + doc = Asciidoctor::Document.new [], :attributes => {'skip-front-matter' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + assert_equal '= Document Title', reader.peek_line.chomp + assert_equal front_matter, doc.attributes['front-matter'] end end - test 'include macro supports line selection' do - input = <<-EOS -include::fixtures/include-file.asciidoc[lines=1;3..4;6..-1] - EOS + context 'Include Macro' do + test 'include macro is disabled by default and becomes a link' do + input = <<-EOS +include::include-file.asciidoc[] + EOS + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input + assert_equal 'link:include-file.asciidoc[]', reader.read_line.chomp + end + + test 'include macro is enabled when safe mode is less than SECURE' do + input = <<-EOS +include::fixtures/include-file.asciidoc[] + EOS + + doc = document_from_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME + output = doc.render + assert_match(/included content/, output) + end - output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :header_footer => false, :attributes => {'docdir' => File.dirname(__FILE__)} - assert_match(/first line/, output) - assert_no_match(/second line/, output) - assert_match(/third line/, output) - assert_match(/fourth line/, output) - assert_no_match(/fifth line/, output) - assert_match(/sixth line/, output) - assert_match(/seventh line/, output) - assert_match(/eighth line/, output) - assert_match(/last line of included content/, output) - end + test 'include macro should resolve file relative to current include' do + input = <<-EOS +include::fixtures/parent-include.adoc[] + EOS - test 'include macro supports line selection using quoted attribute value' do - input = <<-EOS -include::fixtures/include-file.asciidoc[lines="1, 3..4 , 6 .. -1"] - EOS + pseudo_docfile = File.join DIRNAME, 'include-master.adoc' + fixtures_dir = File.join DIRNAME, 'fixtures' + parent_include_docfile = File.join fixtures_dir, 'parent-include.adoc' + child_include_docfile = File.join fixtures_dir, 'child-include.adoc' - output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :header_footer => false, :attributes => {'docdir' => File.dirname(__FILE__)} - assert_match(/first line/, output) - assert_no_match(/second line/, output) - assert_match(/third line/, output) - assert_match(/fourth line/, output) - assert_no_match(/fifth line/, output) - assert_match(/sixth line/, output) - assert_match(/seventh line/, output) - assert_match(/eighth line/, output) - assert_match(/last line of included content/, output) - end + doc = empty_safe_document :base_dir => DIRNAME + reader = Asciidoctor::PreprocessorReader.new doc, input, pseudo_docfile - test 'include macro supports tagged selection' do - input = <<-EOS -include::fixtures/include-file.asciidoc[tags=snippetA;snippetB] - EOS + assert_equal pseudo_docfile, reader.file + assert_equal DIRNAME, reader.dir + assert_equal 'include-master.adoc', reader.path - output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :header_footer => false, :attributes => {'docdir' => File.dirname(__FILE__)} - assert_match(/snippetA content/, output) - assert_match(/snippetB content/, output) - assert_no_match(/non-tagged content/, output) - assert_no_match(/included content/, output) - end + assert_equal "first line of parent\n", reader.read_line - test 'lines attribute takes precedence over tags attribute in include macro' do - input = <<-EOS -include::fixtures/include-file.asciidoc[lines=1, tags=snippetA;snippetB] - EOS + assert_equal 'fixtures/parent-include.adoc: line 1', reader.prev_line_info + assert_equal parent_include_docfile, reader.file + assert_equal fixtures_dir, reader.dir + assert_equal 'fixtures/parent-include.adoc', reader.path - output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :header_footer => false, :attributes => {'docdir' => File.dirname(__FILE__)} - assert_match(/first line of included content/, output) - assert_no_match(/snippetA content/, output) - assert_no_match(/snippetB content/, output) - end + reader.skip_blank_lines + + assert_equal "first line of child\n", reader.read_line - test 'indent of included file can be reset to size of indent attribute' do - input = <<-EOS + assert_equal 'fixtures/child-include.adoc: line 1', reader.prev_line_info + assert_equal child_include_docfile, reader.file + assert_equal fixtures_dir, reader.dir + assert_equal 'fixtures/child-include.adoc', reader.path + + reader.skip_blank_lines + + assert_equal "last line of child\n", reader.read_line + + reader.skip_blank_lines + + assert_equal "last line of parent\n", reader.read_line + + assert_equal 'fixtures/parent-include.adoc: line 5', reader.prev_line_info + assert_equal parent_include_docfile, reader.file + assert_equal fixtures_dir, reader.dir + assert_equal 'fixtures/parent-include.adoc', reader.path + end + + test 'missing file referenced by include macro does not crash processor' do + input = <<-EOS +include::fixtures/no-such-file.ad[] + EOS + + begin + doc = document_from_string input, :safe => :safe, :base_dir => DIRNAME + assert_equal 0, doc.blocks.size + rescue + flunk 'include macro should not raise exception on missing file' + end + end + + test 'include macro can retrieve data from uri' do + input = <<-EOS +.... +include::https://raw.github.com/asciidoctor/asciidoctor/master/LICENSE[] +.... + EOS + + output = render_embedded_string input, :safe => :safe, :attributes => {'allow-uri-read' => ''} + assert_match(/MIT/, output) + end + + test 'inaccessible uri referenced by include macro does not crash processor' do + input = <<-EOS +.... +include::http://127.0.0.1:0[] +.... + EOS + + begin + output = render_embedded_string input, :safe => :safe, :attributes => {'allow-uri-read' => ''} + assert_css 'pre:empty', output, 1 + rescue + flunk 'include macro should not raise exception on inaccessible uri' + end + end + + test 'include macro supports line selection' do + input = <<-EOS +include::fixtures/include-file.asciidoc[lines=1;3..4;6..-1] + EOS + + output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME + assert_match(/first line/, output) + assert_no_match(/second line/, output) + assert_match(/third line/, output) + assert_match(/fourth line/, output) + assert_no_match(/fifth line/, output) + assert_match(/sixth line/, output) + assert_match(/seventh line/, output) + assert_match(/eighth line/, output) + assert_match(/last line of included content/, output) + end + + test 'include macro supports line selection using quoted attribute value' do + input = <<-EOS +include::fixtures/include-file.asciidoc[lines="1, 3..4 , 6 .. -1"] + EOS + + output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME + assert_match(/first line/, output) + assert_no_match(/second line/, output) + assert_match(/third line/, output) + assert_match(/fourth line/, output) + assert_no_match(/fifth line/, output) + assert_match(/sixth line/, output) + assert_match(/seventh line/, output) + assert_match(/eighth line/, output) + assert_match(/last line of included content/, output) + end + + test 'include macro supports tagged selection' do + input = <<-EOS +include::fixtures/include-file.asciidoc[tags=snippetA;snippetB] + EOS + + output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME + assert_match(/snippetA content/, output) + assert_match(/snippetB content/, output) + assert_no_match(/non-tagged content/, output) + assert_no_match(/included content/, output) + end + + test 'lines attribute takes precedence over tags attribute in include macro' do + input = <<-EOS +include::fixtures/include-file.asciidoc[lines=1, tags=snippetA;snippetB] + EOS + + output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME + assert_match(/first line of included content/, output) + assert_no_match(/snippetA content/, output) + assert_no_match(/snippetB content/, output) + end + + test 'indent of included file can be reset to size of indent attribute' do + input = <<-EOS [source, xml] ---- include::fixtures/basic-docinfo.xml[lines=2..3, indent=0] ---- - EOS - - output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :header_footer => false, :attributes => {'docdir' => File.dirname(__FILE__)} - result = xmlnodes_at_xpath('//pre', output, 1).text - assert_equal "<year>2013</year>\n<holder>Acme, Inc.</holder>", result - end - - test "block is called to handle an include macro" do - input = <<-EOS + EOS + + output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME + result = xmlnodes_at_xpath('//pre', output, 1).text + assert_equal "<year>2013</year>\n<holder>Acme, Inc.</holder>", result + end + +=begin + test 'block is called to handle an include macro' do + input = <<-EOS first line include::include-file.asciidoc[] last line - EOS - document = Asciidoctor::Document.new [], :safe => Asciidoctor::SafeMode::SAFE - reader = Asciidoctor::Reader.new(input.lines.entries, document, true) {|inc, doc| - ":includefile: #{inc}\n\nmiddle line".lines.entries - } - lines = [] - while reader.has_more_lines? - lines << reader.get_line - end - assert_match(/^:includefile: include-file.asciidoc$/, lines.join) - end - - test 'attributes are substituted in target of include macro' do - input = <<-EOS + EOS + document = Asciidoctor::Document.new [], :safe => Asciidoctor::SafeMode::SAFE + reader = Asciidoctor::Reader.new(input.lines.entries, document, true) {|inc, doc| + ":includefile: #{inc}\n\nmiddle line".lines.entries + } + lines = [] + while reader.has_more_lines? + lines << reader.get_line + end + assert_match(/^:includefile: include-file.asciidoc$/, lines.join) + end +=end + + test 'attributes are substituted in target of include macro' do + input = <<-EOS :fixturesdir: fixtures :ext: asciidoc include::{fixturesdir}/include-file.{ext}[] - EOS - - doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)} - output = doc.render - assert_match(/included content/, output) - end - - test 'line is dropped if target of include macro resolves to empty' do - input = <<-EOS + EOS + + doc = document_from_string input, :safe => :safe, :base_dir => DIRNAME + output = doc.render + assert_match(/included content/, output) + end + + test 'line is skipped by default if target of include macro resolves to empty' do + input = <<-EOS include::{foodir}/include-file.asciidoc[] - EOS - - output = render_embedded_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)} - assert output.strip.empty? - end + EOS + + doc = empty_safe_document :base_dir => DIRNAME + reader = Asciidoctor::PreprocessorReader.new doc, input + assert_equal "include::{foodir}/include-file.asciidoc[]\n", reader.read_line + end - test 'line is dropped but not following line if target of include macro resolves to empty' do - input = <<-EOS + test 'line is dropped if target of include macro resolves to empty and attribute-missing attribute is not skip' do + input = <<-EOS +include::{foodir}/include-file.asciidoc[] + EOS + + doc = empty_safe_document :base_dir => DIRNAME, :attributes => {'attribute-missing' => 'drop'} + reader = Asciidoctor::PreprocessorReader.new doc, input + assert_nil reader.read_line + end + + test 'line following dropped include is not dropped' do + input = <<-EOS include::{foodir}/include-file.asciidoc[] yo - EOS - - output = render_embedded_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)} - assert_xpath '//p', output, 1 - assert_xpath '//p[text()="yo"]', output, 1 - end - - test 'escaped include macro is left unprocessed' do - input = <<-EOS -\\include::include-file.asciidoc[] - EOS - para = block_from_string input - assert_equal 1, para.lines.size - assert_equal 'include::include-file.asciidoc[]', para.source - end - - test 'include macro not at start of line is ignored' do - input = <<-EOS + EOS + + doc = empty_safe_document :base_dir => DIRNAME, :attributes => {'attribute-missing' => 'drop'} + reader = Asciidoctor::PreprocessorReader.new doc, input + assert_equal "yo\n", reader.read_line + end + + test 'escaped include macro is left unprocessed' do + input = <<-EOS +\\include::fixtures/include-file.asciidoc[] +\\escape preserved here + EOS + doc = empty_safe_document :base_dir => DIRNAME + reader = Asciidoctor::PreprocessorReader.new doc, input + # we should be able to peek it multiple times and still have the backslash preserved + # this is the test for @unescape_next_line + assert_equal 'include::fixtures/include-file.asciidoc[]', reader.peek_line.chomp + assert_equal 'include::fixtures/include-file.asciidoc[]', reader.peek_line.chomp + assert_equal 'include::fixtures/include-file.asciidoc[]', reader.read_line.chomp + assert_equal '\\escape preserved here', reader.read_line.chomp + end + + test 'include macro not at start of line is ignored' do + input = <<-EOS include::include-file.asciidoc[] - EOS - para = block_from_string input - assert_equal 1, para.lines.size - # NOTE the space gets stripped because the line is treated as an inline literal - assert_equal :literal, para.context - assert_equal 'include::include-file.asciidoc[]', para.source - end - - test 'include macro is disabled when include-depth attribute is 0' do - input = <<-EOS + EOS + para = block_from_string input + assert_equal 1, para.lines.size + # NOTE the space gets stripped because the line is treated as an inline literal + assert_equal :literal, para.context + assert_equal 'include::include-file.asciidoc[]', para.source + end + + test 'include macro is disabled when include-depth attribute is 0' do + input = <<-EOS include::include-file.asciidoc[] - EOS - para = block_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => { 'include-depth' => 0 } - assert_equal 1, para.lines.size - assert_equal 'include::include-file.asciidoc[]', para.source - end - - test 'include-depth cannot be set by document' do - input = <<-EOS + EOS + para = block_from_string input, :safe => :safe, :attributes => { 'include-depth' => 0 } + assert_equal 1, para.lines.size + assert_equal 'include::include-file.asciidoc[]', para.source + end + + test 'include-depth cannot be set by document' do + input = <<-EOS :include-depth: 1 - + include::include-file.asciidoc[] - EOS - para = block_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => { 'include-depth' => 0 } - assert_equal 1, para.lines.size - assert_equal 'include::include-file.asciidoc[]', para.source - end - end + EOS + para = block_from_string input, :safe => :safe, :attributes => { 'include-depth' => 0 } + assert_equal 1, para.lines.size + assert_equal 'include::include-file.asciidoc[]', para.source + end - context 'build secure asset path' do - test 'allows us to specify a path relative to the current dir' do - doc = Asciidoctor::Document.new - Asciidoctor::Reader.new([], doc, true) - legit_path = Dir.pwd + "/foo" - assert_equal legit_path, doc.normalize_asset_path(legit_path) - end + test 'include macro should be disabled if max include depth has been exceeded' do + input = <<-EOS +include::fixtures/parent-include.adoc[depth=1] + EOS - test "keeps naughty absolute paths from getting outside" do - naughty_path = "#{disk_root}etc/passwd" - doc = Asciidoctor::Document.new - Asciidoctor::Reader.new([], doc, true) - secure_path = doc.normalize_asset_path(naughty_path) - assert naughty_path != secure_path - assert_match(/^#{doc.base_dir}/, secure_path) - end + pseudo_docfile = File.join DIRNAME, 'include-master.adoc' + + doc = empty_safe_document :base_dir => DIRNAME + reader = Asciidoctor::PreprocessorReader.new doc, input, pseudo_docfile - test "keeps naughty relative paths from getting outside" do - naughty_path = "safe/ok/../../../../../etc/passwd" - doc = Asciidoctor::Document.new - Asciidoctor::Reader.new([], doc, true) - secure_path = doc.normalize_asset_path(naughty_path) - assert naughty_path != secure_path - assert_match(/^#{doc.base_dir}/, secure_path) + lines = reader.readlines + assert lines.include?("include::child-include.adoc[]\n") + end end - end - context 'Conditional Inclusions' do - test 'preprocess_next_line returns true if cursor advanced' do - input = <<-EOS + context 'Conditional Inclusions' do + test 'preprocess_line returns nil if cursor advanced' do + input = <<-EOS ifdef::asciidoctor[] Asciidoctor! endif::asciidoctor[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - assert reader.preprocess_next_line == true - end + EOS + + reader = Asciidoctor::PreprocessorReader.new empty_document, input + assert_nil reader.process_line(reader.lines.first) + end - test 'preprocess_next_line returns false if cursor not advanced' do - input = <<-EOS + test 'peek_line advances cursor to next conditional line of content' do + input = <<-EOS +ifdef::asciidoctor[] +Asciidoctor! +endif::asciidoctor[] + EOS + + reader = Asciidoctor::PreprocessorReader.new empty_document, input + assert_equal 1, reader.lineno + assert_equal "Asciidoctor!\n", reader.peek_line + assert_equal 2, reader.lineno + end + + test 'process_line returns line if cursor not advanced' do + input = <<-EOS content ifdef::asciidoctor[] Asciidoctor! endif::asciidoctor[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - assert reader.preprocess_next_line == false - end + EOS + + reader = Asciidoctor::PreprocessorReader.new empty_document, input + assert_not_nil reader.process_line(reader.lines.first) + end - test 'preprocess_next_line returns nil if cursor advanced past end of source' do - input = <<-EOS + test 'peek_line does not advance cursor when on a regular content line' do + input = <<-EOS +content +ifdef::asciidoctor[] +Asciidoctor! +endif::asciidoctor[] + EOS + + reader = Asciidoctor::PreprocessorReader.new empty_document, input + assert_equal 1, reader.lineno + assert_equal "content\n", reader.peek_line + assert_equal 1, reader.lineno + end + + test 'peek_line returns nil if cursor advances past end of source' do + input = <<-EOS ifdef::foobar[] swallowed content endif::foobar[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - assert reader.preprocess_next_line.nil? - end - - test 'ifdef with defined attribute includes block' do - input = <<-EOS + EOS + + reader = Asciidoctor::PreprocessorReader.new empty_document, input + assert_equal 1, reader.lineno + assert_nil reader.peek_line + assert_equal 4, reader.lineno + end + + test 'ifdef with defined attribute includes content' do + input = <<-EOS ifdef::holygrail[] There is a holy grail! endif::holygrail[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'holygrail' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line - end - assert_equal 'There is a holy grail!', lines.join.strip - end - - test 'ifdef with defined attribute includes text in brackets' do - input = <<-EOS + EOS + + doc = empty_document :attributes => {'holygrail' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'There is a holy grail!', lines.join.chomp + end + + test 'ifdef with defined attribute includes text in brackets' do + input = <<-EOS On our quest we go... ifdef::holygrail[There is a holy grail!] There was much rejoicing. - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'holygrail' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line - end - assert_equal "On our quest we go...\nThere is a holy grail!\nThere was much rejoicing.", lines.join.strip - end - - test 'ifndef with defined attribute does not include text in brackets' do - input = <<-EOS + EOS + + doc = empty_document :attributes => {'holygrail' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal "On our quest we go...\nThere is a holy grail!\nThere was much rejoicing.", lines.join.chomp + end + + test 'ifndef with defined attribute does not include text in brackets' do + input = <<-EOS On our quest we go... ifndef::hardships[There is a holy grail!] There was no rejoicing. - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'hardships' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line - end - assert_equal "On our quest we go...\nThere was no rejoicing.", lines.join.strip - end - - test 'include with non-matching nested exclude' do - input = <<-EOS + EOS + + doc = empty_document :attributes => {'hardships' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal "On our quest we go...\nThere was no rejoicing.", lines.join.chomp + end + + test 'include with non-matching nested exclude' do + input = <<-EOS ifdef::grail[] holy ifdef::swallow[] @@ -528,37 +828,37 @@ swallow endif::swallow[] grail endif::grail[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'grail' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line - end - assert_equal "holy\ngrail", lines.join.strip - end - - test 'nested excludes with same condition' do - input = <<-EOS + EOS + + doc = empty_document :attributes => {'grail' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal "holy\ngrail", lines.join.chomp + end + + test 'nested excludes with same condition' do + input = <<-EOS ifndef::grail[] ifndef::grail[] not here endif::grail[] endif::grail[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'grail' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line - end - assert_equal '', lines.join.strip - end - - test 'include with nested exclude of inverted condition' do - input = <<-EOS + EOS + + doc = empty_document :attributes => {'grail' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal '', lines.join.chomp + end + + test 'include with nested exclude of inverted condition' do + input = <<-EOS ifdef::grail[] holy ifndef::grail[] @@ -566,19 +866,19 @@ not here endif::grail[] grail endif::grail[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'grail' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line - end - assert_equal "holy\ngrail", lines.join.strip - end - - test 'exclude with matching nested exclude' do - input = <<-EOS + EOS + + doc = empty_document :attributes => {'grail' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal "holy\ngrail", lines.join.chomp + end + + test 'exclude with matching nested exclude' do + input = <<-EOS poof ifdef::swallow[] no @@ -588,19 +888,19 @@ endif::swallow[] here endif::swallow[] gone - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'grail' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line - end - assert_equal "poof\ngone", lines.join.strip - end - - test 'exclude with nested include using shorthand end' do - input = <<-EOS + EOS + + doc = empty_document :attributes => {'grail' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal "poof\ngone", lines.join.chomp + end + + test 'exclude with nested include using shorthand end' do + input = <<-EOS poof ifndef::grail[] no grail @@ -610,364 +910,303 @@ endif::[] in here endif::[] gone - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'grail' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line - end - assert_equal "poof\ngone", lines.join.strip - end - - test 'ifdef with one alternative attribute set includes content' do - input = <<-EOS + EOS + + doc = empty_document :attributes => {'grail' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal "poof\ngone", lines.join.chomp + end + + test 'ifdef with one alternative attribute set includes content' do + input = <<-EOS ifdef::holygrail,swallow[] Our quest is complete! endif::holygrail,swallow[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'swallow' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document :attributes => {'swallow' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'Our quest is complete!', lines.join.chomp end - assert_equal 'Our quest is complete!', lines.join.strip - end - - test 'ifdef with no alternative attributes set does not include content' do - input = <<-EOS + + test 'ifdef with no alternative attributes set does not include content' do + input = <<-EOS ifdef::holygrail,swallow[] Our quest is complete! endif::holygrail,swallow[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal '', lines.join.chomp end - assert_equal '', lines.join.strip - end - - test 'ifdef with all required attributes set includes content' do - input = <<-EOS + + test 'ifdef with all required attributes set includes content' do + input = <<-EOS ifdef::holygrail+swallow[] Our quest is complete! endif::holygrail+swallow[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'holygrail' => '', 'swallow' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document :attributes => {'holygrail' => '', 'swallow' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'Our quest is complete!', lines.join.chomp end - assert_equal 'Our quest is complete!', lines.join.strip - end - - test 'ifdef with missing required attributes does not include content' do - input = <<-EOS + + test 'ifdef with missing required attributes does not include content' do + input = <<-EOS ifdef::holygrail+swallow[] Our quest is complete! endif::holygrail+swallow[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'holygrail' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document :attributes => {'holygrail' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal '', lines.join.chomp end - assert_equal '', lines.join.strip - end - - test 'ifndef with undefined attribute includes block' do - input = <<-EOS + + test 'ifndef with undefined attribute includes block' do + input = <<-EOS ifndef::holygrail[] Our quest continues to find the holy grail! endif::holygrail[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'Our quest continues to find the holy grail!', lines.join.chomp end - assert_equal 'Our quest continues to find the holy grail!', lines.join.strip - end - - test 'ifndef with one alternative attribute set includes content' do - input = <<-EOS + + test 'ifndef with one alternative attribute set includes content' do + input = <<-EOS ifndef::holygrail,swallow[] Our quest is complete! endif::holygrail,swallow[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'swallow' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document :attributes => {'swallow' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'Our quest is complete!', lines.join.chomp end - assert_equal 'Our quest is complete!', lines.join.strip - end - - test 'ifndef with no alternative attributes set includes content' do - input = <<-EOS + + test 'ifndef with no alternative attributes set includes content' do + input = <<-EOS ifndef::holygrail,swallow[] Our quest is complete! endif::holygrail,swallow[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'Our quest is complete!', lines.join.chomp end - assert_equal 'Our quest is complete!', lines.join.strip - end - - test 'ifndef with any required attributes set does not include content' do - input = <<-EOS + + test 'ifndef with any required attributes set does not include content' do + input = <<-EOS ifndef::holygrail+swallow[] Our quest is complete! endif::holygrail+swallow[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'swallow' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document :attributes => {'swallow' => ''} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal '', lines.join.chomp end - assert_equal '', lines.join.strip - end - - test 'ifndef with no required attributes set includes content' do - input = <<-EOS + + test 'ifndef with no required attributes set includes content' do + input = <<-EOS ifndef::holygrail+swallow[] Our quest is complete! endif::holygrail+swallow[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'Our quest is complete!', lines.join.chomp end - assert_equal 'Our quest is complete!', lines.join.strip - end - - test 'escaped ifdef is unescaped and ignored' do - input = <<-EOS + + test 'escaped ifdef is unescaped and ignored' do + input = <<-EOS \\ifdef::holygrail[] content \\endif::holygrail[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal "ifdef::holygrail[]\ncontent\nendif::holygrail[]", lines.join.chomp end - assert_equal "ifdef::holygrail[]\ncontent\nendif::holygrail[]", lines.join.strip - end - - test 'ifeval comparing double-quoted attribute to matching string is included' do - input = <<-EOS + + test 'ifeval comparing double-quoted attribute to matching string is included' do + input = <<-EOS ifeval::["{gem}" == "asciidoctor"] Asciidoctor it is! endif::[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'gem' => 'asciidoctor'} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document :attributes => {'gem' => 'asciidoctor'} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'Asciidoctor it is!', lines.join.chomp end - assert_equal 'Asciidoctor it is!', lines.join.strip - end - - test 'ifeval comparing single-quoted attribute to matching string is included' do - input = <<-EOS + + test 'ifeval comparing single-quoted attribute to matching string is included' do + input = <<-EOS ifeval::['{gem}' == 'asciidoctor'] Asciidoctor it is! endif::[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'gem' => 'asciidoctor'} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document :attributes => {'gem' => 'asciidoctor'} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'Asciidoctor it is!', lines.join.chomp end - assert_equal 'Asciidoctor it is!', lines.join.strip - end - - test 'ifeval comparing quoted attribute to non-matching string is ignored' do - input = <<-EOS + + test 'ifeval comparing quoted attribute to non-matching string is ignored' do + input = <<-EOS ifeval::['{gem}' == 'asciidoctor'] Asciidoctor it is! endif::[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'gem' => 'tilt'} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document :attributes => {'gem' => 'tilt'} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal '', lines.join.chomp end - assert_equal '', lines.join.strip - end - - test 'ifeval comparing attribute to lower version number is included' do - input = <<-EOS + + test 'ifeval comparing attribute to lower version number is included' do + input = <<-EOS ifeval::['{asciidoctor-version}' >= '0.1.0'] That version will do! endif::[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'That version will do!', lines.join.chomp end - assert_equal 'That version will do!', lines.join.strip - end - - test 'ifeval comparing attribute to self is included' do - input = <<-EOS + + test 'ifeval comparing attribute to self is included' do + input = <<-EOS ifeval::['{asciidoctor-version}' == '{asciidoctor-version}'] Of course it's the same! endif::[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'Of course it\'s the same!', lines.join.chomp end - assert_equal 'Of course it\'s the same!', lines.join.strip - end - - test 'ifeval arguments can be mirrored' do - input = <<-EOS + + test 'ifeval arguments can be transposed' do + input = <<-EOS ifeval::["0.1.0" <= "{asciidoctor-version}"] That version will do! endif::[] - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'That version will do!', lines.join.chomp end - assert_equal 'That version will do!', lines.join.strip - end - - test 'ifeval matching numeric comparison is included' do - input = <<-EOS + + test 'ifeval matching numeric comparison is included' do + input = <<-EOS ifeval::[{rings} == 1] One ring to rule them all! endif::[] - EOS - - doc = Asciidoctor::Document.new [], :attributes => {'rings' => 1} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line + EOS + + doc = empty_document :attributes => {'rings' => 1} + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal 'One ring to rule them all!', lines.join.chomp end - assert_equal 'One ring to rule them all!', lines.join.strip - end - - test 'ifdef with no target is ignored' do - input = <<-EOS + + test 'ifdef with no target is ignored' do + input = <<-EOS ifdef::[] content - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - lines = [] - while reader.has_more_lines? - lines << reader.get_line - end - assert_equal "ifdef::[]\ncontent", lines.join.strip - end - end - - context 'Text processing' do - test 'should clean CRLF from end of lines' do - input = <<-EOS -source\r -with\r -CRLF\r -endlines\r - EOS - - reader = Asciidoctor::Reader.new(input.lines.entries, Asciidoctor::Document.new, true) - reader.lines.each do |line| - assert !line.end_with?("\r\n") + EOS + + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input + lines = [] + while reader.has_more_lines? + lines << reader.read_line + end + assert_equal "ifdef::[]\ncontent", lines.join.chomp end end - - test 'sanitize attribute name' do - assert_equal 'foobar', @reader.sanitize_attribute_name("Foo Bar") - assert_equal 'foo', @reader.sanitize_attribute_name("foo") - assert_equal 'foo3-bar', @reader.sanitize_attribute_name("Foo 3^ # - Bar[") - end - - test 'should not skip front matter by default' do - input = <<-EOS ---- -layout: post -title: Document Title -author: username -tags: [ first, second ] ---- -= Document Title -Author Name - -preamble - EOS - - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - assert_equal '---', reader.peek_line.rstrip - end - - test 'should skip front matter if specified by skip-front-matter attribute' do - front_matter = %(layout: post -title: Document Title -author: username -tags: [ first, second ]) - input = <<-EOS ---- -#{front_matter} ---- -= Document Title -Author Name - -preamble - EOS - - doc = Asciidoctor::Document.new nil, :attributes => {'skip-front-matter' => ''} - reader = Asciidoctor::Reader.new(input.lines.entries, doc, true) - assert_equal '= Document Title', reader.peek_line.rstrip - assert_equal front_matter, doc.attributes['front-matter'] - end end end diff --git a/test/tables_test.rb b/test/tables_test.rb index 935f845d..8dcebe8a 100644 --- a/test/tables_test.rb +++ b/test/tables_test.rb @@ -556,7 +556,7 @@ a|include::fixtures/include-file.asciidoc[] |=== EOS - output = render_embedded_string input, :safe => Asciidoctor::SafeMode::SAFE, :base_dir => File.dirname(__FILE__) + output = render_embedded_string input, :safe => :safe, :base_dir => File.dirname(__FILE__) assert_match(/included content/, output) end diff --git a/test/test_helper.rb b/test/test_helper.rb index 8c658e9e..cba58e5d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -22,6 +22,15 @@ class Test::Unit::TestCase "#{windows? ? File.expand_path(__FILE__).split('/').first : nil}/" end + def empty_document options = {} + Asciidoctor::Document.new [], options + end + + def empty_safe_document options = {} + options[:safe] = :safe + Asciidoctor::Document.new [], options + end + def sample_doc_path(name) name = name.to_s unless name.include?('.') diff --git a/test/text_test.rb b/test/text_test.rb index d5c13dbe..22e1825e 100644 --- a/test/text_test.rb +++ b/test/text_test.rb @@ -27,22 +27,22 @@ context "Text" do end # NOTE this test ensures we have the encoding line on block templates too - test "proper encoding to handle utf8 characters in arbitrary block" do + test 'proper encoding to handle utf8 characters in arbitrary block' do input = [] input << "[verse]\n" input.concat(File.readlines(sample_doc_path(:encoding))) - doc = Asciidoctor::Document.new - reader = Asciidoctor::Reader.new(input, doc, true) + doc = empty_document + reader = Asciidoctor::PreprocessorReader.new doc, input block = Asciidoctor::Lexer.next_block(reader, doc) assert_xpath '//pre', block.render.gsub(/^\s*\n/, ''), 1 end - test "proper encoding to handle utf8 characters from included file" do + test 'proper encoding to handle utf8 characters from included file' do input = <<-EOS include::fixtures/encoding.asciidoc[tags=romé] EOS - doc = Asciidoctor::Document.new [], :safe => Asciidoctor::SafeMode::SAFE, :base_dir => File.expand_path(File.dirname(__FILE__)) - reader = Asciidoctor::Reader.new(input, doc, true) + doc = empty_safe_document :base_dir => File.expand_path(File.dirname(__FILE__)) + reader = Asciidoctor::PreprocessorReader.new doc, input block = Asciidoctor::Lexer.next_block(reader, doc) output = block.render assert_css '.paragraph', output, 1 |
