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