diff options
| -rwxr-xr-x | lib/asciidoctor.rb | 76 | ||||
| -rw-r--r-- | lib/asciidoctor/abstract_block.rb | 39 | ||||
| -rw-r--r-- | lib/asciidoctor/abstract_node.rb | 21 | ||||
| -rw-r--r-- | lib/asciidoctor/attribute_list.rb | 1 | ||||
| -rw-r--r-- | lib/asciidoctor/backends/html5.rb | 4 | ||||
| -rw-r--r-- | lib/asciidoctor/block.rb | 5 | ||||
| -rw-r--r-- | lib/asciidoctor/document.rb | 12 | ||||
| -rw-r--r-- | lib/asciidoctor/lexer.rb | 659 | ||||
| -rw-r--r-- | lib/asciidoctor/reader.rb | 50 | ||||
| -rw-r--r-- | test/attributes_test.rb | 46 | ||||
| -rw-r--r-- | test/blocks_test.rb | 60 | ||||
| -rw-r--r-- | test/lists_test.rb | 49 | ||||
| -rw-r--r-- | test/paragraphs_test.rb | 226 | ||||
| -rw-r--r-- | test/sections_test.rb | 17 | ||||
| -rw-r--r-- | test/substitutions_test.rb | 4 | ||||
| -rw-r--r-- | test/tables_test.rb | 15 | ||||
| -rw-r--r-- | test/test_helper.rb | 4 |
17 files changed, 888 insertions, 400 deletions
diff --git a/lib/asciidoctor.rb b/lib/asciidoctor.rb index 3efbcb73..eb90eabb 100755 --- a/lib/asciidoctor.rb +++ b/lib/asciidoctor.rb @@ -1,4 +1,5 @@ require 'strscan' +require 'set' $:.unshift(File.dirname(__FILE__)) #$:.unshift(File.join(File.dirname(__FILE__), '..', 'vendor')) @@ -112,19 +113,35 @@ module Asciidoctor 'markdown' => '.md' } + SECTION_LEVELS = { + '=' => 0, + '-' => 1, + '~' => 2, + '^' => 3, + '+' => 4 + } + + ADMONITION_STYLES = ['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION'].to_set + + # NOTE: AsciiDoc doesn't support pass style for paragraph + PARAGRAPH_STYLES = ['comment', 'example', 'literal', 'listing', 'normal', 'pass', 'quote', 'sidebar', 'source', 'verse'].to_set + + VERBATIM_STYLES = ['literal', 'listing', 'source', 'verse'].to_set + DELIMITED_BLOCKS = { - '--' => :open, - '----' => :listing, - '....' => :literal, - '====' => :example, - '****' => :sidebar, - '____' => :quote, - '++++' => :pass, - '|===' => :table, - '!===' => :table, - '////' => :comment, - '```' => :fenced_code, - '~~~' => :fenced_code + # NOTE: AsciiDoc doesn't support pass style for open block + '--' => [:open, ['comment', 'example', 'literal', 'listing', 'pass', 'quote', 'sidebar', 'source', 'verse', 'admonition'].to_set], + '----' => [:listing, ['literal', 'source'].to_set], + '....' => [:literal, ['listing', 'source'].to_set], + '====' => [:example, ['admonition'].to_set], + '****' => [:sidebar, Set.new], + '____' => [:quote, ['verse'].to_set], + '++++' => [:pass, Set.new], + '|===' => [:table, Set.new], + '!===' => [:table, Set.new], + '////' => [:comment, Set.new], + '```' => [:fenced_code, Set.new], + '~~~' => [:fenced_code, Set.new] } BREAK_LINES = { @@ -155,6 +172,27 @@ module Asciidoctor LINE_FEED_ENTITY = ' ' # or 
 + # Flags to control compliance with the behavior of AsciiDoc + COMPLIANCE = { + # AsciiDoc terminates paragraphs adjacent to + # block content (delimiter or block attribute list) + # Compliance value: true + # TODO what about literal paragraph? + :block_terminates_paragraph => true, + + # AsciiDoc does not treat paragraphs labeled with a + # verbatim style (literal, listing, source, verse) + # as verbatim; override this behavior + # Compliance value: false + :strict_verbatim_paragraphs => true, + + # AsciiDoc allows start and end delimiters around + # a block to be different lengths + # this option requires that they be the same + # Compliance value: false + :congruent_block_delimiters => true + } + # The following pattern, which appears frequently, captures the contents between square brackets, # ignoring escaped closing brackets (closing brackets prefixed with a backslash '\' character) # @@ -163,8 +201,11 @@ module Asciidoctor # Matches: # [enclosed text here] or [enclosed [text\] here] REGEXP = { + # NOTE: this is a inline admonition note + :admonition_inline => /^(#{ADMONITION_STYLES.to_a * '|'}):\s/, + # [[Foo]] - :anchor => /^\[\[([^\[\]]+)\]\]$/, + :anchor => /^\[\[([^\s\[\]]+)\]\]$/, # Foowhatevs [[Bar]] :anchor_embedded => /^(.*?)\s*\[\[([^\[\]]+)\]\]$/, @@ -203,10 +244,10 @@ module Asciidoctor # [NOTE, caption="Good to know"] # Can be defined by an attribute # [{lead}] - :blk_attr_list => /^\[(|[\w\{].*)\]$/, + :blk_attr_list => /^\[(|[[:blank:]]*[\w\{,].*)\]$/, # block attribute list or block id (bulk query) - :attr_line => /^\[(|[\w\{].*|\[[^\[\]]*\])\]$/, + :attr_line => /^\[(|[[:blank:]]*[\w\{,].*|\[[^\[\]]*\])\]$/, # attribute reference # {foo} @@ -425,7 +466,8 @@ module Asciidoctor :section_name => /^((?=.*\w+.*)[^.].*?)$/, # ====== || ------ || ~~~~~~ || ^^^^^^ || ++++++ - :section_underline => /^([=\-~^\+])+$/, + # TODO build from SECTION_LEVELS keys + :section_underline => /^(?:=|-|~|\^|\+)+$/, # * Foo (up to 5 consecutive asterisks) # - Foo @@ -462,8 +504,6 @@ module Asciidoctor :uri_encode_chars => /[^\w\-.!~*';:@=+$,()\[\]]/ } - ADMONITION_STYLES = ['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION'] - INTRINSICS = Hash.new{|h,k| STDERR.puts "Missing intrinsic: #{k.inspect}"; "{#{k}}"}.merge( { 'startsb' => '[', diff --git a/lib/asciidoctor/abstract_block.rb b/lib/asciidoctor/abstract_block.rb index 73b6086e..fef6e8e0 100644 --- a/lib/asciidoctor/abstract_block.rb +++ b/lib/asciidoctor/abstract_block.rb @@ -189,6 +189,45 @@ class AbstractBlock < AbstractNode } end + # Public: Generate a caption and assign it to this block if one + # is not already assigned. + # + # If the block has a title and a caption prefix is available + # for this block, then build a caption from this information, + # assign it a number and store it to the caption attribute on + # the block. + # + # If an explicit caption has been specified on this block, then + # do nothing. + # + # key - The prefix of the caption and counter attribute names. + # If not provided, the name of the context for this block + # is used. (default: nil). + # + # returns nothing + def assign_caption(caption = nil, key = nil) + unless title? || @caption.nil? + return nil + end + + if caption.nil? + if @document.attr?('caption') + @caption = @document.attr('caption') + else + key ||= @context.to_s + caption_key = "#{key}-caption" + if @document.attributes.has_key?(caption_key) + caption_title = @document.attributes["#{key}-caption"] + caption_num = @document.counter_increment("#{key}-number", self) + @caption = @attributes['caption'] = "#{caption_title} #{caption_num}. " + end + end + else + @caption = caption + end + nil + end + # Internal: Assign the next index (0-based) to this section # # Assign the next index of this section within the parent diff --git a/lib/asciidoctor/abstract_node.rb b/lib/asciidoctor/abstract_node.rb index f765af98..ba2dffa4 100644 --- a/lib/asciidoctor/abstract_node.rb +++ b/lib/asciidoctor/abstract_node.rb @@ -94,6 +94,27 @@ class AbstractNode end end + # Public: Assign the value to the specified key in this + # block's attributes hash. + # + # key - The attribute key (or name) + # val - The value to assign to the key + # + # returns a flag indicating whether the assignment was performed + def set_attr(key, val, overwrite = nil) + if overwrite.nil? + @attributes[key] = val + true + else + if overwrite || @attributes.has_key?(key) + @attributes[key] = val + true + else + false + end + end + end + # Public: Get the execution context of this object (via Kernel#binding). # # This method is used to set the 'self' reference as well as local variables diff --git a/lib/asciidoctor/attribute_list.rb b/lib/asciidoctor/attribute_list.rb index b5f64e45..51120675 100644 --- a/lib/asciidoctor/attribute_list.rb +++ b/lib/asciidoctor/attribute_list.rb @@ -84,6 +84,7 @@ class AttributeList def self.rekey(attributes, pos_attrs) pos_attrs.each_with_index do |key, index| + next if key.nil? pos = index + 1 unless (val = attributes[pos]).nil? attributes[key] = val diff --git a/lib/asciidoctor/backends/html5.rb b/lib/asciidoctor/backends/html5.rb index addec254..17c268ab 100644 --- a/lib/asciidoctor/backends/html5.rb +++ b/lib/asciidoctor/backends/html5.rb @@ -344,8 +344,8 @@ end class BlockParagraphTemplate < BaseTemplate def paragraph(id, role, title, content) - %(<div#{id && " id=\"#{id}\""} class=\"paragraph#{role && " #{role}"}\"> - #{title && "<div class=\"title\">#{title}</div>"} + %(<div#{id && " id=\"#{id}\""} class=\"paragraph#{role && " #{role}"}\">#{title && " + <div class=\"title\">#{title}</div>"} <p>#{content}</p> </div>) end diff --git a/lib/asciidoctor/block.rb b/lib/asciidoctor/block.rb index a43e9485..9c44d02f 100644 --- a/lib/asciidoctor/block.rb +++ b/lib/asciidoctor/block.rb @@ -26,6 +26,7 @@ class Block < AbstractBlock def initialize(parent, context, buffer = nil) super(parent, context) @buffer = buffer + @caption = nil end # Public: Get the rendered String content for this Block. If the block @@ -95,7 +96,7 @@ class Block < AbstractBlock # => ["<em>This</em> is what happens when you <meet> a stranger in the <alps>!"] def content case @context - when :preamble, :open, :example, :sidebar + when :preamble, :open @blocks.map {|b| b.render }.join # lists get iterated in the template (for now) # list items recurse into this block when their text and content methods are called @@ -105,7 +106,7 @@ class Block < AbstractBlock apply_literal_subs(@buffer) when :pass apply_passthrough_subs(@buffer) - when :quote, :verse, :admonition + when :admonition, :example, :sidebar, :quote, :verse if !@buffer.nil? apply_para_subs(@buffer) else diff --git a/lib/asciidoctor/document.rb b/lib/asciidoctor/document.rb index 43e8a719..9b224878 100644 --- a/lib/asciidoctor/document.rb +++ b/lib/asciidoctor/document.rb @@ -269,6 +269,18 @@ class Document < AbstractBlock (@attributes[name] = @counters[name]) end + # Public: Increment the specified counter and store it in the block's attributes + # + # counter_name - the String name of the counter attribute + # block - the Block on which to save the counter + # + # returns the next number in the sequence for the specified counter + def counter_increment(counter_name, block) + val = counter(counter_name) + AttributeEntry.new(counter_name, val).save_to(block.attributes) + val + end + # Internal: Get the next value in the sequence. # # Handles both integer and character sequences. diff --git a/lib/asciidoctor/lexer.rb b/lib/asciidoctor/lexer.rb index f7dde9ec..e875e4b5 100644 --- a/lib/asciidoctor/lexer.rb +++ b/lib/asciidoctor/lexer.rb @@ -23,7 +23,7 @@ module Asciidoctor # # => Asciidoctor::Block class Lexer - BlockMatchData = Struct.new(:name, :tip, :terminator) + BlockMatchData = Struct.new(:context, :masq, :tip, :terminator) # Public: Make sure the Lexer object doesn't get initialized. # @@ -137,6 +137,8 @@ class Lexer def self.next_section(reader, parent, attributes = {}) preamble = false + # FIXME if attributes[1] is a verbatim style, then don't check for section + # check if we are at the start of processing the document # NOTE we could drop a hint in the attributes to indicate # that we are at a section title (so we don't have to check) @@ -249,351 +251,316 @@ class Lexer # bail if we've reached the end of the parent block or document return nil unless reader.has_more_lines? + # check for option to find list item text only + # if skipped a line, assume a list continuation was + # used and block content is acceptable if options[:text] && skipped > 0 options.delete(:text) end - - Debug.debug { - msg = [] - msg << '/' * 64 - msg << 'next_block() - First two lines are:' - msg.concat reader.peek_lines(2) - msg << '/' * 64 - msg * "\n" - } - parse_metadata = options[:parse_metadata] || true - parse_sections = options[:parse_sections] || false + parse_metadata = options.fetch(:parse_metadata, true) + #parse_sections = options.fetch(:parse_sections, false) document = parent.document - context = parent.is_a?(Block) ? parent.context : nil + parent_context = parent.is_a?(Block) ? parent.context : nil block = nil + style = nil while reader.has_more_lines? && block.nil? + # if parsing metadata, read until there is no more to read if parse_metadata && parse_block_metadata_line(reader, document, attributes, options) reader.advance next - elsif parse_sections && context.nil? && is_next_line_section?(reader, attributes) - block, attributes = next_section(reader, parent, attributes) - break + #elsif parse_sections && parent_context.nil? && is_next_line_section?(reader, attributes) + # block, attributes = next_section(reader, parent, attributes) + # break end + # QUESTION introduce parsing context object? this_line = reader.get_line - + delimited_block = false block_context = nil terminator = nil + # QUESTION put this inside call to rekey attributes? + if attributes.has_key? 1 + style = attributes['style'] = attributes[1] + end + if delimited_blk_match = is_delimited_block?(this_line, true) - block_context = delimited_blk_match.name + delimited_block = true + block_context = delimited_blk_match.context terminator = delimited_blk_match.terminator + if !style + style = attributes['style'] = block_context.to_s + elsif style != block_context.to_s + if delimited_blk_match.masq.include? style + block_context = style.to_sym + elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style) + block_context = :admonition + else + puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for #{block_context} block: #{style}" + style = block_context.to_s + end + end end - # NOTE we're letting break lines (ruler, page_break, etc) have attributes - if !options[:text] && block_context.nil? && (match = this_line.match(REGEXP[:break_line])) - block = Block.new(parent, BREAK_LINES[match[0][0..2]]) - reader.skip_blank_lines - - elsif !options[:text] && block_context.nil? && (match = this_line.match(REGEXP[:image_blk])) - block = Block.new(parent, :image) - - block.parse_attributes(match[2], ['alt', 'width', 'height'], :sub_input => true, :sub_result => false, :into => attributes) - target = block.sub_attributes(match[1]) - if !target.to_s.empty? - attributes['target'] = target - document.register(:images, target) - attributes['alt'] ||= File.basename(target, File.extname(target)) - block.title = attributes['title'] - if block.title? && !attributes.has_key?('caption') && !block.attr?('caption') - number = document.counter('figure-number') - attributes['caption'] = "#{document.attributes['figure-caption']} #{number}. " - Document::AttributeEntry.new('figure-number', number).save_to(attributes) + if !delimited_block + + # use begin block for flow control (yes, could be done better) + begin + + # process lines verbatim + if !style.nil? && COMPLIANCE[:strict_verbatim_paragraphs] && VERBATIM_STYLES.include?(style) + block_context = style.to_sym + reader.unshift_line this_line + # advance to block parsing => + break end - else - # drop the line if target resolves to nothing - block = nil - end - reader.skip_blank_lines - elsif block_context == :open - # an open block is surrounded by '--' lines and has zero or more blocks inside - buffer = Reader.new reader.grab_lines_until(:terminator => terminator) + # process lines normally + if !options[:text] + # NOTE we're letting break lines (ruler, page_break, etc) have attributes + if (match = this_line.match(REGEXP[:break_line])) + block = Block.new(parent, BREAK_LINES[match[0][0..2]]) + reader.skip_blank_lines + break + + elsif (match = this_line.match(REGEXP[:image_blk])) + block = Block.new(parent, :image) + unless style.nil? + attributes['alt'] = style + attributes.delete('style') + style = nil + end + block.parse_attributes(match[2], ['alt', 'width', 'height'], :sub_input => true, :sub_result => false, :into => attributes) + target = block.sub_attributes(match[1]) + if !target.to_s.empty? + attributes['target'] = target + document.register(:images, target) + attributes['alt'] ||= File.basename(target, File.extname(target)) + block.title = attributes.delete('title') if attributes.has_key?('title') + block.assign_caption attributes.delete('caption'), 'figure' + break + else + # drop the line if target resolves to nothing + block = nil + reader.skip_blank_lines + return nil + end + end + end - # Strip lines off end of block - not implemented yet - # while buffer.has_more_lines? && buffer.last.strip.empty? - # buffer.pop - # end + # haven't found anything yet, continue + if (match = this_line.match(REGEXP[:colist])) + block = Block.new(parent, :colist) + attributes['style'] = 'arabic' + items = [] + block.buffer = items + reader.unshift_line this_line + expected_index = 1 + begin + # might want to move this check to a validate method + if match[1].to_i != expected_index + puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}" + end + list_item = next_list_item(reader, block, match) + expected_index += 1 + if !list_item.nil? + items << list_item + coids = document.callouts.callout_ids(items.size) + if !coids.empty? + list_item.attributes['coids'] = coids + else + puts "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{items.size}" + end + end + end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist]) - block = Block.new(parent, block_context) - while buffer.has_more_lines? - new_block = next_block(buffer, block) - block.blocks << new_block unless new_block.nil? - end + document.callouts.next_list + break - # needs to come before list detection - elsif block_context == :sidebar - # sidebar is surrounded by '****' (4 or more '*' chars) lines - # FIXME violates DRY because it's a duplication of quote parsing - block = Block.new(parent, block_context) - buffer = Reader.new reader.grab_lines_until(:terminator => terminator) + elsif (match = this_line.match(REGEXP[:ulist])) + reader.unshift_line this_line + block = next_outline_list(reader, :ulist, parent) + break - while buffer.has_more_lines? - new_block = next_block(buffer, block) - block.blocks << new_block unless new_block.nil? - end + elsif (match = this_line.match(REGEXP[:olist])) + reader.unshift_line this_line + block = next_outline_list(reader, :olist, parent) + # QUESTION move this logic to next_outline_list? + if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style') + marker = block.buffer.first.marker + if marker.start_with? '.' + # first one makes more sense, but second on is AsciiDoc-compliant + #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s + attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s + else + style = ORDERED_LIST_STYLES.detect{|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) } + attributes['style'] = (style || ORDERED_LIST_STYLES.first).to_s + end + end + break - elsif block_context.nil? && (match = this_line.match(REGEXP[:colist])) - block = Block.new(parent, :colist) - attributes['style'] = 'arabic' - items = [] - block.buffer = items - reader.unshift_line this_line - expected_index = 1 - begin - # might want to move this check to a validate method - if match[1].to_i != expected_index - puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}" - end - list_item = next_list_item(reader, block, match) - expected_index += 1 - if !list_item.nil? - items << list_item - coids = document.callouts.callout_ids(items.size) - if !coids.empty? - list_item.attributes['coids'] = coids + elsif (match = this_line.match(REGEXP[:dlist])) + reader.unshift_line this_line + block = next_labeled_list(reader, match, parent) + break + + elsif (style == 'float' || style == 'discrete') && is_section_title?(this_line, reader.peek_line) + reader.unshift_line this_line + float_id, float_title, float_level, _ = parse_section_title(reader, document) + float_id ||= attributes['id'] if attributes.has_key?('id') + block = Block.new(parent, :floating_title) + if float_id.nil? || float_id.empty? + # FIXME remove hack of creating throwaway Section to get at the generate_id method + tmp_sect = Section.new(parent) + tmp_sect.title = float_title + block.id = tmp_sect.generate_id else - puts "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{items.size}" + block.id = float_id + document.register(:ids, [float_id, float_title]) end - end - end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist]) - - document.callouts.next_list - - elsif block_context.nil? && (match = this_line.match(REGEXP[:ulist])) - AttributeList.rekey(attributes, ['style']) - reader.unshift_line this_line - block = next_outline_list(reader, :ulist, parent) - - elsif block_context.nil? && (match = this_line.match(REGEXP[:olist])) - AttributeList.rekey(attributes, ['style']) - reader.unshift_line this_line - block = next_outline_list(reader, :olist, parent) - # QUESTION move this logic to next_outline_list? - if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style') - marker = block.buffer.first.marker - if marker.start_with? '.' - # first one makes more sense, but second on is AsciiDoc-compliant - #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s - attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s - else - style = ORDERED_LIST_STYLES.detect{|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) } - attributes['style'] = (style || ORDERED_LIST_STYLES.first).to_s - end - end + block.level = float_level + block.title = float_title + break - elsif block_context.nil? && (match = this_line.match(REGEXP[:dlist])) - reader.unshift_line this_line - block = next_labeled_list(reader, match, parent) - AttributeList.rekey(attributes, ['style']) - - elsif block_context == :table - # table is surrounded by lines starting with a | followed by 3 or more '=' chars - AttributeList.rekey(attributes, ['style']) - table_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true) - block = next_table(table_reader, parent, attributes) - block.title = attributes['title'] - if block.title? && !attributes.has_key?('caption') && !block.attr?('caption') - number = document.counter('table-number') - attributes['caption'] = "#{document.attributes['table-caption']} #{number}. " - Document::AttributeEntry.new('table-number', number).save_to(attributes) - end - - # FIXME violates DRY because it's a duplication of other block parsing - elsif block_context == :example - # example is surrounded by lines with 4 or more '=' chars - AttributeList.rekey(attributes, ['style']) - if admonition_style = ADMONITION_STYLES.detect {|s| attributes['style'] == s} - block = Block.new(parent, :admonition) - attributes['name'] = admonition_name = admonition_style.downcase - attributes['caption'] ||= document.attributes["#{admonition_name}-caption"] - else - block = Block.new(parent, block_context) - block.title = attributes['title'] - if block.title? && !attributes.has_key?('caption') && !block.attr?('caption') - number = document.counter('example-number') - attributes['caption'] = "#{document.attributes['example-caption']} #{number}. " - Document::AttributeEntry.new('example-number', number).save_to(attributes) + elsif !style.nil? && style != 'normal' + if PARAGRAPH_STYLES.include?(style) + block_context = style.to_sym + reader.unshift_line this_line + # advance to block parsing => + break + elsif ADMONITION_STYLES.include?(style) + block_context = :admonition + reader.unshift_line this_line + # advance to block parsing => + break + else + puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for paragraph: #{style}" + style = nil + end end - end - buffer = Reader.new reader.grab_lines_until(:terminator => terminator) - while buffer.has_more_lines? - new_block = next_block(buffer, block) - block.blocks << new_block unless new_block.nil? - end + # 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 + reader.unshift_line this_line + buffer = reader.grab_lines_until( + :break_on_blank_lines => true, + :break_on_list_continuation => true, + :preserve_last_line => true) {|line| + # labeled list terms can be indented, but a preceding blank line indicates + # we are in a list continuation and therefore literals should be strictly literal + (skipped == 0 && parent_context == :dlist && line.match(REGEXP[:dlist])) || + (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line]))) + } - # FIXME violates DRY w/ non-delimited block listing - elsif block_context == :listing || block_context == :fenced_code - if block_context == :fenced_code - attributes['style'] = 'source' - lang = this_line[3..-1].strip - attributes['language'] = lang unless lang.empty? - terminator = terminator[0..2] if terminator.length > 3 - else - AttributeList.rekey(attributes, ['style', 'language', 'linenums']) - end - buffer = reader.grab_lines_until(:terminator => terminator) - buffer.last.chomp! unless buffer.empty? - block = Block.new(parent, :listing, buffer) - block.title = attributes['title'] - if document.attributes.has_key?('listing-caption') && - block.title? && !attributes.has_key?('caption') && !block.attr?('caption') - number = document.counter('listing-number') - attributes['caption'] = "#{document.attributes['listing-caption']} #{number}. " - Document::AttributeEntry.new('listing-number', number).save_to(attributes) - end + # trim off the indentation equivalent to the size of the least indented line + if !buffer.empty? + offset = buffer.map {|line| line.match(REGEXP[:leading_blanks])[1].length }.min + if offset > 0 + buffer = buffer.map {|l| l.sub(/^\s{1,#{offset}}/, '') } + end + end - elsif block_context == :quote - # multi-line verse or quote is surrounded by a block delimiter - AttributeList.rekey(attributes, ['style', 'attribution', 'citetitle']) - quote_context = (attributes['style'] == 'verse' ? :verse : :quote) - block_reader = Reader.new reader.grab_lines_until(:terminator => terminator) + block = Block.new(parent, :literal, buffer) + # a literal gets special meaning inside of a definition list + if LIST_CONTEXTS.include?(parent_context) + attributes['options'] ||= [] + # TODO this feels hacky, better way to distinguish from explicit literal block? + attributes['options'] << 'listparagraph' + end + break - # only quote can have other section elements (as section block) - section_body = (quote_context == :quote) + # a paragraph is contiguous nonblank/noncontinuation lines + else + reader.unshift_line this_line + buffer = reader.grab_lines_until( + :break_on_blank_lines => true, + :break_on_list_continuation => true, + :preserve_last_line => true, + :skip_line_comments => true) {|line| + # a preceding blank line (skipped > 0) indicates we are in a list continuation + # and therefore we should not break at a definition list term + # however, this won't stop breaking on item of same level since we've already parsed them out + (skipped == 0 && parent_context == :dlist && line.match(REGEXP[:dlist])) || + (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line]))) + } - if section_body - block = Block.new(parent, quote_context) - while block_reader.has_more_lines? - new_block = next_block(block_reader, block) - block.blocks << new_block unless new_block.nil? - end - else - block_reader.chomp_last! - block = Block.new(parent, quote_context, block_reader.lines) - end + # NOTE we need this logic because we've asked the reader to skip + # line comments, which may leave us w/ an empty buffer if those + # were the only lines found + if buffer.empty? + reader.get_line + break + end - elsif block_context == :literal || block_context == :pass - # literal is surrounded by '....' (4 or more '.' chars) lines - # pass is surrounded by '++++' (4 or more '+' chars) lines - buffer = reader.grab_lines_until(:terminator => terminator) - buffer.last.chomp! unless buffer.empty? - # a literal can masquerade as a listing - if attributes[1] == 'listing' - block_context = :listing - end - block = Block.new(parent, block_context, buffer) - - elsif this_line.match(REGEXP[:lit_par]) - # literal paragraph is contiguous lines starting with - # one or more space or tab characters - - # So we need to actually include this one in the grab_lines group - reader.unshift_line this_line - buffer = reader.grab_lines_until(:preserve_last_line => true, :break_on_blank_lines => true) {|line| - # labeled list terms can be indented, but a preceding blank indicates - # we are in a list continuation and therefore literals should be strictly literal - (context == :dlist && skipped == 0 && line.match(REGEXP[:dlist])) || - is_delimited_block?(line) - } + catalog_inline_anchors(buffer.join, document) - # trim off the indentation equivalent to the size of the least indented line - if !buffer.empty? - offset = buffer.map {|line| line.match(REGEXP[:leading_blanks])[1].length }.min - if offset > 0 - buffer = buffer.map {|l| l.sub(/^\s{1,#{offset}}/, '') } + if !options[:text] && (admonition_match = buffer.first.match(REGEXP[:admonition_inline])) + buffer[0] = admonition_match.post_match.lstrip + block = Block.new(parent, :admonition, buffer) + attributes['style'] = admonition_match[1] + attributes['name'] = admonition_name = admonition_match[1].downcase + attributes['caption'] ||= document.attributes["#{admonition_name}-caption"] + else + # QUESTION is this necessary? + #if style == 'normal' && [' ', "\t"].include?(buffer.first[0..0]) + # # QUESTION should we only trim leading blanks? + # buffer.map! &:lstrip + #end + + block = Block.new(parent, :paragraph, buffer) + end + break end - buffer.last.chomp! - end + end while false + end - block = Block.new(parent, :literal, buffer) - # a literal gets special meaning inside of a definition list - if LIST_CONTEXTS.include?(context) - attributes['options'] ||= [] - # TODO this feels hacky, better way to distinguish from explicit literal block? - attributes['options'] << 'listparagraph' - end + # either delimited block or styled paragraph + if block.nil? && !block_context.nil? + case block_context + when :admonition + attributes['name'] = admonition_name = style.downcase + attributes['caption'] ||= document.attributes["#{admonition_name}-caption"] + block = build_block(block_context, :complex, terminator, parent, reader, attributes) - ## these switches based on style need to come immediately before the else ## + when :comment + reader.grab_lines_until(:break_on_blank_lines => true, :chomp_last_line => false) + # QUESTION should we skip blank lines here? + break - elsif attributes[1] == 'source' || attributes[1] == 'listing' - if attributes[1] == 'source' - AttributeList.rekey(attributes, ['style', 'language', 'linenums']) - end - reader.unshift_line this_line - buffer = reader.grab_lines_until(:break_on_blank_lines => true) - buffer.last.chomp! unless buffer.empty? - block = Block.new(parent, :listing, buffer) - - elsif attributes[1] == 'literal' - reader.unshift_line this_line - buffer = reader.grab_lines_until(:break_on_blank_lines => true) - buffer.last.chomp! unless buffer.empty? - block = Block.new(parent, :literal, buffer) - - elsif admonition_style = ADMONITION_STYLES.detect{|s| attributes[1] == s} - # an admonition preceded by [<TYPE>] and lasts until a blank line - reader.unshift_line this_line - buffer = reader.grab_lines_until(:break_on_blank_lines => true) - buffer.last.chomp! unless buffer.empty? - block = Block.new(parent, :admonition, buffer) - attributes['style'] = admonition_style - attributes['name'] = admonition_name = admonition_style.downcase - attributes['caption'] ||= document.attributes["#{admonition_name}-caption"] - - elsif quote_context = [:quote, :verse].detect{|s| attributes[1] == s.to_s} - # single-paragraph verse or quote is preceded by [verse] or [quote], respectively, and lasts until a blank line - AttributeList.rekey(attributes, ['style', 'attribution', 'citetitle']) - reader.unshift_line this_line - buffer = reader.grab_lines_until(:break_on_blank_lines => true) - buffer.last.chomp! unless buffer.empty? - block = Block.new(parent, quote_context, buffer) - - # a floating (i.e., discrete) title - elsif ['float', 'discrete'].include?(attributes[1]) && is_section_title?(this_line, reader.peek_line) - attributes['style'] = attributes[1] - reader.unshift_line this_line - float_id, float_title, float_level, _ = parse_section_title(reader, document) - block = Block.new(parent, :floating_title) - if float_id.nil? || float_id.empty? - # FIXME remove hack of creating throwaway Section to get at the generate_id method - tmp_sect = Section.new(parent) - tmp_sect.title = float_title - block.id = tmp_sect.generate_id - else - block.id = float_id - @document.register(:ids, [float_id, float_title]) - end - block.level = float_level - block.title = float_title + when :example + block = build_block(block_context, :complex, terminator, parent, reader, attributes, true) + + when :listing, :fenced_code, :source + if block_context == :fenced_code + style = attributes['style'] = 'source' + lang = this_line[3..-1].strip + attributes['language'] = lang unless lang.empty? + terminator = terminator[0..2] if terminator.length > 3 + elsif block_context == :source + AttributeList.rekey(attributes, [nil, 'language', 'linenums']) + end + block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, true) - # a paragraph - contiguous nonblank/noncontinuation lines - else - reader.unshift_line this_line - buffer = reader.grab_lines_until(:break_on_blank_lines => true, :preserve_last_line => true, :skip_line_comments => true) {|line| - is_delimited_block?(line) || line.match(REGEXP[:attr_line]) || - # next list item can be directly adjacent to paragraph of previous list item - context == :dlist && line.match(REGEXP[:dlist]) - # not sure if there are any cases when we need this check for other list types - #LIST_CONTEXTS.include?(context) && line.match(REGEXP[context]) - } + when :literal + block = build_block(block_context, :verbatim, terminator, parent, reader, attributes) + + when :pass + block = build_block(block_context, :simple, terminator, parent, reader, attributes) - # NOTE we need this logic because the reader is processing line - # comments and that might leave us w/ an empty buffer - if buffer.empty? - reader.get_line - break - end + when :open, :sidebar + block = build_block(block_context, :complex, terminator, parent, reader, attributes) - catalog_inline_anchors(buffer.join, document) + when :table + block_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true) + block = next_table(block_reader, parent, attributes) + + when :quote, :verse + AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle']) + block = build_block(block_context, (block_context == :verse ? :verbatim : :complex), terminator, parent, reader, attributes) - if !options[:text] && (admonition = buffer.first.match(Regexp.new('^(' + ADMONITION_STYLES.join('|') + '):\s+'))) - buffer[0] = admonition.post_match - block = Block.new(parent, :admonition, buffer) - attributes['style'] = admonition[1] - attributes['name'] = admonition_name = admonition[1].downcase - attributes['caption'] ||= document.attributes["#{admonition_name}-caption"] - else - buffer.last.chomp! - block = Block.new(parent, :paragraph, buffer) end end end @@ -602,7 +569,7 @@ class Lexer # blocks or trailing attribute lists could leave us without a block, # so handle accordingly if !block.nil? - block.id = attributes['id'] if attributes.has_key?('id') + block.id ||= attributes['id'] if attributes.has_key?('id') block.title = attributes['title'] unless block.title? block.caption ||= attributes['caption'] unless block.is_a?(Section) # AsciiDoc always use [id] as the reftext in HTML output, @@ -645,9 +612,21 @@ class Lexer if DELIMITED_BLOCKS.has_key? tip # if tip is the full line if tl == line_len - 1 - return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, tip) : true + #return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, tip) : true + if return_match_data + context, masq = *DELIMITED_BLOCKS[tip] + BlockMatchData.new(context, masq, tip, tip) + else + true + end elsif match = line.match(REGEXP[:any_blk]) - return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, match[0]) : true + #return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, match[0]) : true + if return_match_data + context, masq = *DELIMITED_BLOCKS[tip] + BlockMatchData.new(context, masq, tip, match[0]) + else + true + end else nil end @@ -659,6 +638,44 @@ class Lexer end end + # whether a block supports complex content should be a config setting + def self.build_block(block_context, content_type, terminator, parent, reader, attributes, supports_caption = false) + if terminator.nil? + if content_type == :verbatim + buffer = reader.grab_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true) + else + buffer = reader.grab_lines_until( + :break_on_blank_lines => true, + :break_on_list_continuation => true, + :preserve_last_line => true, + :skip_line_comments => true) {|line| + COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])) + } + # QUESTION check for empty buffer? + end + elsif content_type != :complex + buffer = reader.grab_lines_until(:terminator => terminator, :chomp_last_line => true) + else + buffer = nil + block_reader = Reader.new reader.grab_lines_until(:terminator => terminator) + end + + block = Block.new(parent, block_context, buffer) + # should supports_caption be necessary? + if supports_caption + block.title = attributes.delete('title') if attributes.has_key?('title') + block.assign_caption attributes.delete('caption') + end + + if buffer.nil? + while block_reader.has_more_lines? + parsed_block = next_block(block_reader, block) + block.blocks << parsed_block unless parsed_block.nil? + end + end + block + end + # Internal: Parse and construct an outline list Block from the current position of the Reader # # reader - The Reader from which to retrieve the outline list @@ -939,12 +956,12 @@ class Lexer if this_line.match(REGEXP[:lit_par]) reader.unshift_line this_line buffer.concat reader.grab_lines_until( - :preserve_last_line => true, - :break_on_blank_lines => true, - :break_on_list_continuation => true) {|line| - # we may be in an indented list disguised as a literal paragraph - # so we need to make sure we don't slurp up a legitimate sibling - list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait) + :preserve_last_line => true, + :break_on_blank_lines => true, + :break_on_list_continuation => true) {|line| + # we may be in an indented list disguised as a literal paragraph + # so we need to make sure we don't slurp up a legitimate sibling + list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait) } continuation = :inactive # let block metadata play out until we find the block @@ -982,13 +999,13 @@ class Lexer if this_line.match(REGEXP[:lit_par]) reader.unshift_line this_line buffer.concat reader.grab_lines_until( - :preserve_last_line => true, - :break_on_blank_lines => true, - :break_on_list_continuation => true) {|line| - # we may be in an indented list disguised as a literal paragraph - # so we need to make sure we don't slurp up a legitimate sibling - list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait) - } + :preserve_last_line => true, + :break_on_blank_lines => true, + :break_on_list_continuation => true) {|line| + # we may be in an indented list disguised as a literal paragraph + # so we need to make sure we don't slurp up a legitimate sibling + list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait) + } # TODO any way to combine this with the check after skipping blank lines? elsif is_sibling_list_item?(this_line, list_type, sibling_trait) break @@ -1068,6 +1085,7 @@ class Lexer section.sectname = attributes[1] section.special = true document = parent.document + # FIXME refactor to use assign_caption (also check requirements) if section.sectname == 'appendix' && !attributes.has_key?('caption') && !document.attributes.has_key?('caption') @@ -1089,14 +1107,7 @@ class Lexer # # line - the String line from under the section title. def self.section_level(line) - char = line.chomp.chars.to_a.uniq - case char - when ['=']; 0 - when ['-']; 1 - when ['~']; 2 - when ['^']; 3 - when ['+']; 4 - end + SECTION_LEVELS[line[0..0]] end #-- @@ -1619,6 +1630,8 @@ class Lexer # returns an instance of Asciidoctor::Table parsed from the provided reader def self.next_table(table_reader, parent, attributes) table = Table.new(parent, attributes) + table.title = attributes.delete('title') if attributes.has_key?('title') + table.assign_caption attributes.delete('caption') if attributes.has_key? 'cols' table.create_columns(parse_col_specs(attributes['cols'])) diff --git a/lib/asciidoctor/reader.rb b/lib/asciidoctor/reader.rb index 31e353bf..c0a7a5ee 100644 --- a/lib/asciidoctor/reader.rb +++ b/lib/asciidoctor/reader.rb @@ -592,32 +592,54 @@ class Reader def grab_lines_until(options = {}, &block) buffer = [] - finis = false advance if options[:skip_first_line] - # save options to locals for minor optimization - terminator = options[:terminator] - terminator.chomp! if terminator - break_on_blank_lines = options[:break_on_blank_lines] - break_on_list_continuation = options[:break_on_list_continuation] + # 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 + 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 + end skip_line_comments = options[:skip_line_comments] preprocess = options.fetch(:preprocess, true) + buffer_empty = true while !(this_line = get_line(preprocess)).nil? - Debug.debug { "Reader processing line: '#{this_line}'" } - finis = true if terminator && this_line.chomp == terminator - finis = true if !finis && break_on_blank_lines && this_line.strip.empty? - finis = true if !finis && break_on_list_continuation && this_line.chomp == LIST_CONTINUATION - finis = true if !finis && block && yield(this_line) - if finis - buffer << this_line if options[:grab_last_line] - unshift_line(this_line) if options[:preserve_last_line] + # 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 + + 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 end unless skip_line_comments && this_line.match(REGEXP[:comment]) buffer << this_line + buffer_empty = false end end + # should we dup the line before chopping? + buffer.last.chomp! if chomp_last_line && !buffer_empty buffer end diff --git a/test/attributes_test.rb b/test/attributes_test.rb index c2152d44..6dff6b6f 100644 --- a/test/attributes_test.rb +++ b/test/attributes_test.rb @@ -403,10 +403,10 @@ of the attribute named foo in your document. end - context "Block attributes" do - test "Position attributes assigned to block" do + context 'Block attributes' do + test 'Positional attributes assigned to block' do input = <<-EOS -[quote, Name, Source] +[quote, author, source] ____ A famous quote. ____ @@ -415,13 +415,13 @@ ____ qb = doc.blocks.first assert_equal 'quote', qb.attributes['style'] assert_equal 'quote', qb.attr(:style) - assert_equal 'Name', qb.attributes['attribution'] - assert_equal 'Source', qb.attributes['citetitle'] + assert_equal 'author', qb.attributes['attribution'] + assert_equal 'source', qb.attributes['citetitle'] end - test "Normal substitutions are performed on single-quoted attributes" do + test 'Normal substitutions are performed on single-quoted attributes' do input = <<-EOS -[quote, Name, 'http://wikipedia.org[Source]'] +[quote, author, 'http://wikipedia.org[source]'] ____ A famous quote. ____ @@ -430,8 +430,36 @@ ____ qb = doc.blocks.first assert_equal 'quote', qb.attributes['style'] assert_equal 'quote', qb.attr(:style) - assert_equal 'Name', qb.attributes['attribution'] - assert_equal '<a href="http://wikipedia.org">Source</a>', qb.attributes['citetitle'] + assert_equal 'author', qb.attributes['attribution'] + assert_equal '<a href="http://wikipedia.org">source</a>', qb.attributes['citetitle'] + end + + test 'attribute list may begin with space' do + input = <<-EOS +[ quote] +____ +A famous quote. +____ + EOS + + doc = document_from_string input + qb = doc.blocks.first + assert_equal 'quote', qb.attributes['style'] + end + + test 'attribute list may begin with comma' do + input = <<-EOS +[, author, source] +____ +A famous quote. +____ + EOS + + doc = document_from_string input + qb = doc.blocks.first + assert_equal 'quote', qb.attributes['style'] + assert_equal 'author', qb.attributes['attribution'] + assert_equal 'source', qb.attributes['citetitle'] end test "Attribute substitutions are performed on attribute list before parsing attributes" do diff --git a/test/blocks_test.rb b/test/blocks_test.rb index 54c37c50..340bf515 100644 --- a/test/blocks_test.rb +++ b/test/blocks_test.rb @@ -558,8 +558,8 @@ Block content end end - context "Images" do - test "can render block image with alt text" do + context 'Images' do + test 'can render block image with alt text define in macro' do input = <<-EOS image::images/tiger.png[Tiger] EOS @@ -568,6 +568,26 @@ image::images/tiger.png[Tiger] assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 end + test 'can render block image with alt text defined in above macro' do + input = <<-EOS +[Tiger] +image::images/tiger.png[] + EOS + + output = render_string input + assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 + end + + test 'alt text in macro overrides alt text above macro' do + input = <<-EOS +[Alt Text] +image::images/tiger.png[Tiger] + EOS + + output = render_string input + assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 + end + test "can render block image with auto-generated alt text" do input = <<-EOS image::images/tiger.png[] @@ -608,6 +628,42 @@ image::images/tiger.png[Tiger] assert_equal 1, doc.attributes['figure-number'] end + test 'can render block image with explicit caption' do + input = <<-EOS +[caption="Voila! "] +.The AsciiDoc Tiger +image::images/tiger.png[Tiger] + EOS + + doc = document_from_string input + output = doc.render + assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 + assert_xpath '//*[@class="imageblock"]/*[@class="title"][text() = "Voila! The AsciiDoc Tiger"]', output, 1 + assert !doc.attributes.has_key?('figure-number') + end + + test 'drops line if image target is missing attribute reference' do + input = <<-EOS +image::{bogus}[] + EOS + + output = render_embedded_string input + assert output.strip.empty? + end + + test 'dropped image does not break processing of following section' do + input = <<-EOS +image::{bogus}[] + +== Section Title + EOS + + output = render_embedded_string input + assert_css 'img', output, 0 + assert_css 'h2', output, 1 + assert !output.include?('== Section Title') + end + test 'should pass through image that is a uri reference' do input = <<-EOS :imagesdir: images diff --git a/test/lists_test.rb b/test/lists_test.rb index 11e7e855..10ead704 100644 --- a/test/lists_test.rb +++ b/test/lists_test.rb @@ -2666,6 +2666,55 @@ para assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]/p[text()="para"]', output, 1 end + + test 'attached paragraph does not break on adjacent nested labeled list term' do + input = <<-EOS +term1:: def ++ +more definition +not a term::: def + EOS + + output = render_embedded_string input + assert_css '.dlist > dl > dt', output, 1 + assert_css '.dlist > dl > dd', output, 1 + assert_css '.dlist > dl > dd > .paragraph', output, 1 + assert output.include?('not a term::: def') + end + + # FIXME pending +=begin + test 'attached paragraph does not break on adjacent sibling labeled list term' do + input = <<-EOS +term1:: def ++ +more definition +not a term:: def + EOS + + output = render_embedded_string input + assert_css '.dlist > dl > dt', output, 1 + assert_css '.dlist > dl > dd', output, 1 + assert_css '.dlist > dl > dd > .paragraph', output, 1 + assert output.include?('not a term:: def') + end +=end + + test 'attached styled paragraph does not break on adjacent nested labeled list term' do + input = <<-EOS +term1:: def ++ +[quote] +more definition +not a term::: def + EOS + + output = render_embedded_string input + assert_css '.dlist > dl > dt', output, 1 + assert_css '.dlist > dl > dd', output, 1 + assert_css '.dlist > dl > dd > .quoteblock', output, 1 + assert output.include?('not a term::: def') + end test 'appends line as paragraph if attached by continuation following blank line and line comment when term has no inline definition' do input = <<-EOS diff --git a/test/paragraphs_test.rb b/test/paragraphs_test.rb index 0360f165..796bdfe4 100644 --- a/test/paragraphs_test.rb +++ b/test/paragraphs_test.rb @@ -1,21 +1,53 @@ require 'test_helper' -context "Paragraphs" do +context 'Paragraphs' do context 'Normal' do - test "rendered correctly" do - assert_xpath "//p", render_string("Plain text for the win.\n\nYes, plainly."), 2 + test 'should treat plain text separated by blank lines as paragraphs' do + input = <<-EOS +Plain text for the win! + +Yep. Text. Plain and simple. + EOS + output = render_embedded_string input + assert_css 'p', output, 2 + assert_xpath '(//p)[1][text() = "Plain text for the win!"]', output, 1 + assert_xpath '(//p)[2][text() = "Yep. Text. Plain and simple."]', output, 1 end - test "with title" do - rendered = render_string(".Titled\nParagraph.\n\nWinning") + test 'should associate block title with paragraph' do + input = <<-EOS +.Titled +Paragraph. + +Winning. + EOS + output = render_embedded_string input - assert_xpath "//div[@class='title']", rendered - assert_xpath "//p", rendered, 2 + assert_css 'p', output, 2 + assert_xpath '(//p)[1]/preceding-sibling::*[@class = "title"]', output, 1 + assert_xpath '(//p)[1]/preceding-sibling::*[@class = "title"][text() = "Titled"]', output, 1 + assert_xpath '(//p)[2]/preceding-sibling::*[@class = "title"]', output, 0 end - test "no duplicate block before next section" do - rendered = render_string("Title\n=====\n\nPreamble.\n\n== First Section\n\nParagraph 1\n\nParagraph 2\n\n\n== Second Section\n\nLast words") - assert_xpath '//p[text()="Paragraph 2"]', rendered, 1 + test 'no duplicate block before next section' do + input = <<-EOS += Title + +Preamble + +== First Section + +Paragraph 1 + +Paragraph 2 + +== Second Section + +Last words + EOS + + output = render_string input + assert_xpath '//p[text() = "Paragraph 2"]', output, 1 end test 'does not treat wrapped line as a list item' do @@ -40,6 +72,62 @@ paragraph assert_xpath %(//p[text()="paragraph\n.wrapped line"]), output, 1 end + test 'interprets normal paragraph style as normal paragraph' do + input = <<-EOS +[normal] +Normal paragraph. +Nothing special. + EOS + + output = render_embedded_string input + assert_css 'p', output, 1 + end + + test 'normal paragraph terminates at block attribute list' do + input = <<-EOS +normal text +[literal] +literal text + EOS + output = render_embedded_string input + assert_css '.paragraph:root', output, 1 + assert_css '.literalblock:root', output, 1 + end + + test 'normal paragraph terminates at block delimiter' do + input = <<-EOS +normal text +-- +text in open block +-- + EOS + output = render_embedded_string input + assert_css '.paragraph:root', output, 1 + assert_css '.openblock:root', output, 1 + end + + test 'normal paragraph terminates at list continuation' do + input = <<-EOS +normal text ++ + EOS + output = render_embedded_string input + assert_css '.paragraph:root', output, 2 + assert_xpath %((/*[@class="paragraph"])[1]/p[text() = "normal text"]), output, 1 + assert_xpath %((/*[@class="paragraph"])[2]/p[text() = "+"]), output, 1 + end + + test 'normal style turns literal paragraph into normal paragraph' do + input = <<-EOS +[normal] + normal paragraph, + despite the leading indent + EOS + + output = render_embedded_string input + assert_css '.paragraph:root > p', output, 1 + end + test 'expands index term macros in DocBook backend' do input = <<-EOS Here is an index entry for ((tigers)). @@ -93,12 +181,20 @@ Note that multi-entry terms generate separate index entries. end end - context "code" do - test "single-line literal paragraphs" do - assert_xpath "//pre", render_string(" LITERALS\n\n ARE LITERALLY\n\n AWESOMMMME.") + context 'Literal' do + test 'single-line literal paragraphs' do + input = <<-EOS + LITERALS + + ARE LITERALLY + + AWESOME! + EOS + output = render_embedded_string input + assert_xpath '//pre', output, 3 end - test "multi-line literal paragraph" do + test 'multi-line literal paragraph' do input = <<-EOS Install instructions: @@ -107,29 +203,95 @@ Install instructions: You're good to go! EOS - output = render_string(input) - assert_xpath "//pre", output, 1 - assert_match(/^gem install asciidoctor/, output, "Indentation should be trimmed from literal block") + output = render_embedded_string input + assert_xpath '//pre', output, 1 + # indentation should be trimmed from literal block + assert_xpath %(//pre[text() = "yum install ruby rubygems\ngem install asciidoctor"]), output, 1 + end + + test 'literal paragraph' do + input = <<-EOS +[literal] +this text is literally literal + EOS + output = render_embedded_string input + assert_xpath %(/*[@class="literalblock"]//pre[text()="this text is literally literal"]), output, 1 + end + + test 'should read content below literal style verbatim' do + input = <<-EOS +[literal] +image::not-an-image-block[] + EOS + output = render_embedded_string input + assert_xpath %(/*[@class="literalblock"]//pre[text()="image::not-an-image-block[]"]), output, 1 + assert_css 'img', output, 0 + end + + test 'listing paragraph' do + input = <<-EOS +[listing] +this text is a listing + EOS + output = render_embedded_string input + assert_xpath %(/*[@class="listingblock"]//pre[text()="this text is a listing"]), output, 1 + end + + test 'source paragraph' do + input = <<-EOS +[source] +use the source, luke! + EOS + output = render_embedded_string input + assert_xpath %(/*[@class="listingblock"]//pre[@class="highlight"]/code[text()="use the source, luke!"]), output, 1 end - test "literal paragraph" do - assert_xpath "//*[@class='literalblock']//pre[text()='blah blah blah']", render_string("[literal]\nblah blah blah") + test 'source code paragraph with language' do + input = <<-EOS +[source, perl] +die 'zomg perl sucks'; + EOS + output = render_embedded_string input + assert_xpath %(/*[@class="listingblock"]//pre[@class="highlight"]/code[@class="perl"][text()="die 'zomg perl sucks';"]), output, 1 end - test "listing paragraph" do - assert_xpath "//*[@class='listingblock']//pre[text()='blah blah blah']", render_string("[listing]\nblah blah blah") + test 'literal paragraph terminates at block attribute list' do + input = <<-EOS + literal text +[normal] +normal text + EOS + output = render_embedded_string input + assert_xpath %(/*[@class="literalblock"]), output, 1 + assert_xpath %(/*[@class="paragraph"]), output, 1 end - test "source code paragraph" do - assert_xpath "//pre[@class='highlight']/code", render_string("[source]\nblah blah blah") + test 'literal paragraph terminates at block delimiter' do + input = <<-EOS + literal text +-- +normal text +-- + EOS + output = render_embedded_string input + assert_xpath %(/*[@class="literalblock"]), output, 1 + assert_xpath %(/*[@class="openblock"]), output, 1 end - test "source code paragraph with language" do - assert_xpath "//pre[@class='highlight']/code[@class='perl']", render_string("[source, perl]\ndie 'zomg perl sucks';") + test 'literal paragraph terminates at list continuation' do + input = <<-EOS + literal text ++ + EOS + output = render_embedded_string input + assert_xpath %(/*[@class="literalblock"]), output, 1 + assert_xpath %(/*[@class="literalblock"]//pre[text() = "literal text"]), output, 1 + assert_xpath %(/*[@class="paragraph"]), output, 1 + assert_xpath %(/*[@class="paragraph"]/p[text() = "+"]), output, 1 end end - context "quote" do + context 'Quote' do test "quote block" do output = render_string("____\nFamous quote.\n____") assert_xpath '//*[@class = "quoteblock"]', output, 1 @@ -157,6 +319,18 @@ You're good to go! assert_xpath '//*[@class = "quoteblock"]//*[contains(text(), "Famous quote.")]', output, 1 end + test 'quote paragraph terminates at list continuation' do + input = <<-EOS +[quote] +A famouse quote. ++ + EOS + output = render_embedded_string input + assert_css '.quoteblock:root', output, 1 + assert_css '.paragraph:root', output, 1 + assert_xpath %(/*[@class="paragraph"]/p[text() = "+"]), output, 1 + end + test "verse paragraph" do output = render_string("[verse]\nFamous verse.") assert_xpath '//*[@class = "verseblock"]', output, 1 diff --git a/test/sections_test.rb b/test/sections_test.rb index cd881d47..4bf2891f 100644 --- a/test/sections_test.rb +++ b/test/sections_test.rb @@ -257,6 +257,23 @@ not in section assert floatingtitle.is_a?(Asciidoctor::Block) assert !floatingtitle.is_a?(Asciidoctor::Section) assert_equal :floating_title, floatingtitle.context + assert_equal '_plain_ol_heading', floatingtitle.id + assert doc.references[:ids].has_key?('_plain_ol_heading') + end + + test 'can assign explicit id to floating title' do + input = <<-EOS +[[unchained]] +[float] +=== Plain Ol' Heading + +not in section + EOS + + doc = document_from_string input + floating_title = doc.blocks.first + assert_equal 'unchained', floating_title.id + assert doc.references[:ids].has_key?('unchained') end test 'should not include floating title in toc' do diff --git a/test/substitutions_test.rb b/test/substitutions_test.rb index 4db8146f..c720f828 100644 --- a/test/substitutions_test.rb +++ b/test/substitutions_test.rb @@ -255,8 +255,8 @@ context 'Substitutions' do end test 'multi-line superscript chars' do - para = block_from_string(%Q{x^(n\n+\n1)^}) - assert_equal "x<sup>(n\n+\n1)</sup>", para.sub_quotes(para.buffer.join) + para = block_from_string(%Q{x^(n\n-\n1)^}) + assert_equal "x<sup>(n\n-\n1)</sup>", para.sub_quotes(para.buffer.join) end test 'single-line subscript chars' do diff --git a/test/tables_test.rb b/test/tables_test.rb index b8e15783..8980ce90 100644 --- a/test/tables_test.rb +++ b/test/tables_test.rb @@ -43,6 +43,21 @@ context 'Tables' do assert_xpath '/table/caption/following-sibling::colgroup', output, 1 end + test 'renders explicit caption on simple psv table' do + input = <<-EOS +[caption="All the Data. "] +.Simple psv table +|======= +|A |B |C +|a |b |c +|1 |2 |3 +|======= + EOS + output = render_embedded_string input + assert_xpath '/table/caption[@class="title"][text()="All the Data. Simple psv table"]', output, 1 + assert_xpath '/table/caption/following-sibling::colgroup', output, 1 + end + test 'ignores escaped separators' do input = <<-EOS |=== diff --git a/test/test_helper.rb b/test/test_helper.rb index 9e5f63bb..b3653f01 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -67,8 +67,8 @@ class Test::Unit::TestCase xmlnodes_at_path(:css, css, content) end - def xmlnodes_at_xpath(css, content, count = nil) - xmlnodes_at_path(:xpath, css, content) + def xmlnodes_at_xpath(xpath, content, count = nil) + xmlnodes_at_path(:xpath, xpath, content) end def xmlnodes_at_path(type, path, content, count = nil) |
