lib/asciidoctor/parser.rb in asciidoctor-1.5.3 vs lib/asciidoctor/parser.rb in asciidoctor-1.5.4
- old
+ new
@@ -83,66 +83,67 @@
# This method assumes that there are no blank lines at the start of the document,
# which are automatically removed by the reader.
#
# returns the Hash of orphan block attributes captured above the header
def self.parse_document_header(reader, document)
- # capture any lines of block-level metadata and plow away any comment lines
- # that precede first block
+ # capture lines of block-level metadata and plow away comment lines that precede first block
block_attributes = parse_block_metadata_lines(reader, document)
# special case, block title is not allowed above document title,
# carry attributes over to the document body
- if block_attributes.has_key?('title')
+ if (has_doctitle_line = is_next_line_document_title?(reader, block_attributes)) &&
+ block_attributes.has_key?('title')
return document.finalize_header block_attributes, false
end
# yep, document title logic in AsciiDoc is just insanity
# definitely an area for spec refinement
assigned_doctitle = nil
unless (val = document.attributes['doctitle']).nil_or_empty?
- document.title = val
- assigned_doctitle = val
+ document.title = assigned_doctitle = val
end
section_title = nil
- # check if the first line is the document title
- # if so, add a header to the document and parse the header metadata
- if is_next_line_document_title?(reader, block_attributes)
+ # if the first line is the document title, add a header to the document and parse the header metadata
+ if has_doctitle_line
source_location = reader.cursor if document.sourcemap
- document.id, _, doctitle, _, single_line = parse_section_title(reader, document)
+ document.id, _, doctitle, _, single_line = parse_section_title reader, document
unless assigned_doctitle
- document.title = doctitle
- assigned_doctitle = doctitle
+ document.title = assigned_doctitle = doctitle
end
# default to compat-mode if document uses atx-style doctitle
document.set_attribute 'compat-mode', '' unless single_line
- if (separator = block_attributes.delete('separator'))
- document.set_attribute('title-separator', separator)
+ if (separator = block_attributes.delete 'separator')
+ document.set_attribute 'title-separator', separator
end
document.header.source_location = source_location if source_location
document.attributes['doctitle'] = section_title = doctitle
# QUESTION: should the id assignment on Document be encapsulated in the Document class?
- unless document.id
- document.id = block_attributes.delete('id')
+ if document.id
+ block_attributes.delete 1
+ block_attributes.delete 'id'
+ else
+ if (style = block_attributes.delete 1)
+ style_attrs = { 1 => style }
+ parse_style_attribute style_attrs, reader
+ block_attributes['id'] = style_attrs['id'] if style_attrs.key? 'id'
+ end
+ document.id = block_attributes.delete 'id'
end
- parse_header_metadata(reader, document)
+ parse_header_metadata reader, document
end
- if !(val = document.attributes['doctitle']).nil_or_empty? &&
- val != section_title
- document.title = val
- assigned_doctitle = val
+ unless (val = document.attributes['doctitle']).nil_or_empty? || val == section_title
+ document.title = assigned_doctitle = val
end
# restore doctitle attribute to original assignment
- if assigned_doctitle
- document.attributes['doctitle'] = assigned_doctitle
- end
+ document.attributes['doctitle'] = assigned_doctitle if assigned_doctitle
# parse title and consume name section of manpage document
parse_manpage_header(reader, document) if document.doctype == 'manpage'
-
+
# NOTE block_attributes are the block-level attributes (not document attributes) that
# precede the first line of content (document title, first section or first block)
document.finalize_header block_attributes
end
@@ -166,11 +167,11 @@
name_section = initialize_section(reader, document, {})
if name_section.level == 1
name_section_buffer = reader.read_lines_until(:break_on_blank_lines => true).join(' ').tr_s(' ', ' ')
if (m = ManpageNamePurposeRx.match(name_section_buffer))
document.attributes['manname'] = document.sub_attributes m[1]
- document.attributes['manpurpose'] = m[2]
+ document.attributes['manpurpose'] = m[2]
# TODO parse multiple man names
if document.backend == 'manpage'
document.attributes['docname'] = document.attributes['manname']
document.attributes['outfilesuffix'] = %(.#{document.attributes['manvolnum']})
@@ -337,11 +338,11 @@
warn %(asciidoctor: ERROR: #{block_line_info}: illegal block content outside of partintro block)
# rebuild [partintro] paragraph as an open block
elsif first_block.content_model != :compound
intro = Block.new section, :open, :content_model => :compound
intro.style = 'partintro'
- section.blocks.shift
+ section.blocks.shift
if first_block.style == 'partintro'
first_block.context = :paragraph
first_block.style = nil
end
first_block.parent = intro
@@ -404,11 +405,11 @@
# and Block delimiters. The ensuing lines are then processed according
# to the type of content.
#
# reader - The Reader from which to retrieve the next block
# parent - The Document, Section or Block to which the next block belongs
- #
+ #
# Returns a Section or Block object holding the parsed content of the processed lines
#--
# QUESTION should next_block have an option for whether it should keep looking until
# a block is found? right now it bails when it encounters a line to be skipped
def self.next_block(reader, parent, attributes = {}, options = {})
@@ -423,11 +424,11 @@
# used and block content is acceptable
if (text_only = options[:text]) && skipped > 0
options.delete(:text)
text_only = false
end
-
+
parse_metadata = options.fetch(:parse_metadata, true)
#parse_sections = options.fetch(:parse_sections, false)
document = parent.document
if (extensions = document.extensions)
@@ -832,11 +833,11 @@
end
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
when :literal
block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
-
+
when :pass
block = build_block(block_context, :raw, terminator, parent, reader, attributes)
when :stem, :latexmath, :asciimath
if block_context == :stem
@@ -1076,11 +1077,11 @@
if content_model == :verbatim
if (indent = attributes['indent'])
adjust_indentation! lines, indent, (attributes['tabsize'] || parent.document.attributes['tabsize'])
elsif (tab_size = (attributes['tabsize'] || parent.document.attributes['tabsize']).to_i) > 0
adjust_indentation! lines, nil, tab_size
- end
+ end
end
if (extension = options[:extension])
# QUESTION do we want to delete the style?
attributes.delete('style')
@@ -1323,11 +1324,11 @@
cursor = reader.cursor
list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), cursor
if list_item_reader.has_more_lines?
comment_lines = list_item_reader.skip_line_comments
subsequent_line = list_item_reader.peek_line
- list_item_reader.unshift_lines comment_lines unless comment_lines.empty?
+ list_item_reader.unshift_lines comment_lines unless comment_lines.empty?
if !subsequent_line.nil?
continuation_connects_first_block = subsequent_line.empty?
# if there's no continuation connecting the first block, then
# treat the lines as paragraph text (activated when has_text = false)
@@ -1372,21 +1373,21 @@
# terminator (such as a line comment). Definition lists are more greedy if
# they don't have optional inline item text...they want that text
#
# reader - The Reader from which to retrieve the lines.
# list_type - The Symbol context of the list (:ulist, :olist, :colist or :dlist)
- # sibling_trait - A Regexp that matches a sibling of this list item or String list marker
+ # sibling_trait - A Regexp that matches a sibling of this list item or String list marker
# of the items in this list (default: nil)
# 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.read_lines_for_list_item(reader, list_type, sibling_trait = nil, has_text = true)
buffer = []
# three states for continuation: :inactive, :active & :frozen
# :frozen signifies we've detected sequential continuation lines &
- # continuation is not permitted until reset
+ # continuation is not permitted until reset
continuation = :inactive
# if we are within a nested list, we don't throw away the list
# continuation marks because they will be processed when grabbing
# the lines for those nested lists
@@ -1442,11 +1443,11 @@
# FIXME to be AsciiDoc compliant, we shouldn't break if style in attribute line is "literal" (i.e., [literal])
elsif list_type == :dlist && continuation != :active && BlockAttributeLineRx =~ this_line
break
else
if continuation == :active && !this_line.empty?
- # literal paragraphs have special considerations (and this is one of
+ # literal paragraphs have special considerations (and this is one of
# two entry points into one)
# if we don't process it as a whole, then a line in it that looks like a
# list item will throw off the exit from it
if LiteralParagraphRx =~ this_line
reader.unshift_line this_line
@@ -1475,11 +1476,11 @@
end
elsif !prev_line.nil? && prev_line.empty?
# advance to the next line of content
if this_line.empty?
reader.skip_blank_lines
- this_line = reader.read_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
if this_line == LIST_CONTINUATION
@@ -1832,11 +1833,11 @@
process_attribute_entries(reader, document)
rev_metadata = {}
if reader.has_more_lines? && !reader.next_line_empty?
- rev_line = reader.read_line
+ rev_line = reader.read_line
if (match = RevisionInfoLineRx.match(rev_line))
rev_metadata['revnumber'] = match[1].rstrip if match[1]
unless (component = match[2].strip) == ''
# version must begin with 'v' if date is absent
if !match[1] && (component.start_with? 'v')
@@ -1937,12 +1938,15 @@
end
end
segments = nil
if names_only
- # splitting on ' ' with limit will collapse repeating spaces
- segments = author_entry.split(' ', 3)
+ # splitting on ' ' collapses repeating spaces uniformly
+ # `split ' ', 3` causes odd behavior in Opal; see https://github.com/asciidoctor/asciidoctor.js/issues/159
+ if (segments = author_entry.split ' ').size > 3
+ segments = segments[0..1].push(segments[2..-1].join ' ')
+ end
elsif (match = AuthorInfoLineRx.match(author_entry))
segments = match.to_a
segments.shift
end
@@ -2185,11 +2189,11 @@
#
# marker = 'B.'
# Parser.resolve_ordered_list_marker(marker, 1, true)
# # => 'A.'
#
- # Returns the String of the first marker in this number series
+ # Returns the String of the first marker in this number series
def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false, reader = nil)
number_style = ORDERED_LIST_STYLES.detect {|s| OrderedListMarkerRxMap[s] =~ marker }
expected = actual = nil
case number_style
when :arabic
@@ -2236,11 +2240,11 @@
# Internal: Determine whether the this line is a sibling list item
# according to the list type and trait (marker) provided.
#
# line - The String line to check
# list_type - The context of the list (:olist, :ulist, :colist, :dlist)
- # sibling_trait - The String marker for the list or the Regexp to match a sibling
+ # sibling_trait - The String marker for the list or the Regexp to match a sibling
#
# Returns a Boolean indicating whether this line is a sibling list item given
# the criteria provided
def self.is_sibling_list_item?(line, list_type, sibling_trait)
if sibling_trait.is_a? ::Regexp
@@ -2274,15 +2278,15 @@
if (attributes.has_key? 'title')
table.title = attributes.delete 'title'
table.assign_caption attributes.delete('caption')
end
- if attributes['cols'].nil_or_empty?
- explicit_col_specs = false
- else
- table.create_columns(parse_col_specs(attributes['cols']))
+ if (attributes.key? 'cols') && !(col_specs = parse_col_specs attributes['cols']).empty?
+ table.create_columns col_specs
explicit_col_specs = true
+ else
+ explicit_col_specs = false
end
skipped = table_reader.skip_blank_lines
parser_ctx = Table::ParserContext.new(table_reader, table, attributes)
@@ -2374,16 +2378,12 @@
# NOTE may have already closed cell in csv or dsv table (see previous call to parser_ctx.close_cell(true))
parser_ctx.close_cell true if parser_ctx.cell_open?
end
end
- table.attributes['colcount'] ||= parser_ctx.col_count
-
- if !explicit_col_specs
- # TODO further encapsulate this logic (into table perhaps?)
- even_width = (100.0 / parser_ctx.col_count).floor
- table.columns.each {|c| c.assign_width(0, even_width) }
+ unless (table.attributes['colcount'] ||= table.columns.size) == 0 || explicit_col_specs
+ table.assign_col_widths
end
table.partition_header_footer attributes
table
@@ -2391,21 +2391,21 @@
# Internal: Parse the column specs for this table.
#
# The column specs dictate the number of columns, relative
# width of columns, default alignments for cells in each
- # column, and/or default styles or filters applied to the cells in
+ # column, and/or default styles or filters applied to the cells in
# the column.
#
# Every column spec is guaranteed to have a width
#
# returns a Hash of attributes that specify how to format
# and layout the cells in the table.
- def self.parse_col_specs(records)
+ def self.parse_col_specs records
+ records = records.tr ' ', '' if records.include? ' '
# check for deprecated syntax: single number, equal column spread
- # REVIEW could use records == records.to_i.to_s instead of regexp
- if DigitsRx =~ records
+ if records == records.to_i.to_s
return ::Array.new(records.to_i) { { 'width' => 1 } }
end
specs = []
# NOTE -1 argument ensures we don't drop empty records
@@ -2453,11 +2453,11 @@
# colspan, rowspan and/or repeating content.
#
# The default spec when pos == :end is {} since we already know we're at a
# delimiter. When pos == :start, we *may* be at a delimiter, nil indicates
# we're not.
- #
+ #
# returns the Hash of attributes that indicate how to layout
# and style this cell in the table.
def self.parse_cell_spec(line, pos = :start, delimiter = nil)
m = nil
rest = ''
@@ -2494,11 +2494,11 @@
spec['rowspan'] = rowspec unless rowspec == 1
elsif m[2] == '*'
spec['repeatcol'] = colspec unless colspec == 1
end
end
-
+
if m[3]
colspec, rowspec = m[3].split '.'
if !colspec.nil_or_empty? && Table::ALIGNMENTS[:h].has_key?(colspec)
spec['halign'] = Table::ALIGNMENTS[:h][colspec]
end
@@ -2582,11 +2582,11 @@
end
else
collector.push c
end
end
-
+
# small optimization if no shorthand is found
if type == :style
parsed_style = attributes['style'] = raw_style
else
save_current.call
@@ -2608,11 +2608,11 @@
if parsed.has_key? :option
(options = parsed[:option]).each do |option|
attributes[%(#{option}-option)] = ''
end
if (existing_opts = attributes['options'])
- attributes['options'] = (options + existing_opts.split(',')) * ','
+ attributes['options'] = (options + existing_opts.split(',')) * ','
else
attributes['options'] = options * ','
end
end
end
@@ -2753,10 +2753,10 @@
# Returns the Integer for this Roman numeral
def self.roman_numeral_to_int(value)
value = value.downcase
digits = { 'i' => 1, 'v' => 5, 'x' => 10 }
result = 0
-
+
(0..value.length - 1).each {|i|
digit = digits[value[i..i]]
if i + 1 < value.length && digits[value[i+1..i+1]] > digit
result -= digit
else