asciidoctor 1.5.6.2 → 1.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of asciidoctor might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +330 -143
- data/README-fr.adoc +441 -0
- data/README-jp.adoc +418 -0
- data/README-zh_CN.adoc +430 -0
- data/README.adoc +454 -0
- data/Rakefile +57 -0
- data/asciidoctor.gemspec +7 -1
- data/data/locale/attributes-ar.adoc +22 -0
- data/data/locale/attributes-bg.adoc +22 -0
- data/data/locale/attributes-ca.adoc +22 -0
- data/data/locale/attributes-cs.adoc +22 -0
- data/data/locale/attributes-da.adoc +22 -0
- data/data/locale/attributes-de.adoc +22 -0
- data/data/locale/attributes-en.adoc +23 -0
- data/data/locale/attributes-es.adoc +22 -0
- data/data/locale/attributes-fa.adoc +22 -0
- data/data/locale/attributes-fi.adoc +22 -0
- data/data/locale/attributes-fr.adoc +22 -0
- data/data/locale/attributes-hu.adoc +22 -0
- data/data/locale/attributes-id.adoc +22 -0
- data/data/locale/attributes-it.adoc +22 -0
- data/data/locale/attributes-ja.adoc +22 -0
- data/data/locale/attributes-kr.adoc +22 -0
- data/data/locale/attributes-nb.adoc +22 -0
- data/data/locale/attributes-nl.adoc +22 -0
- data/data/locale/attributes-nn.adoc +22 -0
- data/data/locale/attributes-pl.adoc +22 -0
- data/data/locale/attributes-pt.adoc +22 -0
- data/data/locale/attributes-pt_BR.adoc +22 -0
- data/data/locale/attributes-ro.adoc +22 -0
- data/data/locale/attributes-ru.adoc +22 -0
- data/data/locale/attributes-sr.adoc +22 -0
- data/data/locale/attributes-sr_Latn.adoc +22 -0
- data/data/locale/attributes-tr.adoc +22 -0
- data/data/locale/attributes-uk.adoc +22 -0
- data/data/locale/attributes-zh_CN.adoc +22 -0
- data/data/locale/attributes-zh_TW.adoc +22 -0
- data/data/locale/attributes.adoc +8 -649
- data/data/stylesheets/asciidoctor-default.css +77 -72
- data/features/xref.feature +366 -7
- data/lib/asciidoctor.rb +107 -93
- data/lib/asciidoctor/abstract_block.rb +247 -239
- data/lib/asciidoctor/abstract_node.rb +56 -58
- data/lib/asciidoctor/block.rb +3 -3
- data/lib/asciidoctor/callouts.rb +1 -1
- data/lib/asciidoctor/cli/invoker.rb +36 -9
- data/lib/asciidoctor/cli/options.rb +63 -25
- data/lib/asciidoctor/converter.rb +23 -13
- data/lib/asciidoctor/converter/base.rb +4 -0
- data/lib/asciidoctor/converter/docbook45.rb +16 -9
- data/lib/asciidoctor/converter/docbook5.rb +115 -97
- data/lib/asciidoctor/converter/factory.rb +29 -31
- data/lib/asciidoctor/converter/html5.rb +229 -192
- data/lib/asciidoctor/converter/manpage.rb +72 -50
- data/lib/asciidoctor/converter/template.rb +12 -12
- data/lib/asciidoctor/core_ext.rb +5 -1
- data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +6 -0
- data/lib/asciidoctor/document.rb +168 -77
- data/lib/asciidoctor/extensions.rb +79 -47
- data/lib/asciidoctor/helpers.rb +33 -11
- data/lib/asciidoctor/inline.rb +3 -2
- data/lib/asciidoctor/list.rb +2 -1
- data/lib/asciidoctor/logging.rb +122 -0
- data/lib/asciidoctor/parser.rb +406 -382
- data/lib/asciidoctor/path_resolver.rb +169 -162
- data/lib/asciidoctor/reader.rb +166 -121
- data/lib/asciidoctor/section.rb +45 -28
- data/lib/asciidoctor/stylesheets.rb +13 -5
- data/lib/asciidoctor/substitutors.rb +328 -254
- data/lib/asciidoctor/table.rb +105 -48
- data/lib/asciidoctor/timings.rb +34 -6
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +41 -23
- data/man/asciidoctor.adoc +14 -8
- data/test/api_test.rb +1004 -0
- data/test/attributes_test.rb +241 -50
- data/test/blocks_test.rb +549 -124
- data/test/converter_test.rb +170 -78
- data/test/document_test.rb +208 -767
- data/test/extensions_test.rb +188 -53
- data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +1 -1
- data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +1 -1
- data/test/fixtures/file-with-missing-include.adoc +1 -0
- data/test/fixtures/include-file.jsx +8 -0
- data/test/fixtures/lists.adoc +96 -0
- data/test/fixtures/other-chapters.adoc +11 -0
- data/test/fixtures/outer-include.adoc +5 -0
- data/test/fixtures/sample.asciidoc +5 -1
- data/test/fixtures/subdir/index.adoc +3 -0
- data/test/fixtures/subdir/inner-include.adoc +3 -0
- data/test/fixtures/subdir/middle-include.adoc +5 -0
- data/test/fixtures/tagged-class-enclosed.rb +0 -1
- data/test/fixtures/unclosed-tag.adoc +3 -0
- data/test/fixtures/unexpected-end-tag.adoc +4 -0
- data/test/invoker_test.rb +101 -40
- data/test/links_test.rb +266 -72
- data/test/lists_test.rb +243 -45
- data/test/logger_test.rb +211 -0
- data/test/manpage_test.rb +124 -6
- data/test/options_test.rb +46 -1
- data/test/paragraphs_test.rb +23 -10
- data/test/parser_test.rb +30 -1
- data/test/paths_test.rb +115 -33
- data/test/preamble_test.rb +1 -1
- data/test/reader_test.rb +337 -81
- data/test/sections_test.rb +656 -72
- data/test/substitutions_test.rb +182 -57
- data/test/tables_test.rb +324 -57
- data/test/test_helper.rb +77 -32
- data/test/text_test.rb +7 -7
- metadata +67 -3
    
        data/lib/asciidoctor/parser.rb
    CHANGED
    
    | @@ -23,6 +23,7 @@ module Asciidoctor | |
| 23 23 | 
             
            #   block.class
         | 
| 24 24 | 
             
            #   # => Asciidoctor::Block
         | 
| 25 25 | 
             
            class Parser
         | 
| 26 | 
            +
              include Logging
         | 
| 26 27 |  | 
| 27 28 | 
             
              BlockMatchData = Struct.new :context, :masq, :tip, :terminator
         | 
| 28 29 |  | 
| @@ -93,7 +94,10 @@ class Parser | |
| 93 94 |  | 
| 94 95 | 
             
                while reader.has_more_lines?
         | 
| 95 96 | 
             
                  new_section, block_attributes = next_section(reader, document, block_attributes)
         | 
| 96 | 
            -
                   | 
| 97 | 
            +
                  if new_section
         | 
| 98 | 
            +
                    document.assign_numeral new_section
         | 
| 99 | 
            +
                    document.blocks << new_section
         | 
| 100 | 
            +
                  end
         | 
| 97 101 | 
             
                end unless options[:header_only]
         | 
| 98 102 |  | 
| 99 103 | 
             
                document
         | 
| @@ -113,65 +117,63 @@ class Parser | |
| 113 117 | 
             
              # returns the Hash of orphan block attributes captured above the header
         | 
| 114 118 | 
             
              def self.parse_document_header(reader, document)
         | 
| 115 119 | 
             
                # capture lines of block-level metadata and plow away comment lines that precede first block
         | 
| 116 | 
            -
                 | 
| 120 | 
            +
                block_attrs = parse_block_metadata_lines reader, document
         | 
| 121 | 
            +
                doc_attrs = document.attributes
         | 
| 117 122 |  | 
| 118 123 | 
             
                # special case, block title is not allowed above document title,
         | 
| 119 124 | 
             
                # carry attributes over to the document body
         | 
| 120 | 
            -
                if (implicit_doctitle = is_next_line_doctitle? reader,  | 
| 121 | 
            -
             | 
| 122 | 
            -
                  return document.finalize_header block_attributes, false
         | 
| 125 | 
            +
                if (implicit_doctitle = is_next_line_doctitle? reader, block_attrs, doc_attrs['leveloffset']) && block_attrs['title']
         | 
| 126 | 
            +
                  return document.finalize_header block_attrs, false
         | 
| 123 127 | 
             
                end
         | 
| 124 128 |  | 
| 125 129 | 
             
                # yep, document title logic in AsciiDoc is just insanity
         | 
| 126 130 | 
             
                # definitely an area for spec refinement
         | 
| 127 131 | 
             
                assigned_doctitle = nil
         | 
| 128 | 
            -
                unless (val =  | 
| 132 | 
            +
                unless (val = doc_attrs['doctitle']).nil_or_empty?
         | 
| 129 133 | 
             
                  document.title = assigned_doctitle = val
         | 
| 130 134 | 
             
                end
         | 
| 131 135 |  | 
| 132 | 
            -
                section_title = nil
         | 
| 133 136 | 
             
                # if the first line is the document title, add a header to the document and parse the header metadata
         | 
| 134 137 | 
             
                if implicit_doctitle
         | 
| 135 138 | 
             
                  source_location = reader.cursor if document.sourcemap
         | 
| 136 139 | 
             
                  document.id, _, doctitle, _, atx = parse_section_title reader, document
         | 
| 137 | 
            -
                  unless assigned_doctitle
         | 
| 138 | 
            -
             | 
| 139 | 
            -
                  end
         | 
| 140 | 
            +
                  document.title = assigned_doctitle = doctitle unless assigned_doctitle
         | 
| 141 | 
            +
                  document.header.source_location = source_location if source_location
         | 
| 140 142 | 
             
                  # default to compat-mode if document uses atx-style doctitle
         | 
| 141 | 
            -
                   | 
| 142 | 
            -
                  if (separator =  | 
| 143 | 
            -
                     | 
| 143 | 
            +
                  doc_attrs['compat-mode'] = '' unless atx || (document.attribute_locked? 'compat-mode')
         | 
| 144 | 
            +
                  if (separator = block_attrs['separator'])
         | 
| 145 | 
            +
                    doc_attrs['title-separator'] = separator unless document.attribute_locked? 'title-separator'
         | 
| 144 146 | 
             
                  end
         | 
| 145 | 
            -
                   | 
| 146 | 
            -
                   | 
| 147 | 
            -
             | 
| 148 | 
            -
                  if document.id
         | 
| 149 | 
            -
                    block_attributes.delete 1
         | 
| 150 | 
            -
                    block_attributes.delete 'id'
         | 
| 147 | 
            +
                  doc_attrs['doctitle'] = section_title = doctitle
         | 
| 148 | 
            +
                  if (doc_id = block_attrs['id'])
         | 
| 149 | 
            +
                    document.id = doc_id
         | 
| 151 150 | 
             
                  else
         | 
| 152 | 
            -
                     | 
| 153 | 
            -
             | 
| 154 | 
            -
             | 
| 155 | 
            -
             | 
| 156 | 
            -
                    end
         | 
| 157 | 
            -
                    document.id = block_attributes.delete 'id'
         | 
| 151 | 
            +
                    doc_id = document.id
         | 
| 152 | 
            +
                  end
         | 
| 153 | 
            +
                  if (doc_role = block_attrs['role'])
         | 
| 154 | 
            +
                    doc_attrs['docrole'] = doc_role
         | 
| 158 155 | 
             
                  end
         | 
| 156 | 
            +
                  if (doc_reftext = block_attrs['reftext'])
         | 
| 157 | 
            +
                    doc_attrs['reftext'] = doc_reftext
         | 
| 158 | 
            +
                  end
         | 
| 159 | 
            +
                  block_attrs = {}
         | 
| 159 160 | 
             
                  parse_header_metadata reader, document
         | 
| 161 | 
            +
                  document.register :refs, [doc_id, document] if doc_id
         | 
| 160 162 | 
             
                end
         | 
| 161 163 |  | 
| 162 | 
            -
                unless (val =  | 
| 164 | 
            +
                unless (val = doc_attrs['doctitle']).nil_or_empty? || val == section_title
         | 
| 163 165 | 
             
                  document.title = assigned_doctitle = val
         | 
| 164 166 | 
             
                end
         | 
| 165 167 |  | 
| 166 168 | 
             
                # restore doctitle attribute to original assignment
         | 
| 167 | 
            -
                 | 
| 169 | 
            +
                doc_attrs['doctitle'] = assigned_doctitle if assigned_doctitle
         | 
| 168 170 |  | 
| 169 171 | 
             
                # parse title and consume name section of manpage document
         | 
| 170 172 | 
             
                parse_manpage_header(reader, document) if document.doctype == 'manpage'
         | 
| 171 173 |  | 
| 172 | 
            -
                # NOTE  | 
| 174 | 
            +
                # NOTE block_attrs are the block-level attributes (not document attributes) that
         | 
| 173 175 | 
             
                # precede the first line of content (document title, first section or first block)
         | 
| 174 | 
            -
                document.finalize_header  | 
| 176 | 
            +
                document.finalize_header block_attrs
         | 
| 175 177 | 
             
              end
         | 
| 176 178 |  | 
| 177 179 | 
             
              # Public: Parses the manpage header of the AsciiDoc source read from the Reader
         | 
| @@ -179,10 +181,10 @@ class Parser | |
| 179 181 | 
             
              # returns Nothing
         | 
| 180 182 | 
             
              def self.parse_manpage_header(reader, document)
         | 
| 181 183 | 
             
                if ManpageTitleVolnumRx =~ document.attributes['doctitle']
         | 
| 182 | 
            -
                  document.attributes['mantitle'] = document.sub_attributes $1.downcase
         | 
| 184 | 
            +
                  document.attributes['mantitle'] = (($1.include? ATTR_REF_HEAD) ? (document.sub_attributes $1) : $1).downcase
         | 
| 183 185 | 
             
                  document.attributes['manvolnum'] = $2
         | 
| 184 186 | 
             
                else
         | 
| 185 | 
            -
                   | 
| 187 | 
            +
                  logger.error message_with_context 'malformed manpage title', :source_location => reader.cursor_at_prev_line
         | 
| 186 188 | 
             
                  # provide sensible fallbacks
         | 
| 187 189 | 
             
                  document.attributes['mantitle'] = document.attributes['doctitle']
         | 
| 188 190 | 
             
                  document.attributes['manvolnum'] = '1'
         | 
| @@ -190,28 +192,36 @@ class Parser | |
| 190 192 |  | 
| 191 193 | 
             
                reader.skip_blank_lines
         | 
| 192 194 |  | 
| 193 | 
            -
                if is_next_line_section? | 
| 194 | 
            -
                  name_section = initialize_section | 
| 195 | 
            +
                if is_next_line_section? reader, {}
         | 
| 196 | 
            +
                  name_section = initialize_section reader, document, {}
         | 
| 195 197 | 
             
                  if name_section.level == 1
         | 
| 196 | 
            -
                    name_section_buffer = reader.read_lines_until | 
| 197 | 
            -
                    if  | 
| 198 | 
            -
                      document.attributes['manname']  | 
| 199 | 
            -
                      document.attributes[' | 
| 200 | 
            -
                       | 
| 198 | 
            +
                    name_section_buffer = (reader.read_lines_until :break_on_blank_lines => true, :skip_line_comments => true) * ' '
         | 
| 199 | 
            +
                    if ManpageNamePurposeRx =~ name_section_buffer
         | 
| 200 | 
            +
                      document.attributes['manname-title'] ||= name_section.title
         | 
| 201 | 
            +
                      document.attributes['manname-id'] = name_section.id if name_section.id
         | 
| 202 | 
            +
                      document.attributes['manpurpose'] = $2
         | 
| 203 | 
            +
                      if (manname = ($1.include? ATTR_REF_HEAD) ? (document.sub_attributes $1) : $1).include? ','
         | 
| 204 | 
            +
                        manname = (mannames = (manname.split ',').map {|n| n.lstrip })[0]
         | 
| 205 | 
            +
                      else
         | 
| 206 | 
            +
                        mannames = [manname]
         | 
| 207 | 
            +
                      end
         | 
| 208 | 
            +
                      document.attributes['manname'] = manname
         | 
| 209 | 
            +
                      document.attributes['mannames'] = mannames
         | 
| 201 210 |  | 
| 202 211 | 
             
                      if document.backend == 'manpage'
         | 
| 203 | 
            -
                        document.attributes['docname'] =  | 
| 212 | 
            +
                        document.attributes['docname'] = manname
         | 
| 204 213 | 
             
                        document.attributes['outfilesuffix'] = %(.#{document.attributes['manvolnum']})
         | 
| 205 214 | 
             
                      end
         | 
| 206 215 | 
             
                    else
         | 
| 207 | 
            -
                       | 
| 216 | 
            +
                      logger.error message_with_context 'malformed name section body', :source_location => reader.cursor_at_prev_line
         | 
| 208 217 | 
             
                    end
         | 
| 209 218 | 
             
                  else
         | 
| 210 | 
            -
                     | 
| 219 | 
            +
                    logger.error message_with_context 'name section title must be at level 1', :source_location => reader.cursor_at_prev_line
         | 
| 211 220 | 
             
                  end
         | 
| 212 221 | 
             
                else
         | 
| 213 | 
            -
                   | 
| 222 | 
            +
                  logger.error message_with_context 'name section expected', :source_location => reader.cursor_at_prev_line
         | 
| 214 223 | 
             
                end
         | 
| 224 | 
            +
                nil
         | 
| 215 225 | 
             
              end
         | 
| 216 226 |  | 
| 217 227 | 
             
              # Public: Return the next section from the Reader.
         | 
| @@ -254,37 +264,41 @@ class Parser | |
| 254 264 | 
             
              def self.next_section reader, parent, attributes = {}
         | 
| 255 265 | 
             
                preamble = intro = part = false
         | 
| 256 266 |  | 
| 257 | 
            -
                # FIXME if attributes[1] is a verbatim style, then don't check for section
         | 
| 258 | 
            -
             | 
| 259 267 | 
             
                # check if we are at the start of processing the document
         | 
| 260 268 | 
             
                # NOTE we could drop a hint in the attributes to indicate
         | 
| 261 269 | 
             
                # that we are at a section title (so we don't have to check)
         | 
| 262 270 | 
             
                if parent.context == :document && parent.blocks.empty? && ((has_header = parent.has_header?) ||
         | 
| 263 271 | 
             
                    (attributes.delete 'invalid-header') || !(is_next_line_section? reader, attributes))
         | 
| 264 | 
            -
                   | 
| 265 | 
            -
                  if has_header || ( | 
| 272 | 
            +
                  book = (document = parent).doctype == 'book'
         | 
| 273 | 
            +
                  if has_header || (book && attributes[1] != 'abstract')
         | 
| 266 274 | 
             
                    preamble = intro = (Block.new parent, :preamble, :content_model => :compound)
         | 
| 267 | 
            -
                    preamble.title = parent.attr 'preface-title' if  | 
| 268 | 
            -
                    parent << preamble
         | 
| 275 | 
            +
                    preamble.title = parent.attr 'preface-title' if book && (parent.attr? 'preface-title')
         | 
| 276 | 
            +
                    parent.blocks << preamble
         | 
| 269 277 | 
             
                  end
         | 
| 270 278 | 
             
                  section = parent
         | 
| 271 | 
            -
             | 
| 272 279 | 
             
                  current_level = 0
         | 
| 273 280 | 
             
                  if parent.attributes.key? 'fragment'
         | 
| 274 | 
            -
                     | 
| 281 | 
            +
                    expected_next_level = -1
         | 
| 275 282 | 
             
                  # small tweak to allow subsequent level-0 sections for book doctype
         | 
| 276 | 
            -
                  elsif  | 
| 277 | 
            -
                     | 
| 283 | 
            +
                  elsif book
         | 
| 284 | 
            +
                    expected_next_level, expected_next_level_alt = 1, 0
         | 
| 278 285 | 
             
                  else
         | 
| 279 | 
            -
                     | 
| 286 | 
            +
                    expected_next_level = 1
         | 
| 280 287 | 
             
                  end
         | 
| 281 288 | 
             
                else
         | 
| 282 | 
            -
                   | 
| 289 | 
            +
                  book = (document = parent.document).doctype == 'book'
         | 
| 283 290 | 
             
                  section = initialize_section reader, parent, attributes
         | 
| 284 291 | 
             
                  # clear attributes except for title attribute, which must be carried over to next content block
         | 
| 285 292 | 
             
                  attributes = (title = attributes['title']) ? { 'title' => title } : {}
         | 
| 286 | 
            -
                   | 
| 287 | 
            -
                   | 
| 293 | 
            +
                  expected_next_level = (current_level = section.level) + 1
         | 
| 294 | 
            +
                  if current_level == 0
         | 
| 295 | 
            +
                    part = book
         | 
| 296 | 
            +
                  elsif current_level == 1 && section.special
         | 
| 297 | 
            +
                    # NOTE technically preface and abstract sections are only permitted in the book doctype
         | 
| 298 | 
            +
                    unless (sectname = section.sectname) == 'appendix' || sectname == 'preface' || sectname == 'abstract'
         | 
| 299 | 
            +
                      expected_next_level = nil
         | 
| 300 | 
            +
                    end
         | 
| 301 | 
            +
                  end
         | 
| 288 302 | 
             
                end
         | 
| 289 303 |  | 
| 290 304 | 
             
                reader.skip_blank_lines
         | 
| @@ -300,28 +314,32 @@ class Parser | |
| 300 314 | 
             
                # otherwise subsequent metadata lines get interpreted as block content
         | 
| 301 315 | 
             
                while reader.has_more_lines?
         | 
| 302 316 | 
             
                  parse_block_metadata_lines reader, document, attributes
         | 
| 303 | 
            -
             | 
| 304 317 | 
             
                  if (next_level = is_next_line_section?(reader, attributes))
         | 
| 305 318 | 
             
                    next_level += document.attr('leveloffset').to_i if document.attr?('leveloffset')
         | 
| 306 | 
            -
                    if next_level > current_level | 
| 307 | 
            -
                      if  | 
| 308 | 
            -
                         | 
| 309 | 
            -
             | 
| 310 | 
            -
             | 
| 319 | 
            +
                    if next_level > current_level
         | 
| 320 | 
            +
                      if expected_next_level
         | 
| 321 | 
            +
                        unless next_level == expected_next_level || (expected_next_level_alt && next_level == expected_next_level_alt) || expected_next_level < 0
         | 
| 322 | 
            +
                          expected_condition = expected_next_level_alt ? %(expected levels #{expected_next_level_alt} or #{expected_next_level}) : %(expected level #{expected_next_level})
         | 
| 323 | 
            +
                          logger.warn message_with_context %(section title out of sequence: #{expected_condition}, got level #{next_level}), :source_location => reader.cursor
         | 
| 324 | 
            +
                        end
         | 
| 325 | 
            +
                      else
         | 
| 326 | 
            +
                        logger.error message_with_context %(#{sectname} sections do not support nested sections), :source_location => reader.cursor
         | 
| 311 327 | 
             
                      end
         | 
| 312 | 
            -
                      # the attributes returned are those that are orphaned
         | 
| 313 328 | 
             
                      new_section, attributes = next_section reader, section, attributes
         | 
| 314 | 
            -
                      section  | 
| 329 | 
            +
                      section.assign_numeral new_section
         | 
| 330 | 
            +
                      section.blocks << new_section
         | 
| 331 | 
            +
                    elsif next_level == 0 && section == document
         | 
| 332 | 
            +
                      logger.error message_with_context 'level 0 sections can only be used when doctype is book', :source_location => reader.cursor unless book
         | 
| 333 | 
            +
                      new_section, attributes = next_section reader, section, attributes
         | 
| 334 | 
            +
                      section.assign_numeral new_section
         | 
| 335 | 
            +
                      section.blocks << new_section
         | 
| 315 336 | 
             
                    else
         | 
| 316 | 
            -
                      if next_level == 0 && doctype != 'book'
         | 
| 317 | 
            -
                        warn %(asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections)
         | 
| 318 | 
            -
                      end
         | 
| 319 337 | 
             
                      # close this section (and break out of the nesting) to begin a new one
         | 
| 320 338 | 
             
                      break
         | 
| 321 339 | 
             
                    end
         | 
| 322 340 | 
             
                  else
         | 
| 323 341 | 
             
                    # just take one block or else we run the risk of overrunning section boundaries
         | 
| 324 | 
            -
                     | 
| 342 | 
            +
                    block_cursor = reader.cursor
         | 
| 325 343 | 
             
                    if (new_block = next_block reader, intro || section, attributes, :parse_metadata => false)
         | 
| 326 344 | 
             
                      # REVIEW this may be doing too much
         | 
| 327 345 | 
             
                      if part
         | 
| @@ -334,36 +352,32 @@ class Parser | |
| 334 352 | 
             
                              new_block.style = 'partintro'
         | 
| 335 353 | 
             
                            # emulate [partintro] open block
         | 
| 336 354 | 
             
                            else
         | 
| 337 | 
            -
                              intro = Block.new section, :open, :content_model => :compound
         | 
| 355 | 
            +
                              new_block.parent = (intro = Block.new section, :open, :content_model => :compound)
         | 
| 338 356 | 
             
                              intro.style = 'partintro'
         | 
| 339 | 
            -
                               | 
| 340 | 
            -
                              section << intro
         | 
| 357 | 
            +
                              section.blocks << intro
         | 
| 341 358 | 
             
                            end
         | 
| 342 359 | 
             
                          end
         | 
| 343 360 | 
             
                        elsif section.blocks.size == 1
         | 
| 344 361 | 
             
                          first_block = section.blocks[0]
         | 
| 345 362 | 
             
                          # open the [partintro] open block for appending
         | 
| 346 363 | 
             
                          if !intro && first_block.content_model == :compound
         | 
| 347 | 
            -
                             | 
| 348 | 
            -
                            warn %(asciidoctor: ERROR: #{block_line_info}: illegal block content outside of partintro block)
         | 
| 364 | 
            +
                            logger.error message_with_context 'illegal block content outside of partintro block', :source_location => block_cursor
         | 
| 349 365 | 
             
                          # rebuild [partintro] paragraph as an open block
         | 
| 350 366 | 
             
                          elsif first_block.content_model != :compound
         | 
| 351 | 
            -
                            intro = Block.new section, :open, :content_model => :compound
         | 
| 367 | 
            +
                            new_block.parent = (intro = Block.new section, :open, :content_model => :compound)
         | 
| 352 368 | 
             
                            intro.style = 'partintro'
         | 
| 353 369 | 
             
                            section.blocks.shift
         | 
| 354 370 | 
             
                            if first_block.style == 'partintro'
         | 
| 355 371 | 
             
                              first_block.context = :paragraph
         | 
| 356 372 | 
             
                              first_block.style = nil
         | 
| 357 373 | 
             
                            end
         | 
| 358 | 
            -
                            first_block.parent = intro
         | 
| 359 374 | 
             
                            intro << first_block
         | 
| 360 | 
            -
                             | 
| 361 | 
            -
                            section << intro
         | 
| 375 | 
            +
                            section.blocks << intro
         | 
| 362 376 | 
             
                          end
         | 
| 363 377 | 
             
                        end
         | 
| 364 378 | 
             
                      end
         | 
| 365 379 |  | 
| 366 | 
            -
                      (intro || section) << new_block
         | 
| 380 | 
            +
                      (intro || section).blocks << new_block
         | 
| 367 381 | 
             
                      attributes = {}
         | 
| 368 382 | 
             
                    #else
         | 
| 369 383 | 
             
                    #  # don't clear attributes if we don't find a block because they may
         | 
| @@ -376,18 +390,17 @@ class Parser | |
| 376 390 |  | 
| 377 391 | 
             
                if part
         | 
| 378 392 | 
             
                  unless section.blocks? && section.blocks[-1].context == :section
         | 
| 379 | 
            -
                     | 
| 393 | 
            +
                    logger.error message_with_context 'invalid part, must have at least one section (e.g., chapter, appendix, etc.)', :source_location => reader.cursor
         | 
| 380 394 | 
             
                  end
         | 
| 381 395 | 
             
                # NOTE we could try to avoid creating a preamble in the first place, though
         | 
| 382 396 | 
             
                # that would require reworking assumptions in next_section since the preamble
         | 
| 383 397 | 
             
                # is treated like an untitled section
         | 
| 384 398 | 
             
                elsif preamble # implies parent == document
         | 
| 385 399 | 
             
                  if preamble.blocks?
         | 
| 386 | 
            -
                    # unwrap standalone preamble (i.e., no sections), if permissible
         | 
| 387 | 
            -
                     | 
| 400 | 
            +
                    # unwrap standalone preamble (i.e., document has no sections) except for books, if permissible
         | 
| 401 | 
            +
                    unless book || document.blocks[1] || !Compliance.unwrap_standalone_preamble
         | 
| 388 402 | 
             
                      document.blocks.shift
         | 
| 389 403 | 
             
                      while (child_block = preamble.blocks.shift)
         | 
| 390 | 
            -
                        child_block.parent = document
         | 
| 391 404 | 
             
                        document << child_block
         | 
| 392 405 | 
             
                      end
         | 
| 393 406 | 
             
                    end
         | 
| @@ -453,10 +466,9 @@ class Parser | |
| 453 466 | 
             
                end
         | 
| 454 467 |  | 
| 455 468 | 
             
                # QUESTION should we introduce a parsing context object?
         | 
| 456 | 
            -
                 | 
| 457 | 
            -
                 | 
| 469 | 
            +
                reader.mark
         | 
| 470 | 
            +
                this_line, doc_attrs, style = reader.read_line, document.attributes, attributes[1]
         | 
| 458 471 | 
             
                block = block_context = cloaked_context = terminator = nil
         | 
| 459 | 
            -
                style = attributes[1] ? (parse_style_attribute attributes, reader) : nil
         | 
| 460 472 |  | 
| 461 473 | 
             
                if (delimited_block = is_delimited_block? this_line, true)
         | 
| 462 474 | 
             
                  block_context = cloaked_context = delimited_block.context
         | 
| @@ -471,7 +483,7 @@ class Parser | |
| 471 483 | 
             
                    elsif block_extensions && extensions.registered_for_block?(style, block_context)
         | 
| 472 484 | 
             
                      block_context = style.to_sym
         | 
| 473 485 | 
             
                    else
         | 
| 474 | 
            -
                      warn %( | 
| 486 | 
            +
                      logger.warn message_with_context %(invalid style for #{block_context} block: #{style}), :source_location => reader.cursor_at_mark
         | 
| 475 487 | 
             
                      style = block_context.to_s
         | 
| 476 488 | 
             
                    end
         | 
| 477 489 | 
             
                  end
         | 
| @@ -517,24 +529,26 @@ class Parser | |
| 517 529 | 
             
                        break
         | 
| 518 530 | 
             
                      # NOTE very rare that a text-only line will end in ] (e.g., inline macro), so check that first
         | 
| 519 531 | 
             
                      elsif (this_line.end_with? ']') && (this_line.include? '::')
         | 
| 520 | 
            -
                        #if (this_line.start_with? 'image', 'video', 'audio') &&  | 
| 521 | 
            -
                        if (ch0 == 'i' || (this_line.start_with? 'video:', 'audio:')) &&  | 
| 522 | 
            -
                          blk_ctx, target =  | 
| 523 | 
            -
                          block = Block.new | 
| 524 | 
            -
                           | 
| 525 | 
            -
             | 
| 526 | 
            -
                             | 
| 527 | 
            -
             | 
| 528 | 
            -
                             | 
| 529 | 
            -
             | 
| 530 | 
            -
                             | 
| 532 | 
            +
                        #if (this_line.start_with? 'image', 'video', 'audio') && BlockMediaMacroRx =~ this_line
         | 
| 533 | 
            +
                        if (ch0 == 'i' || (this_line.start_with? 'video:', 'audio:')) && BlockMediaMacroRx =~ this_line
         | 
| 534 | 
            +
                          blk_ctx, target, blk_attrs = $1.to_sym, $2, $3
         | 
| 535 | 
            +
                          block = Block.new parent, blk_ctx, :content_model => :empty
         | 
| 536 | 
            +
                          if blk_attrs
         | 
| 537 | 
            +
                            case blk_ctx
         | 
| 538 | 
            +
                            when :video
         | 
| 539 | 
            +
                              posattrs = ['poster', 'width', 'height']
         | 
| 540 | 
            +
                            when :audio
         | 
| 541 | 
            +
                              posattrs = []
         | 
| 542 | 
            +
                            else # :image
         | 
| 543 | 
            +
                              posattrs = ['alt', 'width', 'height']
         | 
| 544 | 
            +
                            end
         | 
| 545 | 
            +
                            block.parse_attributes blk_attrs, posattrs, :sub_input => true, :sub_result => false, :into => attributes
         | 
| 531 546 | 
             
                          end
         | 
| 532 | 
            -
                          block.parse_attributes(match[3], posattrs, :sub_input => true, :sub_result => false, :into => attributes)
         | 
| 533 547 | 
             
                          # style doesn't have special meaning for media macros
         | 
| 534 548 | 
             
                          attributes.delete 'style' if attributes.key? 'style'
         | 
| 535 549 | 
             
                          if (target.include? ATTR_REF_HEAD) && (target = block.sub_attributes target, :attribute_missing => 'drop-line').empty?
         | 
| 536 550 | 
             
                            # retain as unparsed if attribute-missing is skip
         | 
| 537 | 
            -
                            if  | 
| 551 | 
            +
                            if (doc_attrs['attribute-missing'] || Compliance.attribute_missing) == 'skip'
         | 
| 538 552 | 
             
                              return Block.new(parent, :paragraph, :content_model => :simple, :source => [this_line])
         | 
| 539 553 | 
             
                            # otherwise, drop the line
         | 
| 540 554 | 
             
                            else
         | 
| @@ -543,7 +557,7 @@ class Parser | |
| 543 557 | 
             
                            end
         | 
| 544 558 | 
             
                          end
         | 
| 545 559 | 
             
                          if blk_ctx == :image
         | 
| 546 | 
            -
                             | 
| 560 | 
            +
                            document.register :images, target
         | 
| 547 561 | 
             
                            # NOTE style is the value of the first positional attribute in the block attribute line
         | 
| 548 562 | 
             
                            attributes['alt'] ||= style || (attributes['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
         | 
| 549 563 | 
             
                            unless (scaledwidth = attributes.delete 'scaledwidth').nil_or_empty?
         | 
| @@ -556,22 +570,20 @@ class Parser | |
| 556 570 | 
             
                          attributes['target'] = target
         | 
| 557 571 | 
             
                          break
         | 
| 558 572 |  | 
| 559 | 
            -
                        elsif ch0 == 't' && (this_line.start_with? 'toc:') &&  | 
| 560 | 
            -
                          block = Block.new | 
| 561 | 
            -
                          block.parse_attributes( | 
| 573 | 
            +
                        elsif ch0 == 't' && (this_line.start_with? 'toc:') && BlockTocMacroRx =~ this_line
         | 
| 574 | 
            +
                          block = Block.new parent, :toc, :content_model => :empty
         | 
| 575 | 
            +
                          block.parse_attributes($1, [], :sub_result => false, :into => attributes) if $1
         | 
| 562 576 | 
             
                          break
         | 
| 563 577 |  | 
| 564 | 
            -
                        elsif block_macro_extensions &&  | 
| 565 | 
            -
                            (extension = extensions.registered_for_block_macro? | 
| 566 | 
            -
                          target =  | 
| 567 | 
            -
                          content = match[3]
         | 
| 578 | 
            +
                        elsif block_macro_extensions && CustomBlockMacroRx =~ this_line &&
         | 
| 579 | 
            +
                            (extension = extensions.registered_for_block_macro? $1)
         | 
| 580 | 
            +
                          target, content = $2, $3
         | 
| 568 581 | 
             
                          if extension.config[:content_model] == :attributes
         | 
| 569 | 
            -
                             | 
| 570 | 
            -
                              document.parse_attributes | 
| 571 | 
            -
                                  :sub_input => true, :sub_result => false, :into => attributes)
         | 
| 582 | 
            +
                            if content
         | 
| 583 | 
            +
                              document.parse_attributes content, extension.config[:pos_attrs] || [], :sub_input => true, :sub_result => false, :into => attributes
         | 
| 572 584 | 
             
                            end
         | 
| 573 585 | 
             
                          else
         | 
| 574 | 
            -
                            attributes['text'] = content
         | 
| 586 | 
            +
                            attributes['text'] = content || ''
         | 
| 575 587 | 
             
                          end
         | 
| 576 588 | 
             
                          if (default_attrs = extension.config[:default_attrs])
         | 
| 577 589 | 
             
                            attributes.update(default_attrs) {|_, old_v| old_v }
         | 
| @@ -594,23 +606,22 @@ class Parser | |
| 594 606 | 
             
                    block = List.new(parent, :colist)
         | 
| 595 607 | 
             
                    attributes['style'] = 'arabic'
         | 
| 596 608 | 
             
                    reader.unshift_line this_line
         | 
| 597 | 
            -
                     | 
| 609 | 
            +
                    next_index = 1
         | 
| 598 610 | 
             
                    # NOTE skip the match on the first time through as we've already done it (emulates begin...while)
         | 
| 599 | 
            -
                    while match || ( | 
| 600 | 
            -
                      list_item_lineno = reader.lineno
         | 
| 611 | 
            +
                    while match || ((match = CalloutListRx.match reader.peek_line) && reader.mark)
         | 
| 601 612 | 
             
                      # might want to move this check to a validate method
         | 
| 602 | 
            -
                      unless match[1] ==  | 
| 603 | 
            -
                        warn %( | 
| 613 | 
            +
                      unless match[1] == next_index.to_s
         | 
| 614 | 
            +
                        logger.warn message_with_context %(callout list item index: expected #{next_index}, got #{match[1]}), :source_location => reader.cursor_at_mark
         | 
| 604 615 | 
             
                      end
         | 
| 605 616 | 
             
                      if (list_item = next_list_item reader, block, match)
         | 
| 606 | 
            -
                        block << list_item
         | 
| 617 | 
            +
                        block.items << list_item
         | 
| 607 618 | 
             
                        if (coids = document.callouts.callout_ids block.items.size).empty?
         | 
| 608 | 
            -
                          warn %( | 
| 619 | 
            +
                          logger.warn message_with_context %(no callout found for <#{block.items.size}>), :source_location => reader.cursor_at_mark
         | 
| 609 620 | 
             
                        else
         | 
| 610 621 | 
             
                          list_item.attributes['coids'] = coids
         | 
| 611 622 | 
             
                        end
         | 
| 612 623 | 
             
                      end
         | 
| 613 | 
            -
                       | 
| 624 | 
            +
                      next_index += 1
         | 
| 614 625 | 
             
                      match = nil
         | 
| 615 626 | 
             
                    end
         | 
| 616 627 |  | 
| @@ -619,17 +630,16 @@ class Parser | |
| 619 630 |  | 
| 620 631 | 
             
                  elsif UnorderedListRx.match? this_line
         | 
| 621 632 | 
             
                    reader.unshift_line this_line
         | 
| 622 | 
            -
                     | 
| 623 | 
            -
             | 
| 624 | 
            -
             | 
| 625 | 
            -
             | 
| 626 | 
            -
                    end
         | 
| 633 | 
            +
                    if Section === parent && parent.sectname == 'bibliography'
         | 
| 634 | 
            +
                      style = attributes['style'] = 'bibliography'
         | 
| 635 | 
            +
                    end unless style
         | 
| 636 | 
            +
                    block = next_list(reader, :ulist, parent, style)
         | 
| 627 637 | 
             
                    break
         | 
| 628 638 |  | 
| 629 639 | 
             
                  elsif (match = OrderedListRx.match(this_line))
         | 
| 630 640 | 
             
                    reader.unshift_line this_line
         | 
| 631 | 
            -
                    block =  | 
| 632 | 
            -
                    # FIXME move this logic into  | 
| 641 | 
            +
                    block = next_list(reader, :olist, parent)
         | 
| 642 | 
            +
                    # FIXME move this logic into next_list
         | 
| 633 643 | 
             
                    unless style
         | 
| 634 644 | 
             
                      marker = block.items[0].marker
         | 
| 635 645 | 
             
                      if marker.start_with? '.'
         | 
| @@ -651,13 +661,12 @@ class Parser | |
| 651 661 | 
             
                  elsif (style == 'float' || style == 'discrete') && (Compliance.underline_style_section_titles ?
         | 
| 652 662 | 
             
                      (is_section_title? this_line, reader.peek_line) : !indented && (atx_section_title? this_line))
         | 
| 653 663 | 
             
                    reader.unshift_line this_line
         | 
| 654 | 
            -
                    float_id, float_reftext, float_title, float_level | 
| 664 | 
            +
                    float_id, float_reftext, float_title, float_level = parse_section_title reader, document, attributes['id']
         | 
| 655 665 | 
             
                    attributes['reftext'] = float_reftext if float_reftext
         | 
| 656 666 | 
             
                    block = Block.new(parent, :floating_title, :content_model => :empty)
         | 
| 657 667 | 
             
                    block.title = float_title
         | 
| 658 668 | 
             
                    attributes.delete 'title'
         | 
| 659 | 
            -
                    block.id = float_id ||  | 
| 660 | 
            -
                        ((document.attributes.key? 'sectids') ? (Section.generate_id block.title, document) : nil)
         | 
| 669 | 
            +
                    block.id = float_id || ((doc_attrs.key? 'sectids') ? (Section.generate_id block.title, document) : nil)
         | 
| 661 670 | 
             
                    block.level = float_level
         | 
| 662 671 | 
             
                    break
         | 
| 663 672 |  | 
| @@ -683,32 +692,26 @@ class Parser | |
| 683 692 | 
             
                      # advance to block parsing =>
         | 
| 684 693 | 
             
                      break
         | 
| 685 694 | 
             
                    else
         | 
| 686 | 
            -
                      warn %( | 
| 695 | 
            +
                      logger.warn message_with_context %(invalid style for paragraph: #{style}), :source_location => reader.cursor_at_mark
         | 
| 687 696 | 
             
                      style = nil
         | 
| 688 697 | 
             
                      # continue to process paragraph
         | 
| 689 698 | 
             
                    end
         | 
| 690 699 | 
             
                  end
         | 
| 691 700 |  | 
| 692 | 
            -
                  break_at_list = (skipped == 0 && in_list)
         | 
| 693 701 | 
             
                  reader.unshift_line this_line
         | 
| 694 702 |  | 
| 695 703 | 
             
                  # a literal paragraph: contiguous lines starting with at least one whitespace character
         | 
| 696 704 | 
             
                  # NOTE style can only be nil or "normal" at this point
         | 
| 697 705 | 
             
                  if indented && !style
         | 
| 698 | 
            -
                    lines = read_paragraph_lines reader,  | 
| 699 | 
            -
             | 
| 706 | 
            +
                    lines = read_paragraph_lines reader, (in_list = ListItem === parent) && skipped == 0, :skip_line_comments => text_only
         | 
| 700 707 | 
             
                    adjust_indentation! lines
         | 
| 701 | 
            -
             | 
| 702 708 | 
             
                    block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes)
         | 
| 703 709 | 
             
                    # a literal gets special meaning inside of a description list
         | 
| 704 710 | 
             
                    # TODO this feels hacky, better way to distinguish from explicit literal block?
         | 
| 705 711 | 
             
                    block.set_option('listparagraph') if in_list
         | 
| 706 | 
            -
             | 
| 707 712 | 
             
                  # a normal paragraph: contiguous non-blank/non-continuation lines (left-indented or normal style)
         | 
| 708 713 | 
             
                  else
         | 
| 709 | 
            -
                     | 
| 710 | 
            -
                    lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => true
         | 
| 711 | 
            -
             | 
| 714 | 
            +
                    lines = read_paragraph_lines reader, skipped == 0 && ListItem === parent, :skip_line_comments => true
         | 
| 712 715 | 
             
                    # NOTE don't check indented here since it's extremely rare
         | 
| 713 716 | 
             
                    #if text_only || indented
         | 
| 714 717 | 
             
                    if text_only
         | 
| @@ -719,14 +722,12 @@ class Parser | |
| 719 722 | 
             
                    elsif (ADMONITION_STYLE_HEADS.include? ch0) && (this_line.include? ':') && (AdmonitionParagraphRx =~ this_line)
         | 
| 720 723 | 
             
                      lines[0] = $' # string after match
         | 
| 721 724 | 
             
                      attributes['name'] = admonition_name = (attributes['style'] = $1).downcase
         | 
| 722 | 
            -
                      attributes['textlabel'] = (attributes.delete 'caption') ||  | 
| 725 | 
            +
                      attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
         | 
| 723 726 | 
             
                      block = Block.new(parent, :admonition, :content_model => :simple, :source => lines, :attributes => attributes)
         | 
| 724 727 | 
             
                    elsif md_syntax && ch0 == '>' && this_line.start_with?('> ')
         | 
| 725 728 | 
             
                      lines.map! {|line| line == '>' ? line[1..-1] : ((line.start_with? '> ') ? line[2..-1] : line) }
         | 
| 726 729 | 
             
                      if lines[-1].start_with? '-- '
         | 
| 727 | 
            -
                         | 
| 728 | 
            -
                        attributes['attribution'] = attribution if attribution
         | 
| 729 | 
            -
                        attributes['citetitle'] = citetitle if citetitle
         | 
| 730 | 
            +
                        credit_line = (credit_line = lines.pop[3..-1])
         | 
| 730 731 | 
             
                        lines.pop while lines[-1].empty?
         | 
| 731 732 | 
             
                      end
         | 
| 732 733 | 
             
                      attributes['style'] = 'quote'
         | 
| @@ -734,15 +735,21 @@ class Parser | |
| 734 735 | 
             
                      # TODO could assume a discrete heading when inside a block context
         | 
| 735 736 | 
             
                      # FIXME Reader needs to be created w/ line info
         | 
| 736 737 | 
             
                      block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
         | 
| 738 | 
            +
                      if credit_line
         | 
| 739 | 
            +
                        attribution, citetitle = (block.apply_subs credit_line).split ', ', 2
         | 
| 740 | 
            +
                        attributes['attribution'] = attribution if attribution
         | 
| 741 | 
            +
                        attributes['citetitle'] = citetitle if citetitle
         | 
| 742 | 
            +
                      end
         | 
| 737 743 | 
             
                    elsif ch0 == '"' && lines.size > 1 && (lines[-1].start_with? '-- ') && (lines[-2].end_with? '"')
         | 
| 738 744 | 
             
                      lines[0] = this_line[1..-1] # strip leading quote
         | 
| 739 | 
            -
                       | 
| 740 | 
            -
                      attributes['attribution'] = attribution if attribution
         | 
| 741 | 
            -
                      attributes['citetitle'] = citetitle if citetitle
         | 
| 745 | 
            +
                      credit_line = (credit_line = lines.pop).slice(3, credit_line.length)
         | 
| 742 746 | 
             
                      lines.pop while lines[-1].empty?
         | 
| 743 747 | 
             
                      lines[-1] = lines[-1].chop # strip trailing quote
         | 
| 744 748 | 
             
                      attributes['style'] = 'quote'
         | 
| 745 749 | 
             
                      block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes)
         | 
| 750 | 
            +
                      attribution, citetitle = (block.apply_subs credit_line).split ', ', 2
         | 
| 751 | 
            +
                      attributes['attribution'] = attribution if attribution
         | 
| 752 | 
            +
                      attributes['citetitle'] = citetitle if citetitle
         | 
| 746 753 | 
             
                    else
         | 
| 747 754 | 
             
                      # if [normal] is used over an indented paragraph, shift content to left margin
         | 
| 748 755 | 
             
                      # QUESTION do we even need to shift since whitespace is normalized by XML in this case?
         | 
| @@ -765,7 +772,7 @@ class Parser | |
| 765 772 | 
             
                  case block_context
         | 
| 766 773 | 
             
                  when :admonition
         | 
| 767 774 | 
             
                    attributes['name'] = admonition_name = style.downcase
         | 
| 768 | 
            -
                    attributes['textlabel'] = (attributes.delete 'caption') ||  | 
| 775 | 
            +
                    attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
         | 
| 769 776 | 
             
                    block = build_block(block_context, :compound, terminator, parent, reader, attributes)
         | 
| 770 777 |  | 
| 771 778 | 
             
                  when :comment
         | 
| @@ -781,14 +788,14 @@ class Parser | |
| 781 788 |  | 
| 782 789 | 
             
                  when :source
         | 
| 783 790 | 
             
                    AttributeList.rekey attributes, [nil, 'language', 'linenums']
         | 
| 784 | 
            -
                    if  | 
| 785 | 
            -
                      attributes['language'] =  | 
| 791 | 
            +
                    if doc_attrs.key? 'source-language'
         | 
| 792 | 
            +
                      attributes['language'] = doc_attrs['source-language'] || 'text'
         | 
| 786 793 | 
             
                    end unless attributes.key? 'language'
         | 
| 787 | 
            -
                    if (attributes.key? 'linenums-option') || ( | 
| 794 | 
            +
                    if (attributes.key? 'linenums-option') || (doc_attrs.key? 'source-linenums-option')
         | 
| 788 795 | 
             
                      attributes['linenums'] = ''
         | 
| 789 796 | 
             
                    end unless attributes.key? 'linenums'
         | 
| 790 | 
            -
                    if  | 
| 791 | 
            -
                      attributes['indent'] =  | 
| 797 | 
            +
                    if doc_attrs.key? 'source-indent'
         | 
| 798 | 
            +
                      attributes['indent'] = doc_attrs['source-indent']
         | 
| 792 799 | 
             
                    end unless attributes.key? 'indent'
         | 
| 793 800 | 
             
                    block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
         | 
| 794 801 |  | 
| @@ -808,17 +815,17 @@ class Parser | |
| 808 815 | 
             
                      language = language.lstrip
         | 
| 809 816 | 
             
                    end
         | 
| 810 817 | 
             
                    if language.nil_or_empty?
         | 
| 811 | 
            -
                      if  | 
| 812 | 
            -
                        attributes['language'] =  | 
| 818 | 
            +
                      if doc_attrs.key? 'source-language'
         | 
| 819 | 
            +
                        attributes['language'] = doc_attrs['source-language'] || 'text'
         | 
| 813 820 | 
             
                      end
         | 
| 814 821 | 
             
                    else
         | 
| 815 822 | 
             
                      attributes['language'] = language
         | 
| 816 823 | 
             
                    end
         | 
| 817 | 
            -
                    if (attributes.key? 'linenums-option') || ( | 
| 824 | 
            +
                    if (attributes.key? 'linenums-option') || (doc_attrs.key? 'source-linenums-option')
         | 
| 818 825 | 
             
                      attributes['linenums'] = ''
         | 
| 819 826 | 
             
                    end unless attributes.key? 'linenums'
         | 
| 820 | 
            -
                    if  | 
| 821 | 
            -
                      attributes['indent'] =  | 
| 827 | 
            +
                    if doc_attrs.key? 'source-indent'
         | 
| 828 | 
            +
                      attributes['indent'] = doc_attrs['source-indent']
         | 
| 822 829 | 
             
                    end unless attributes.key? 'indent'
         | 
| 823 830 | 
             
                    terminator = terminator.slice 0, 3
         | 
| 824 831 | 
             
                    block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
         | 
| @@ -827,22 +834,15 @@ class Parser | |
| 827 834 | 
             
                    block = build_block(block_context, :raw, terminator, parent, reader, attributes)
         | 
| 828 835 |  | 
| 829 836 | 
             
                  when :stem, :latexmath, :asciimath
         | 
| 830 | 
            -
                    if block_context == :stem
         | 
| 831 | 
            -
                      attributes['style'] = if (explicit_stem_syntax = attributes[2])
         | 
| 832 | 
            -
                        explicit_stem_syntax.include?('tex') ? 'latexmath' : 'asciimath'
         | 
| 833 | 
            -
                      elsif (default_stem_syntax = document.attributes['stem']).nil_or_empty?
         | 
| 834 | 
            -
                        'asciimath'
         | 
| 835 | 
            -
                      else
         | 
| 836 | 
            -
                        default_stem_syntax
         | 
| 837 | 
            -
                      end
         | 
| 838 | 
            -
                    end
         | 
| 837 | 
            +
                    attributes['style'] = STEM_TYPE_ALIASES[attributes[2] || doc_attrs['stem']] if block_context == :stem
         | 
| 839 838 | 
             
                    block = build_block(:stem, :raw, terminator, parent, reader, attributes)
         | 
| 840 839 |  | 
| 841 840 | 
             
                  when :open, :sidebar
         | 
| 842 841 | 
             
                    block = build_block(block_context, :compound, terminator, parent, reader, attributes)
         | 
| 843 842 |  | 
| 844 843 | 
             
                  when :table
         | 
| 845 | 
            -
                     | 
| 844 | 
            +
                    block_cursor = reader.cursor
         | 
| 845 | 
            +
                    block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true, :context => :table, :cursor => :at_mark), block_cursor
         | 
| 846 846 | 
             
                    # NOTE it's very rare that format is set when using a format hint char, so short-circuit
         | 
| 847 847 | 
             
                    unless terminator.start_with? '|', '!'
         | 
| 848 848 | 
             
                      # NOTE infer dsv once all other format hint chars are ruled out
         | 
| @@ -873,32 +873,29 @@ class Parser | |
| 873 873 | 
             
                      end
         | 
| 874 874 | 
             
                    else
         | 
| 875 875 | 
             
                      # this should only happen if there's a misconfiguration
         | 
| 876 | 
            -
                      raise %(Unsupported block type #{block_context} at #{reader. | 
| 876 | 
            +
                      raise %(Unsupported block type #{block_context} at #{reader.cursor})
         | 
| 877 877 | 
             
                    end
         | 
| 878 878 | 
             
                  end
         | 
| 879 879 | 
             
                end
         | 
| 880 880 |  | 
| 881 881 | 
             
                # FIXME we've got to clean this up, it's horrible!
         | 
| 882 | 
            -
                block.source_location =  | 
| 882 | 
            +
                block.source_location = reader.cursor_at_mark if document.sourcemap
         | 
| 883 883 | 
             
                # FIXME title should be assigned when block is constructed
         | 
| 884 884 | 
             
                block.title = attributes.delete 'title' if attributes.key? 'title'
         | 
| 885 | 
            -
                #unless attributes.key? 'reftext'
         | 
| 886 | 
            -
                #  attributes['reftext'] = document.attributes['reftext'] if document.attributes.key? 'reftext'
         | 
| 887 | 
            -
                #end
         | 
| 888 885 | 
             
                # TODO eventually remove the style attribute from the attributes hash
         | 
| 889 886 | 
             
                #block.style = attributes.delete 'style'
         | 
| 890 887 | 
             
                block.style = attributes['style']
         | 
| 891 888 | 
             
                if (block_id = (block.id ||= attributes['id']))
         | 
| 892 889 | 
             
                  unless document.register :refs, [block_id, block, attributes['reftext'] || (block.title? ? block.title : nil)]
         | 
| 893 | 
            -
                    warn %( | 
| 890 | 
            +
                    logger.warn message_with_context %(id assigned to block already in use: #{block_id}), :source_location => reader.cursor_at_mark
         | 
| 894 891 | 
             
                  end
         | 
| 895 892 | 
             
                end
         | 
| 896 893 | 
             
                # FIXME remove the need for this update!
         | 
| 897 894 | 
             
                block.attributes.update(attributes) unless attributes.empty?
         | 
| 898 895 | 
             
                block.lock_in_subs
         | 
| 899 896 |  | 
| 900 | 
            -
                #if  | 
| 901 | 
            -
                #   | 
| 897 | 
            +
                #if doc_attrs.key? :pending_attribute_entries
         | 
| 898 | 
            +
                #  doc_attrs.delete(:pending_attribute_entries).each do |entry|
         | 
| 902 899 | 
             
                #    entry.save_to block.attributes
         | 
| 903 900 | 
             
                #  end
         | 
| 904 901 | 
             
                #end
         | 
| @@ -1015,7 +1012,7 @@ class Parser | |
| 1015 1012 | 
             
                  end
         | 
| 1016 1013 | 
             
                  block_reader = nil
         | 
| 1017 1014 | 
             
                elsif parse_as_content_model != :compound
         | 
| 1018 | 
            -
                  lines = reader.read_lines_until :terminator => terminator, :skip_processing => skip_processing
         | 
| 1015 | 
            +
                  lines = reader.read_lines_until :terminator => terminator, :skip_processing => skip_processing, :context => block_context, :cursor => :at_mark
         | 
| 1019 1016 | 
             
                  block_reader = nil
         | 
| 1020 1017 | 
             
                # terminator is false when reader has already been prepared
         | 
| 1021 1018 | 
             
                elsif terminator == false
         | 
| @@ -1023,7 +1020,8 @@ class Parser | |
| 1023 1020 | 
             
                  block_reader = reader
         | 
| 1024 1021 | 
             
                else
         | 
| 1025 1022 | 
             
                  lines = nil
         | 
| 1026 | 
            -
                   | 
| 1023 | 
            +
                  block_cursor = reader.cursor
         | 
| 1024 | 
            +
                  block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing, :context => block_context, :cursor => :at_mark), block_cursor
         | 
| 1027 1025 | 
             
                end
         | 
| 1028 1026 |  | 
| 1029 1027 | 
             
                if content_model == :verbatim
         | 
| @@ -1081,27 +1079,24 @@ class Parser | |
| 1081 1079 | 
             
              #
         | 
| 1082 1080 | 
             
              # Returns nothing.
         | 
| 1083 1081 | 
             
              def self.parse_blocks(reader, parent)
         | 
| 1084 | 
            -
                while ((block = next_block reader, parent) && parent << block) || reader.has_more_lines?
         | 
| 1082 | 
            +
                while ((block = next_block reader, parent) && parent.blocks << block) || reader.has_more_lines?
         | 
| 1085 1083 | 
             
                end
         | 
| 1086 1084 | 
             
              end
         | 
| 1087 1085 |  | 
| 1088 | 
            -
              # Internal: Parse and construct an  | 
| 1086 | 
            +
              # Internal: Parse and construct an ordered or unordered list at the current position of the Reader
         | 
| 1089 1087 | 
             
              #
         | 
| 1090 | 
            -
              # reader    - The Reader from which to retrieve the  | 
| 1088 | 
            +
              # reader    - The Reader from which to retrieve the list
         | 
| 1091 1089 | 
             
              # list_type - A Symbol representing the list type (:olist for ordered, :ulist for unordered)
         | 
| 1092 | 
            -
              # parent    - The parent Block to which this  | 
| 1090 | 
            +
              # parent    - The parent Block to which this list belongs
         | 
| 1091 | 
            +
              # style     - The block style assigned to this list (optional, default: nil)
         | 
| 1093 1092 | 
             
              #
         | 
| 1094 | 
            -
              # Returns the Block encapsulating the parsed  | 
| 1095 | 
            -
              def self. | 
| 1093 | 
            +
              # Returns the Block encapsulating the parsed unordered or ordered list
         | 
| 1094 | 
            +
              def self.next_list(reader, list_type, parent, style = nil)
         | 
| 1096 1095 | 
             
                list_block = List.new(parent, list_type)
         | 
| 1097 | 
            -
                 | 
| 1098 | 
            -
                  list_block.level = parent.level + 1
         | 
| 1099 | 
            -
                else
         | 
| 1100 | 
            -
                  list_block.level = 1
         | 
| 1101 | 
            -
                end
         | 
| 1096 | 
            +
                list_block.level = parent.context == list_type ? (parent.level + 1) : 1
         | 
| 1102 1097 |  | 
| 1103 | 
            -
                while reader.has_more_lines? &&  | 
| 1104 | 
            -
                  marker = resolve_list_marker(list_type,  | 
| 1098 | 
            +
                while reader.has_more_lines? && ListRxMap[list_type] =~ reader.peek_line
         | 
| 1099 | 
            +
                  match, marker = $~, resolve_list_marker(list_type, $1)
         | 
| 1105 1100 |  | 
| 1106 1101 | 
             
                  # if we are moving to the next item, and the marker is different
         | 
| 1107 1102 | 
             
                  # determine if we are moving up or down in nesting
         | 
| @@ -1122,7 +1117,7 @@ class Parser | |
| 1122 1117 | 
             
                  end
         | 
| 1123 1118 |  | 
| 1124 1119 | 
             
                  if !list_block.items? || this_item_level == list_block.level
         | 
| 1125 | 
            -
                    list_item = next_list_item(reader, list_block, match)
         | 
| 1120 | 
            +
                    list_item = next_list_item(reader, list_block, match, nil, style)
         | 
| 1126 1121 | 
             
                  elsif this_item_level < list_block.level
         | 
| 1127 1122 | 
             
                    # leave this block
         | 
| 1128 1123 | 
             
                    break
         | 
| @@ -1132,7 +1127,7 @@ class Parser | |
| 1132 1127 | 
             
                    list_block.items[-1] << next_block(reader, list_block)
         | 
| 1133 1128 | 
             
                  end
         | 
| 1134 1129 |  | 
| 1135 | 
            -
                  list_block << list_item if list_item
         | 
| 1130 | 
            +
                  list_block.items << list_item if list_item
         | 
| 1136 1131 | 
             
                  list_item = nil
         | 
| 1137 1132 |  | 
| 1138 1133 | 
             
                  reader.skip_blank_lines || break
         | 
| @@ -1159,6 +1154,25 @@ class Parser | |
| 1159 1154 | 
             
                found
         | 
| 1160 1155 | 
             
              end
         | 
| 1161 1156 |  | 
| 1157 | 
            +
              # Internal: Catalog a matched inline anchor.
         | 
| 1158 | 
            +
              #
         | 
| 1159 | 
            +
              # id       - The String id of the anchor
         | 
| 1160 | 
            +
              # reftext  - The optional String reference text of the anchor
         | 
| 1161 | 
            +
              # node     - The AbstractNode parent node of the anchor node
         | 
| 1162 | 
            +
              # location - The source location (file and line) where the anchor was found
         | 
| 1163 | 
            +
              # doc      - The document to which the node belongs; computed from node if not specified
         | 
| 1164 | 
            +
              #
         | 
| 1165 | 
            +
              # Returns nothing
         | 
| 1166 | 
            +
              def self.catalog_inline_anchor id, reftext, node, location, doc = nil
         | 
| 1167 | 
            +
                doc ||= node.document
         | 
| 1168 | 
            +
                reftext = doc.sub_attributes reftext if reftext && (reftext.include? ATTR_REF_HEAD)
         | 
| 1169 | 
            +
                unless doc.register :refs, [id, (Inline.new node, :anchor, reftext, :type => :ref, :id => id), reftext]
         | 
| 1170 | 
            +
                  location = location.cursor if Reader === location
         | 
| 1171 | 
            +
                  logger.warn message_with_context %(id assigned to anchor already in use: #{id}), :source_location => location
         | 
| 1172 | 
            +
                end
         | 
| 1173 | 
            +
                nil
         | 
| 1174 | 
            +
              end
         | 
| 1175 | 
            +
             | 
| 1162 1176 | 
             
              # Internal: Catalog any inline anchors found in the text (but don't convert)
         | 
| 1163 1177 | 
             
              #
         | 
| 1164 1178 | 
             
              # text     - The String text in which to look for inline anchors
         | 
| @@ -1180,7 +1194,7 @@ class Parser | |
| 1180 1194 | 
             
                    end
         | 
| 1181 1195 | 
             
                  end
         | 
| 1182 1196 | 
             
                  unless document.register :refs, [id, (Inline.new block, :anchor, reftext, :type => :ref, :id => id), reftext]
         | 
| 1183 | 
            -
                    warn %( | 
| 1197 | 
            +
                    logger.warn message_with_context %(id assigned to anchor already in use: #{id}), :source_location => document.reader.cursor_at_prev_line
         | 
| 1184 1198 | 
             
                  end
         | 
| 1185 1199 | 
             
                end if (text.include? '[[') || (text.include? 'or:')
         | 
| 1186 1200 | 
             
                nil
         | 
| @@ -1188,17 +1202,16 @@ class Parser | |
| 1188 1202 |  | 
| 1189 1203 | 
             
              # Internal: Catalog the bibliography inline anchor found in the start of the list item (but don't convert)
         | 
| 1190 1204 | 
             
              #
         | 
| 1191 | 
            -
              #  | 
| 1192 | 
            -
              #  | 
| 1193 | 
            -
              #  | 
| 1205 | 
            +
              # id      - The String id of the anchor
         | 
| 1206 | 
            +
              # reftext - The optional String reference text of the anchor
         | 
| 1207 | 
            +
              # node    - The AbstractNode parent node of the anchor node
         | 
| 1208 | 
            +
              # reader  - The source Reader for the current Document, positioned at the current list item
         | 
| 1194 1209 | 
             
              #
         | 
| 1195 1210 | 
             
              # Returns nothing
         | 
| 1196 | 
            -
              def self.catalog_inline_biblio_anchor  | 
| 1197 | 
            -
                 | 
| 1198 | 
            -
             | 
| 1199 | 
            -
                   | 
| 1200 | 
            -
                    warn %(asciidoctor: WARNING: #{document.reader.path}: id assigned to bibliography anchor already in use: #{id})
         | 
| 1201 | 
            -
                  end
         | 
| 1211 | 
            +
              def self.catalog_inline_biblio_anchor id, reftext, node, reader
         | 
| 1212 | 
            +
                # QUESTION should we sub attributes in reftext (like with regular anchors)?
         | 
| 1213 | 
            +
                unless node.document.register :refs, [id, (Inline.new node, :anchor, (styled_reftext = %([#{reftext || id}])), :type => :bibref, :id => id), styled_reftext]
         | 
| 1214 | 
            +
                  logger.warn message_with_context %(id assigned to bibliography anchor already in use: #{id}), :source_location => reader.cursor
         | 
| 1202 1215 | 
             
                end
         | 
| 1203 1216 | 
             
                nil
         | 
| 1204 1217 | 
             
              end
         | 
| @@ -1220,12 +1233,10 @@ class Parser | |
| 1220 1233 | 
             
                # NOTE skip the match on the first time through as we've already done it (emulates begin...while)
         | 
| 1221 1234 | 
             
                while match || (reader.has_more_lines? && (match = sibling_pattern.match(reader.peek_line)))
         | 
| 1222 1235 | 
             
                  term, item = next_list_item(reader, list_block, match, sibling_pattern)
         | 
| 1223 | 
            -
                  if previous_pair && !previous_pair[ | 
| 1224 | 
            -
                    previous_pair.pop
         | 
| 1236 | 
            +
                  if previous_pair && !previous_pair[1]
         | 
| 1225 1237 | 
             
                    previous_pair[0] << term
         | 
| 1226 | 
            -
                    previous_pair  | 
| 1238 | 
            +
                    previous_pair[1] = item
         | 
| 1227 1239 | 
             
                  else
         | 
| 1228 | 
            -
                    # FIXME this misses the automatic parent assignment
         | 
| 1229 1240 | 
             
                    list_block.items << (previous_pair = [[term], item])
         | 
| 1230 1241 | 
             
                  end
         | 
| 1231 1242 | 
             
                  match = nil
         | 
| @@ -1234,9 +1245,9 @@ class Parser | |
| 1234 1245 | 
             
                list_block
         | 
| 1235 1246 | 
             
              end
         | 
| 1236 1247 |  | 
| 1237 | 
            -
              # Internal: Parse and construct the next ListItem for the current  | 
| 1238 | 
            -
              # (unordered or  | 
| 1239 | 
            -
              #  | 
| 1248 | 
            +
              # Internal: Parse and construct the next ListItem for the current list Block
         | 
| 1249 | 
            +
              # (unordered, ordered, or callout list) or the term ListItem and description
         | 
| 1250 | 
            +
              # ListItem pair for the description list Block.
         | 
| 1240 1251 | 
             
              #
         | 
| 1241 1252 | 
             
              # First collect and process all the lines that constitute the next list
         | 
| 1242 1253 | 
             
              # item for the parent list (according to its type). Next, parse those lines
         | 
| @@ -1247,48 +1258,65 @@ class Parser | |
| 1247 1258 | 
             
              # reader        - The Reader from which to retrieve the next list item
         | 
| 1248 1259 | 
             
              # list_block    - The parent list Block of this ListItem. Also provides access to the list type.
         | 
| 1249 1260 | 
             
              # match         - The match Array which contains the marker and text (first-line) of the ListItem
         | 
| 1250 | 
            -
              # sibling_trait - The list marker or the Regexp to match a sibling item
         | 
| 1261 | 
            +
              # sibling_trait - The list marker or the Regexp to match a sibling item (optional, default: nil)
         | 
| 1262 | 
            +
              # style         - The block style assigned to this list (optional, default: nil)
         | 
| 1251 1263 | 
             
              #
         | 
| 1252 1264 | 
             
              # Returns the next ListItem or ListItem pair (depending on the list type)
         | 
| 1253 1265 | 
             
              # for the parent list Block.
         | 
| 1254 | 
            -
              def self.next_list_item(reader, list_block, match, sibling_trait = nil)
         | 
| 1266 | 
            +
              def self.next_list_item(reader, list_block, match, sibling_trait = nil, style = nil)
         | 
| 1255 1267 | 
             
                if (list_type = list_block.context) == :dlist
         | 
| 1256 | 
            -
                   | 
| 1257 | 
            -
                   | 
| 1258 | 
            -
                   | 
| 1259 | 
            -
             | 
| 1260 | 
            -
                   | 
| 1261 | 
            -
                   | 
| 1262 | 
            -
                   | 
| 1263 | 
            -
                  if  | 
| 1264 | 
            -
                     | 
| 1265 | 
            -
             | 
| 1266 | 
            -
                       | 
| 1267 | 
            -
             | 
| 1268 | 
            -
             | 
| 1269 | 
            -
                      checkbox = true
         | 
| 1270 | 
            -
                      checked = true
         | 
| 1271 | 
            -
                      text = text[3..-1].lstrip
         | 
| 1268 | 
            +
                  dlist = true
         | 
| 1269 | 
            +
                  list_term = ListItem.new(list_block, (term_text = match[1]))
         | 
| 1270 | 
            +
                  if term_text.start_with?('[[') && LeadingInlineAnchorRx =~ term_text
         | 
| 1271 | 
            +
                    catalog_inline_anchor $1, ($2 || $'.lstrip), list_term, reader
         | 
| 1272 | 
            +
                  end
         | 
| 1273 | 
            +
                  has_text = true if (item_text = match[3])
         | 
| 1274 | 
            +
                  list_item = ListItem.new(list_block, item_text)
         | 
| 1275 | 
            +
                  if list_block.document.sourcemap
         | 
| 1276 | 
            +
                    list_term.source_location = reader.cursor
         | 
| 1277 | 
            +
                    if has_text
         | 
| 1278 | 
            +
                      list_item.source_location = list_term.source_location
         | 
| 1279 | 
            +
                    else
         | 
| 1280 | 
            +
                      sourcemap_assignment_deferred = true
         | 
| 1272 1281 | 
             
                    end
         | 
| 1273 1282 | 
             
                  end
         | 
| 1274 | 
            -
             | 
| 1275 | 
            -
             | 
| 1276 | 
            -
                  if checkbox
         | 
| 1277 | 
            -
                    # FIXME checklist never makes it into the options attribute
         | 
| 1278 | 
            -
                    list_block.attributes['checklist-option'] = ''
         | 
| 1279 | 
            -
                    list_item.attributes['checkbox'] = ''
         | 
| 1280 | 
            -
                    list_item.attributes['checked'] = '' if checked
         | 
| 1281 | 
            -
                  end
         | 
| 1282 | 
            -
             | 
| 1283 | 
            -
                  sibling_trait ||= resolve_list_marker(list_type, match[1], list_block.items.size, true, reader)
         | 
| 1284 | 
            -
                  list_item.marker = sibling_trait
         | 
| 1283 | 
            +
                else
         | 
| 1285 1284 | 
             
                  has_text = true
         | 
| 1285 | 
            +
                  list_item = ListItem.new(list_block, (item_text = match[2]))
         | 
| 1286 | 
            +
                  list_item.source_location = reader.cursor if list_block.document.sourcemap
         | 
| 1287 | 
            +
                  if list_type == :ulist
         | 
| 1288 | 
            +
                    list_item.marker = (sibling_trait ||= match[1])
         | 
| 1289 | 
            +
                    if item_text.start_with?('[')
         | 
| 1290 | 
            +
                      if style && style == 'bibliography'
         | 
| 1291 | 
            +
                        if InlineBiblioAnchorRx =~ item_text
         | 
| 1292 | 
            +
                          catalog_inline_biblio_anchor $1, $2, list_item, reader
         | 
| 1293 | 
            +
                        end
         | 
| 1294 | 
            +
                      elsif item_text.start_with?('[[')
         | 
| 1295 | 
            +
                        if LeadingInlineAnchorRx =~ item_text
         | 
| 1296 | 
            +
                          catalog_inline_anchor $1, $2, list_item, reader
         | 
| 1297 | 
            +
                        end
         | 
| 1298 | 
            +
                      elsif item_text.start_with?('[ ] ', '[x] ', '[*] ')
         | 
| 1299 | 
            +
                        # FIXME next_block wipes out update to options attribute
         | 
| 1300 | 
            +
                        #list_block.set_option 'checklist' unless list_block.attributes['checklist-option']
         | 
| 1301 | 
            +
                        list_block.attributes['checklist-option'] = ''
         | 
| 1302 | 
            +
                        list_item.attributes['checkbox'] = ''
         | 
| 1303 | 
            +
                        list_item.attributes['checked'] = '' unless item_text.start_with? '[ '
         | 
| 1304 | 
            +
                        list_item.text = item_text.slice(4, item_text.length)
         | 
| 1305 | 
            +
                      end
         | 
| 1306 | 
            +
                    end
         | 
| 1307 | 
            +
                  elsif list_type == :olist
         | 
| 1308 | 
            +
                    list_item.marker = (sibling_trait ||= resolve_ordered_list_marker(match[1], list_block.items.size, true, reader))
         | 
| 1309 | 
            +
                  else # :colist
         | 
| 1310 | 
            +
                    list_item.marker = (sibling_trait ||= '<1>')
         | 
| 1311 | 
            +
                  end
         | 
| 1286 1312 | 
             
                end
         | 
| 1287 1313 |  | 
| 1288 | 
            -
                # first skip the line with the marker / term
         | 
| 1314 | 
            +
                # first skip the line with the marker / term (it gets put back onto the reader by next_block)
         | 
| 1289 1315 | 
             
                reader.shift
         | 
| 1290 | 
            -
                 | 
| 1316 | 
            +
                block_cursor = reader.cursor
         | 
| 1317 | 
            +
                list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), block_cursor
         | 
| 1291 1318 | 
             
                if list_item_reader.has_more_lines?
         | 
| 1319 | 
            +
                  list_item.source_location = block_cursor if sourcemap_assignment_deferred
         | 
| 1292 1320 | 
             
                  # NOTE peek on the other side of any comment lines
         | 
| 1293 1321 | 
             
                  comment_lines = list_item_reader.skip_line_comments
         | 
| 1294 1322 | 
             
                  if (subsequent_line = list_item_reader.peek_line)
         | 
| @@ -1297,8 +1325,8 @@ class Parser | |
| 1297 1325 | 
             
                      content_adjacent = false
         | 
| 1298 1326 | 
             
                    else
         | 
| 1299 1327 | 
             
                      content_adjacent = true
         | 
| 1300 | 
            -
                      # treat lines as paragraph text if continuation does not connect first block (i.e., has_text =  | 
| 1301 | 
            -
                      has_text =  | 
| 1328 | 
            +
                      # treat lines as paragraph text if continuation does not connect first block (i.e., has_text = nil)
         | 
| 1329 | 
            +
                      has_text = nil unless dlist
         | 
| 1302 1330 | 
             
                    end
         | 
| 1303 1331 | 
             
                  else
         | 
| 1304 1332 | 
             
                    # NOTE we have no use for any trailing comment lines we might have found
         | 
| @@ -1306,24 +1334,22 @@ class Parser | |
| 1306 1334 | 
             
                    content_adjacent = false
         | 
| 1307 1335 | 
             
                  end
         | 
| 1308 1336 |  | 
| 1309 | 
            -
                  # only  | 
| 1310 | 
            -
                   | 
| 1337 | 
            +
                  # reader is confined to boundaries of list, which means only blocks will be found (no sections)
         | 
| 1338 | 
            +
                  if (block = next_block(list_item_reader, list_item, (attrs = {}), :text => !has_text))
         | 
| 1339 | 
            +
                    list_item.blocks << block
         | 
| 1340 | 
            +
                  end
         | 
| 1311 1341 |  | 
| 1312 | 
            -
                   | 
| 1313 | 
            -
             | 
| 1314 | 
            -
             | 
| 1315 | 
            -
             | 
| 1342 | 
            +
                  while list_item_reader.has_more_lines?
         | 
| 1343 | 
            +
                    if (block = next_block(list_item_reader, list_item, attrs))
         | 
| 1344 | 
            +
                      list_item.blocks << block
         | 
| 1345 | 
            +
                    end
         | 
| 1316 1346 | 
             
                  end
         | 
| 1317 1347 |  | 
| 1318 1348 | 
             
                  list_item.fold_first(continuation_connects_first_block, content_adjacent)
         | 
| 1319 1349 | 
             
                end
         | 
| 1320 1350 |  | 
| 1321 | 
            -
                if  | 
| 1322 | 
            -
                   | 
| 1323 | 
            -
                    [list_term, list_item]
         | 
| 1324 | 
            -
                  else
         | 
| 1325 | 
            -
                    [list_term, nil]
         | 
| 1326 | 
            -
                  end
         | 
| 1351 | 
            +
                if dlist
         | 
| 1352 | 
            +
                  list_item.text? || list_item.blocks? ? [list_term, list_item] : [list_term]
         | 
| 1327 1353 | 
             
                else
         | 
| 1328 1354 | 
             
                  list_item
         | 
| 1329 1355 | 
             
                end
         | 
| @@ -1396,7 +1422,7 @@ class Parser | |
| 1396 1422 | 
             
                      buffer << this_line
         | 
| 1397 1423 | 
             
                      # grab all the lines in the block, leaving the delimiters in place
         | 
| 1398 1424 | 
             
                      # we're being more strict here about the terminator, but I think that's a good thing
         | 
| 1399 | 
            -
                      buffer.concat reader.read_lines_until(:terminator => match.terminator, :read_last_line => true)
         | 
| 1425 | 
            +
                      buffer.concat reader.read_lines_until(:terminator => match.terminator, :read_last_line => true, :context => nil)
         | 
| 1400 1426 | 
             
                      continuation = :inactive
         | 
| 1401 1427 | 
             
                    else
         | 
| 1402 1428 | 
             
                      break
         | 
| @@ -1528,58 +1554,57 @@ class Parser | |
| 1528 1554 | 
             
              # reader     - the source reader
         | 
| 1529 1555 | 
             
              # parent     - the parent Section or Document of this Section
         | 
| 1530 1556 | 
             
              # attributes - a Hash of attributes to assign to this section (default: {})
         | 
| 1557 | 
            +
              #
         | 
| 1558 | 
            +
              # Returns the section [Block]
         | 
| 1531 1559 | 
             
              def self.initialize_section reader, parent, attributes = {}
         | 
| 1532 1560 | 
             
                document = parent.document
         | 
| 1561 | 
            +
                book = (doctype = document.doctype) == 'book'
         | 
| 1533 1562 | 
             
                source_location = reader.cursor if document.sourcemap
         | 
| 1534 | 
            -
                 | 
| 1563 | 
            +
                sect_style = attributes[1]
         | 
| 1564 | 
            +
                sect_id, sect_reftext, sect_title, sect_level, sect_atx = parse_section_title reader, document, attributes['id']
         | 
| 1565 | 
            +
             | 
| 1535 1566 | 
             
                if sect_reftext
         | 
| 1536 1567 | 
             
                  attributes['reftext'] = sect_reftext
         | 
| 1537 | 
            -
                 | 
| 1568 | 
            +
                else
         | 
| 1538 1569 | 
             
                  sect_reftext = attributes['reftext']
         | 
| 1539 | 
            -
                #elsif document.attributes.key? 'reftext'
         | 
| 1540 | 
            -
                #  sect_reftext = attributes['reftext'] = document.attributes['reftext']
         | 
| 1541 1570 | 
             
                end
         | 
| 1542 1571 |  | 
| 1543 | 
            -
                 | 
| 1544 | 
            -
             | 
| 1545 | 
            -
                if style
         | 
| 1546 | 
            -
                  if style == 'abstract' && document.doctype == 'book'
         | 
| 1572 | 
            +
                if sect_style
         | 
| 1573 | 
            +
                  if book && sect_style == 'abstract'
         | 
| 1547 1574 | 
             
                    sect_name, sect_level = 'chapter', 1
         | 
| 1548 1575 | 
             
                  else
         | 
| 1549 | 
            -
                    sect_name, sect_special =  | 
| 1576 | 
            +
                    sect_name, sect_special = sect_style, true
         | 
| 1550 1577 | 
             
                    sect_level = 1 if sect_level == 0
         | 
| 1551 | 
            -
                     | 
| 1578 | 
            +
                    sect_numbered = sect_style == 'appendix'
         | 
| 1552 1579 | 
             
                  end
         | 
| 1580 | 
            +
                elsif book
         | 
| 1581 | 
            +
                  sect_name = sect_level == 0 ? 'part' : (sect_level > 1 ? 'section' : 'chapter')
         | 
| 1582 | 
            +
                elsif doctype == 'manpage' && (sect_title.casecmp 'synopsis') == 0
         | 
| 1583 | 
            +
                  sect_name, sect_special = 'synopsis', true
         | 
| 1553 1584 | 
             
                else
         | 
| 1554 | 
            -
                   | 
| 1555 | 
            -
                  when 'book'
         | 
| 1556 | 
            -
                    sect_name = sect_level == 0 ? 'part' : (sect_level == 1 ? 'chapter' : 'section')
         | 
| 1557 | 
            -
                  when 'manpage'
         | 
| 1558 | 
            -
                    if (sect_title.casecmp 'synopsis') == 0
         | 
| 1559 | 
            -
                      sect_name, sect_special = 'synopsis', true
         | 
| 1560 | 
            -
                    else
         | 
| 1561 | 
            -
                      sect_name = 'section'
         | 
| 1562 | 
            -
                    end
         | 
| 1563 | 
            -
                  else
         | 
| 1564 | 
            -
                    sect_name = 'section'
         | 
| 1565 | 
            -
                  end
         | 
| 1585 | 
            +
                  sect_name = 'section'
         | 
| 1566 1586 | 
             
                end
         | 
| 1567 1587 |  | 
| 1568 | 
            -
                section = Section.new parent, sect_level | 
| 1588 | 
            +
                section = Section.new parent, sect_level
         | 
| 1569 1589 | 
             
                section.id, section.title, section.sectname, section.source_location = sect_id, sect_title, sect_name, source_location
         | 
| 1570 | 
            -
                # TODO honor special section numbering option (#661)
         | 
| 1571 1590 | 
             
                if sect_special
         | 
| 1572 1591 | 
             
                  section.special = true
         | 
| 1573 | 
            -
                   | 
| 1574 | 
            -
             | 
| 1575 | 
            -
                   | 
| 1592 | 
            +
                  if sect_numbered
         | 
| 1593 | 
            +
                    section.numbered = true
         | 
| 1594 | 
            +
                  elsif document.attributes['sectnums'] == 'all'
         | 
| 1595 | 
            +
                    section.numbered = book && sect_level == 1 ? :chapter : true
         | 
| 1596 | 
            +
                  end
         | 
| 1597 | 
            +
                elsif document.attributes['sectnums'] && sect_level > 0
         | 
| 1598 | 
            +
                  # NOTE a special section here is guaranteed to be nested in another section
         | 
| 1599 | 
            +
                  section.numbered = section.special ? parent.numbered && true : true
         | 
| 1600 | 
            +
                elsif book && sect_level == 0 && document.attributes['partnums']
         | 
| 1601 | 
            +
                  section.numbered = true
         | 
| 1576 1602 | 
             
                end
         | 
| 1577 1603 |  | 
| 1578 1604 | 
             
                # generate an ID if one was not embedded or specified as anchor above section title
         | 
| 1579 | 
            -
                if (id = section.id ||= (attributes | 
| 1580 | 
            -
                    ((document.attributes.key? 'sectids') ? (Section.generate_id section.title, document) : nil)))
         | 
| 1605 | 
            +
                if (id = section.id ||= ((document.attributes.key? 'sectids') ? (Section.generate_id section.title, document) : nil))
         | 
| 1581 1606 | 
             
                  unless document.register :refs, [id, section, sect_reftext || section.title]
         | 
| 1582 | 
            -
                    warn %( | 
| 1607 | 
            +
                    logger.warn message_with_context %(id assigned to section already in use: #{id}), :source_location => (reader.cursor_at_line reader.lineno - (sect_atx ? 1 : 2))
         | 
| 1583 1608 | 
             
                  end
         | 
| 1584 1609 | 
             
                end
         | 
| 1585 1610 |  | 
| @@ -1596,12 +1621,13 @@ class Parser | |
| 1596 1621 | 
             
              #
         | 
| 1597 1622 | 
             
              # Returns the Integer section level if the Reader is positioned at a section title or nil otherwise
         | 
| 1598 1623 | 
             
              def self.is_next_line_section?(reader, attributes)
         | 
| 1599 | 
            -
                if (style = attributes[1]) && (style | 
| 1624 | 
            +
                if (style = attributes[1]) && (style == 'discrete' || style == 'float')
         | 
| 1600 1625 | 
             
                  return
         | 
| 1601 | 
            -
                elsif  | 
| 1602 | 
            -
                   | 
| 1603 | 
            -
             | 
| 1604 | 
            -
             | 
| 1626 | 
            +
                elsif Compliance.underline_style_section_titles
         | 
| 1627 | 
            +
                  next_lines = reader.peek_lines 2, style && style == 'comment'
         | 
| 1628 | 
            +
                  is_section_title?(next_lines[0] || '', next_lines[1])
         | 
| 1629 | 
            +
                else
         | 
| 1630 | 
            +
                  atx_section_title?(reader.peek_line || '')
         | 
| 1605 1631 | 
             
                end
         | 
| 1606 1632 | 
             
              end
         | 
| 1607 1633 |  | 
| @@ -1701,8 +1727,8 @@ class Parser | |
| 1701 1727 | 
             
              # Returns an 5-element [Array] containing the id (String), reftext (String),
         | 
| 1702 1728 | 
             
              # title (String), level (Integer), and flag (Boolean) indicating whether an
         | 
| 1703 1729 | 
             
              # atx section title was matched, or nothing.
         | 
| 1704 | 
            -
              def self.parse_section_title(reader, document)
         | 
| 1705 | 
            -
                 | 
| 1730 | 
            +
              def self.parse_section_title(reader, document, sect_id = nil)
         | 
| 1731 | 
            +
                sect_reftext = nil
         | 
| 1706 1732 | 
             
                line1 = reader.read_line
         | 
| 1707 1733 |  | 
| 1708 1734 | 
             
                if Compliance.markdown_syntax ? ((line1.start_with? '=', '#') && ExtAtxSectionTitleRx =~ line1) :
         | 
| @@ -1711,7 +1737,7 @@ class Parser | |
| 1711 1737 | 
             
                  sect_level, sect_title, atx = $1.length - 1, $2, true
         | 
| 1712 1738 | 
             
                  if sect_title.end_with?(']]') && InlineSectionAnchorRx =~ sect_title && !$1 # escaped
         | 
| 1713 1739 | 
             
                    sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
         | 
| 1714 | 
            -
                  end
         | 
| 1740 | 
            +
                  end unless sect_id
         | 
| 1715 1741 | 
             
                elsif Compliance.underline_style_section_titles && (line2 = reader.peek_line(true)) &&
         | 
| 1716 1742 | 
             
                    (sect_level = SETEXT_SECTION_LEVELS[line2_ch1 = line2.chr]) &&
         | 
| 1717 1743 | 
             
                    line2_ch1 * (line2_len = line2.length) == line2 && (sect_title = SetextSectionTitleRx =~ line1 && $1) &&
         | 
| @@ -1719,10 +1745,10 @@ class Parser | |
| 1719 1745 | 
             
                  atx = false
         | 
| 1720 1746 | 
             
                  if sect_title.end_with?(']]') && InlineSectionAnchorRx =~ sect_title && !$1 # escaped
         | 
| 1721 1747 | 
             
                    sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
         | 
| 1722 | 
            -
                  end
         | 
| 1748 | 
            +
                  end unless sect_id
         | 
| 1723 1749 | 
             
                  reader.shift
         | 
| 1724 1750 | 
             
                else
         | 
| 1725 | 
            -
                  raise %(Unrecognized section at #{reader. | 
| 1751 | 
            +
                  raise %(Unrecognized section at #{reader.cursor_at_prev_line})
         | 
| 1726 1752 | 
             
                end
         | 
| 1727 1753 | 
             
                sect_level += document.attr('leveloffset').to_i if document.attr?('leveloffset')
         | 
| 1728 1754 | 
             
                [sect_id, sect_reftext, sect_title, sect_level, atx]
         | 
| @@ -1758,7 +1784,7 @@ class Parser | |
| 1758 1784 | 
             
              #  # => {'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org',
         | 
| 1759 1785 | 
             
              #  #       'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.'}
         | 
| 1760 1786 | 
             
              def self.parse_header_metadata(reader, document = nil)
         | 
| 1761 | 
            -
                # NOTE this will discard  | 
| 1787 | 
            +
                # NOTE this will discard any comment lines, but not skip blank lines
         | 
| 1762 1788 | 
             
                process_attribute_entries reader, document
         | 
| 1763 1789 |  | 
| 1764 1790 | 
             
                metadata, implicit_author, implicit_authors = {}, nil, nil
         | 
| @@ -1821,6 +1847,8 @@ class Parser | |
| 1821 1847 | 
             
                  process_attribute_entries reader, document
         | 
| 1822 1848 |  | 
| 1823 1849 | 
             
                  reader.skip_blank_lines
         | 
| 1850 | 
            +
                else
         | 
| 1851 | 
            +
                  author_metadata = {}
         | 
| 1824 1852 | 
             
                end
         | 
| 1825 1853 |  | 
| 1826 1854 | 
             
                # process author attribute entries that override (or stand in for) the implicit author line
         | 
| @@ -1863,7 +1891,9 @@ class Parser | |
| 1863 1891 | 
             
                    end
         | 
| 1864 1892 | 
             
                  end
         | 
| 1865 1893 |  | 
| 1866 | 
            -
                   | 
| 1894 | 
            +
                  if author_metadata.empty?
         | 
| 1895 | 
            +
                    metadata['authorcount'] ||= (document.attributes['authorcount'] = 0)
         | 
| 1896 | 
            +
                  else
         | 
| 1867 1897 | 
             
                    document.attributes.update author_metadata
         | 
| 1868 1898 |  | 
| 1869 1899 | 
             
                    # special case
         | 
| @@ -1887,22 +1917,23 @@ class Parser | |
| 1887 1917 | 
             
              # returns a Hash of author metadata
         | 
| 1888 1918 | 
             
              def self.process_authors author_line, names_only = false, multiple = true
         | 
| 1889 1919 | 
             
                author_metadata = {}
         | 
| 1920 | 
            +
                author_idx = 0
         | 
| 1890 1921 | 
             
                keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
         | 
| 1891 1922 | 
             
                author_entries = multiple ? (author_line.split ';').map {|it| it.strip } : Array(author_line)
         | 
| 1892 | 
            -
                author_entries. | 
| 1923 | 
            +
                author_entries.each do |author_entry|
         | 
| 1893 1924 | 
             
                  next if author_entry.empty?
         | 
| 1925 | 
            +
                  author_idx += 1
         | 
| 1894 1926 | 
             
                  key_map = {}
         | 
| 1895 | 
            -
                  if  | 
| 1927 | 
            +
                  if author_idx == 1
         | 
| 1896 1928 | 
             
                    keys.each do |key|
         | 
| 1897 1929 | 
             
                      key_map[key.to_sym] = key
         | 
| 1898 1930 | 
             
                    end
         | 
| 1899 1931 | 
             
                  else
         | 
| 1900 1932 | 
             
                    keys.each do |key|
         | 
| 1901 | 
            -
                      key_map[key.to_sym] = %(#{key}_#{ | 
| 1933 | 
            +
                      key_map[key.to_sym] = %(#{key}_#{author_idx})
         | 
| 1902 1934 | 
             
                    end
         | 
| 1903 1935 | 
             
                  end
         | 
| 1904 1936 |  | 
| 1905 | 
            -
                  segments = nil
         | 
| 1906 1937 | 
             
                  if names_only # when parsing an attribute value
         | 
| 1907 1938 | 
             
                    # QUESTION should we rstrip author_entry?
         | 
| 1908 1939 | 
             
                    if author_entry.include? '<'
         | 
| @@ -1939,20 +1970,20 @@ class Parser | |
| 1939 1970 | 
             
                    author_metadata[key_map[:authorinitials]] = fname.chr
         | 
| 1940 1971 | 
             
                  end
         | 
| 1941 1972 |  | 
| 1942 | 
            -
                   | 
| 1943 | 
            -
                  # only assign the _1 attributes if there are multiple authors
         | 
| 1944 | 
            -
                  if idx == 1
         | 
| 1945 | 
            -
                    keys.each do |key|
         | 
| 1946 | 
            -
                      author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key
         | 
| 1947 | 
            -
                    end
         | 
| 1948 | 
            -
                  end
         | 
| 1949 | 
            -
                  if idx == 0
         | 
| 1973 | 
            +
                  if author_idx == 1
         | 
| 1950 1974 | 
             
                    author_metadata['authors'] = author_metadata[key_map[:author]]
         | 
| 1951 1975 | 
             
                  else
         | 
| 1976 | 
            +
                    # only assign the _1 attributes once we see the second author
         | 
| 1977 | 
            +
                    if author_idx == 2
         | 
| 1978 | 
            +
                      keys.each do |key|
         | 
| 1979 | 
            +
                        author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key
         | 
| 1980 | 
            +
                      end
         | 
| 1981 | 
            +
                    end
         | 
| 1952 1982 | 
             
                    author_metadata['authors'] = %(#{author_metadata['authors']}, #{author_metadata[key_map[:author]]})
         | 
| 1953 1983 | 
             
                  end
         | 
| 1954 1984 | 
             
                end
         | 
| 1955 1985 |  | 
| 1986 | 
            +
                author_metadata['authorcount'] = author_idx
         | 
| 1956 1987 | 
             
                author_metadata
         | 
| 1957 1988 | 
             
              end
         | 
| 1958 1989 |  | 
| @@ -2012,7 +2043,11 @@ class Parser | |
| 2012 2043 | 
             
                        return true
         | 
| 2013 2044 | 
             
                      end
         | 
| 2014 2045 | 
             
                    elsif (next_line.end_with? ']') && BlockAttributeListRx =~ next_line
         | 
| 2015 | 
            -
                       | 
| 2046 | 
            +
                      current_style = attributes[1]
         | 
| 2047 | 
            +
                      # extract id, role, and options from first positional attribute and remove, if present
         | 
| 2048 | 
            +
                      if (document.parse_attributes $1, [], :sub_input => true, :into => attributes)[1]
         | 
| 2049 | 
            +
                        attributes[1] = (parse_style_attribute attributes, reader) || current_style
         | 
| 2050 | 
            +
                      end
         | 
| 2016 2051 | 
             
                      return true
         | 
| 2017 2052 | 
             
                    end
         | 
| 2018 2053 | 
             
                  elsif normal && (next_line.start_with? '.')
         | 
| @@ -2027,7 +2062,7 @@ class Parser | |
| 2027 2062 | 
             
                      return true
         | 
| 2028 2063 | 
             
                    elsif normal && '/' * (ll = next_line.length) == next_line
         | 
| 2029 2064 | 
             
                      unless ll == 3
         | 
| 2030 | 
            -
                        reader.read_lines_until :skip_first_line => true, :preserve_last_line => true, : | 
| 2065 | 
            +
                        reader.read_lines_until :terminator => next_line, :skip_first_line => true, :preserve_last_line => true, :skip_processing => true, :context => :comment
         | 
| 2031 2066 | 
             
                        return true
         | 
| 2032 2067 | 
             
                      end
         | 
| 2033 2068 | 
             
                    else
         | 
| @@ -2041,6 +2076,9 @@ class Parser | |
| 2041 2076 | 
             
                end
         | 
| 2042 2077 | 
             
              end
         | 
| 2043 2078 |  | 
| 2079 | 
            +
              # Process consecutive attribute entry lines, ignoring adjacent line comments and comment blocks.
         | 
| 2080 | 
            +
              #
         | 
| 2081 | 
            +
              # Returns nothing
         | 
| 2044 2082 | 
             
              def self.process_attribute_entries reader, document, attributes = nil
         | 
| 2045 2083 | 
             
                reader.skip_comment_lines
         | 
| 2046 2084 | 
             
                while process_attribute_entry reader, document, attributes
         | 
| @@ -2135,12 +2173,12 @@ class Parser | |
| 2135 2173 | 
             
              #
         | 
| 2136 2174 | 
             
              # Returns the String 0-index marker for this list item
         | 
| 2137 2175 | 
             
              def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil)
         | 
| 2138 | 
            -
                if list_type == : | 
| 2139 | 
            -
                  (marker.start_with? '.') ? marker : (resolve_ordered_list_marker marker, ordinal, validate, reader)
         | 
| 2140 | 
            -
                elsif list_type == :colist
         | 
| 2141 | 
            -
                  '<1>'
         | 
| 2142 | 
            -
                else
         | 
| 2176 | 
            +
                if list_type == :ulist
         | 
| 2143 2177 | 
             
                  marker
         | 
| 2178 | 
            +
                elsif list_type == :olist
         | 
| 2179 | 
            +
                  resolve_ordered_list_marker(marker, ordinal, validate, reader)
         | 
| 2180 | 
            +
                else # :colist
         | 
| 2181 | 
            +
                  '<1>'
         | 
| 2144 2182 | 
             
                end
         | 
| 2145 2183 | 
             
              end
         | 
| 2146 2184 |  | 
| @@ -2166,6 +2204,7 @@ class Parser | |
| 2166 2204 | 
             
              #
         | 
| 2167 2205 | 
             
              # Returns the String of the first marker in this number series
         | 
| 2168 2206 | 
             
              def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false, reader = nil)
         | 
| 2207 | 
            +
                return marker if marker.start_with? '.'
         | 
| 2169 2208 | 
             
                expected = actual = nil
         | 
| 2170 2209 | 
             
                case ORDERED_LIST_STYLES.find {|s| OrderedListMarkerRxMap[s].match? marker }
         | 
| 2171 2210 | 
             
                when :arabic
         | 
| @@ -2188,22 +2227,20 @@ class Parser | |
| 2188 2227 | 
             
                  marker = 'A.'
         | 
| 2189 2228 | 
             
                when :lowerroman
         | 
| 2190 2229 | 
             
                  if validate
         | 
| 2191 | 
            -
                     | 
| 2192 | 
            -
                     | 
| 2193 | 
            -
                    actual = roman_numeral_to_int(marker.chop) # remove trailing ) and coerce to int
         | 
| 2230 | 
            +
                    expected = Helpers.int_to_roman(ordinal + 1).downcase
         | 
| 2231 | 
            +
                    actual = marker.chop # remove trailing )
         | 
| 2194 2232 | 
             
                  end
         | 
| 2195 2233 | 
             
                  marker = 'i)'
         | 
| 2196 2234 | 
             
                when :upperroman
         | 
| 2197 2235 | 
             
                  if validate
         | 
| 2198 | 
            -
                     | 
| 2199 | 
            -
                     | 
| 2200 | 
            -
                    actual = roman_numeral_to_int(marker.chop) # remove trailing ) and coerce to int
         | 
| 2236 | 
            +
                    expected = Helpers.int_to_roman(ordinal + 1)
         | 
| 2237 | 
            +
                    actual = marker.chop # remove trailing )
         | 
| 2201 2238 | 
             
                  end
         | 
| 2202 2239 | 
             
                  marker = 'I)'
         | 
| 2203 2240 | 
             
                end
         | 
| 2204 2241 |  | 
| 2205 2242 | 
             
                if validate && expected != actual
         | 
| 2206 | 
            -
                  warn %( | 
| 2243 | 
            +
                  logger.warn message_with_context %(list item index: expected #{expected}, got #{actual}), :source_location => reader.cursor
         | 
| 2207 2244 | 
             
                end
         | 
| 2208 2245 |  | 
| 2209 2246 | 
             
                marker
         | 
| @@ -2221,18 +2258,13 @@ class Parser | |
| 2221 2258 | 
             
              def self.is_sibling_list_item?(line, list_type, sibling_trait)
         | 
| 2222 2259 | 
             
                if ::Regexp === sibling_trait
         | 
| 2223 2260 | 
             
                  matcher = sibling_trait
         | 
| 2224 | 
            -
                  expected_marker = false
         | 
| 2225 2261 | 
             
                else
         | 
| 2226 2262 | 
             
                  matcher = ListRxMap[list_type]
         | 
| 2227 2263 | 
             
                  expected_marker = sibling_trait
         | 
| 2228 2264 | 
             
                end
         | 
| 2229 2265 |  | 
| 2230 | 
            -
                if  | 
| 2231 | 
            -
                   | 
| 2232 | 
            -
                    expected_marker == resolve_list_marker(list_type, m[1])
         | 
| 2233 | 
            -
                  else
         | 
| 2234 | 
            -
                    true
         | 
| 2235 | 
            -
                  end
         | 
| 2266 | 
            +
                if matcher =~ line
         | 
| 2267 | 
            +
                  expected_marker ? expected_marker == resolve_list_marker(list_type, $1) : true
         | 
| 2236 2268 | 
             
                else
         | 
| 2237 2269 | 
             
                  false
         | 
| 2238 2270 | 
             
                end
         | 
| @@ -2263,7 +2295,7 @@ class Parser | |
| 2263 2295 | 
             
                implicit_header = true unless skipped > 0 || (attributes.key? 'header-option') || (attributes.key? 'noheader-option')
         | 
| 2264 2296 |  | 
| 2265 2297 | 
             
                while (line = table_reader.read_line)
         | 
| 2266 | 
            -
                  if (loop_idx += 1) > 0 && line.empty?
         | 
| 2298 | 
            +
                  if (beyond_first = (loop_idx += 1) > 0) && line.empty?
         | 
| 2267 2299 | 
             
                    line = nil
         | 
| 2268 2300 | 
             
                    implicit_header_boundary += 1 if implicit_header_boundary
         | 
| 2269 2301 | 
             
                  elsif format == 'psv'
         | 
| @@ -2285,58 +2317,63 @@ class Parser | |
| 2285 2317 | 
             
                    end
         | 
| 2286 2318 | 
             
                  end
         | 
| 2287 2319 |  | 
| 2288 | 
            -
                   | 
| 2289 | 
            -
             | 
| 2290 | 
            -
                     | 
| 2291 | 
            -
             | 
| 2292 | 
            -
             | 
| 2293 | 
            -
             | 
| 2320 | 
            +
                  unless beyond_first
         | 
| 2321 | 
            +
                    table_reader.mark
         | 
| 2322 | 
            +
                    # NOTE implicit header is offset by at least one blank line; implicit_header_boundary tracks size of gap
         | 
| 2323 | 
            +
                    if implicit_header
         | 
| 2324 | 
            +
                      if table_reader.has_more_lines? && table_reader.peek_line.empty?
         | 
| 2325 | 
            +
                        implicit_header_boundary = 1
         | 
| 2326 | 
            +
                      else
         | 
| 2327 | 
            +
                        implicit_header = false
         | 
| 2328 | 
            +
                      end
         | 
| 2294 2329 | 
             
                    end
         | 
| 2295 2330 | 
             
                  end
         | 
| 2296 2331 |  | 
| 2297 2332 | 
             
                  # this loop is used for flow control; internal logic controls how many times it executes
         | 
| 2298 2333 | 
             
                  while true
         | 
| 2299 2334 | 
             
                    if line && (m = parser_ctx.match_delimiter line)
         | 
| 2335 | 
            +
                      pre_match, post_match = m.pre_match, m.post_match
         | 
| 2300 2336 | 
             
                      case format
         | 
| 2301 2337 | 
             
                      when 'csv'
         | 
| 2302 | 
            -
                        if parser_ctx.buffer_has_unclosed_quotes?  | 
| 2303 | 
            -
                           | 
| 2338 | 
            +
                        if parser_ctx.buffer_has_unclosed_quotes? pre_match
         | 
| 2339 | 
            +
                          parser_ctx.skip_past_delimiter pre_match
         | 
| 2340 | 
            +
                          break if (line = post_match).empty?
         | 
| 2304 2341 | 
             
                          redo
         | 
| 2305 2342 | 
             
                        end
         | 
| 2306 | 
            -
                        parser_ctx.buffer = %(#{parser_ctx.buffer}#{ | 
| 2343 | 
            +
                        parser_ctx.buffer = %(#{parser_ctx.buffer}#{pre_match})
         | 
| 2307 2344 | 
             
                      when 'dsv'
         | 
| 2308 | 
            -
                        if  | 
| 2309 | 
            -
                           | 
| 2345 | 
            +
                        if pre_match.end_with? '\\'
         | 
| 2346 | 
            +
                          parser_ctx.skip_past_escaped_delimiter pre_match
         | 
| 2347 | 
            +
                          if (line = post_match).empty?
         | 
| 2310 2348 | 
             
                            parser_ctx.buffer = %(#{parser_ctx.buffer}#{LF})
         | 
| 2311 2349 | 
             
                            parser_ctx.keep_cell_open
         | 
| 2312 2350 | 
             
                            break
         | 
| 2313 2351 | 
             
                          end
         | 
| 2314 2352 | 
             
                          redo
         | 
| 2315 2353 | 
             
                        end
         | 
| 2316 | 
            -
                        parser_ctx.buffer = %(#{parser_ctx.buffer}#{ | 
| 2354 | 
            +
                        parser_ctx.buffer = %(#{parser_ctx.buffer}#{pre_match})
         | 
| 2317 2355 | 
             
                      else # psv
         | 
| 2318 | 
            -
                        if  | 
| 2319 | 
            -
                           | 
| 2356 | 
            +
                        if pre_match.end_with? '\\'
         | 
| 2357 | 
            +
                          parser_ctx.skip_past_escaped_delimiter pre_match
         | 
| 2358 | 
            +
                          if (line = post_match).empty?
         | 
| 2320 2359 | 
             
                            parser_ctx.buffer = %(#{parser_ctx.buffer}#{LF})
         | 
| 2321 2360 | 
             
                            parser_ctx.keep_cell_open
         | 
| 2322 2361 | 
             
                            break
         | 
| 2323 2362 | 
             
                          end
         | 
| 2324 2363 | 
             
                          redo
         | 
| 2325 2364 | 
             
                        end
         | 
| 2326 | 
            -
                        next_cellspec, cell_text = parse_cellspec  | 
| 2365 | 
            +
                        next_cellspec, cell_text = parse_cellspec pre_match
         | 
| 2327 2366 | 
             
                        parser_ctx.push_cellspec next_cellspec
         | 
| 2328 2367 | 
             
                        parser_ctx.buffer = %(#{parser_ctx.buffer}#{cell_text})
         | 
| 2329 2368 | 
             
                      end
         | 
| 2330 2369 | 
             
                      # don't break if empty to preserve empty cell found at end of line (see issue #1106)
         | 
| 2331 | 
            -
                      line = nil if (line =  | 
| 2370 | 
            +
                      line = nil if (line = post_match).empty?
         | 
| 2332 2371 | 
             
                      parser_ctx.close_cell
         | 
| 2333 2372 | 
             
                    else
         | 
| 2334 2373 | 
             
                      # no other delimiters to see here; suck up this line into the buffer and move on
         | 
| 2335 2374 | 
             
                      parser_ctx.buffer = %(#{parser_ctx.buffer}#{line}#{LF})
         | 
| 2336 2375 | 
             
                      case format
         | 
| 2337 2376 | 
             
                      when 'csv'
         | 
| 2338 | 
            -
                        # QUESTION make stripping endlines in csv data an option? (unwrap-option?)
         | 
| 2339 | 
            -
                        parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} )
         | 
| 2340 2377 | 
             
                        if parser_ctx.buffer_has_unclosed_quotes?
         | 
| 2341 2378 | 
             
                          implicit_header, implicit_header_boundary = false, nil if implicit_header_boundary && loop_idx == 0
         | 
| 2342 2379 | 
             
                          parser_ctx.keep_cell_open
         | 
| @@ -2412,9 +2449,12 @@ class Parser | |
| 2412 2449 | 
             
                      end
         | 
| 2413 2450 | 
             
                    end
         | 
| 2414 2451 |  | 
| 2415 | 
            -
                     | 
| 2416 | 
            -
             | 
| 2417 | 
            -
             | 
| 2452 | 
            +
                    if (width = m[3])
         | 
| 2453 | 
            +
                      # to_i will strip the optional %
         | 
| 2454 | 
            +
                      spec['width'] = width == '~' ? -1 : width.to_i
         | 
| 2455 | 
            +
                    else
         | 
| 2456 | 
            +
                      spec['width'] = 1
         | 
| 2457 | 
            +
                    end
         | 
| 2418 2458 |  | 
| 2419 2459 | 
             
                    # make this an operation
         | 
| 2420 2460 | 
             
                    if m[4] && TableCellStyles.key?(m[4])
         | 
| @@ -2527,7 +2567,11 @@ class Parser | |
| 2527 2567 | 
             
                  save_current = lambda {
         | 
| 2528 2568 | 
             
                    if collector.empty?
         | 
| 2529 2569 | 
             
                      unless type == :style
         | 
| 2530 | 
            -
                         | 
| 2570 | 
            +
                        if reader
         | 
| 2571 | 
            +
                          logger.warn message_with_context %(invalid empty #{type} detected in style attribute), :source_location => reader.cursor_at_prev_line
         | 
| 2572 | 
            +
                        else
         | 
| 2573 | 
            +
                          logger.warn %(invalid empty #{type} detected in style attribute)
         | 
| 2574 | 
            +
                        end
         | 
| 2531 2575 | 
             
                      end
         | 
| 2532 2576 | 
             
                    else
         | 
| 2533 2577 | 
             
                      case type
         | 
| @@ -2535,7 +2579,11 @@ class Parser | |
| 2535 2579 | 
             
                        (parsed[type] ||= []) << collector.join
         | 
| 2536 2580 | 
             
                      when :id
         | 
| 2537 2581 | 
             
                        if parsed.key? :id
         | 
| 2538 | 
            -
                           | 
| 2582 | 
            +
                          if reader
         | 
| 2583 | 
            +
                            logger.warn message_with_context 'multiple ids detected in style attribute', :source_location => reader.cursor_at_prev_line
         | 
| 2584 | 
            +
                          else
         | 
| 2585 | 
            +
                            logger.warn 'multiple ids detected in style attribute'
         | 
| 2586 | 
            +
                          end
         | 
| 2539 2587 | 
             
                        end
         | 
| 2540 2588 | 
             
                        parsed[type] = collector.join
         | 
| 2541 2589 | 
             
                      else
         | 
| @@ -2571,15 +2619,13 @@ class Parser | |
| 2571 2619 |  | 
| 2572 2620 | 
             
                    attributes['id'] = parsed[:id] if parsed.key? :id
         | 
| 2573 2621 |  | 
| 2574 | 
            -
                     | 
| 2622 | 
            +
                    if parsed.key? :role
         | 
| 2623 | 
            +
                      attributes['role'] = (existing_role = attributes['role']).nil_or_empty? ? (parsed[:role].join ' ') : %(#{existing_role} #{parsed[:role].join ' '})
         | 
| 2624 | 
            +
                    end
         | 
| 2575 2625 |  | 
| 2576 2626 | 
             
                    if parsed.key? :option
         | 
| 2577 | 
            -
                      ( | 
| 2578 | 
            -
                       | 
| 2579 | 
            -
                        attributes['options'] = (options + existing_opts.split(',')) * ','
         | 
| 2580 | 
            -
                      else
         | 
| 2581 | 
            -
                        attributes['options'] = options * ','
         | 
| 2582 | 
            -
                      end
         | 
| 2627 | 
            +
                      (opts = parsed[:option]).each {|opt| attributes[%(#{opt}-option)] = '' }
         | 
| 2628 | 
            +
                      attributes['options'] = (existing_opts = attributes['options']).nil_or_empty? ? (opts.join ',') : %(#{existing_opts},#{opts.join ','})
         | 
| 2583 2629 | 
             
                    end
         | 
| 2584 2630 |  | 
| 2585 2631 | 
             
                    parsed_style
         | 
| @@ -2710,27 +2756,5 @@ class Parser | |
| 2710 2756 | 
             
              def self.sanitize_attribute_name(name)
         | 
| 2711 2757 | 
             
                name.gsub(InvalidAttributeNameCharsRx, '').downcase
         | 
| 2712 2758 | 
             
              end
         | 
| 2713 | 
            -
             | 
| 2714 | 
            -
              # Internal: Converts a Roman numeral to an integer value.
         | 
| 2715 | 
            -
              #
         | 
| 2716 | 
            -
              # value - The String Roman numeral to convert
         | 
| 2717 | 
            -
              #
         | 
| 2718 | 
            -
              # Returns the Integer for this Roman numeral
         | 
| 2719 | 
            -
              def self.roman_numeral_to_int(value)
         | 
| 2720 | 
            -
                value = value.downcase
         | 
| 2721 | 
            -
                digits = { 'i' => 1, 'v' => 5, 'x' => 10 }
         | 
| 2722 | 
            -
                result = 0
         | 
| 2723 | 
            -
             | 
| 2724 | 
            -
                (0..value.length - 1).each {|i|
         | 
| 2725 | 
            -
                  digit = digits[value[i..i]]
         | 
| 2726 | 
            -
                  if i + 1 < value.length && digits[value[i+1..i+1]] > digit
         | 
| 2727 | 
            -
                    result -= digit
         | 
| 2728 | 
            -
                  else
         | 
| 2729 | 
            -
                    result += digit
         | 
| 2730 | 
            -
                  end
         | 
| 2731 | 
            -
                }
         | 
| 2732 | 
            -
             | 
| 2733 | 
            -
                result
         | 
| 2734 | 
            -
              end
         | 
| 2735 2759 | 
             
            end
         | 
| 2736 2760 | 
             
            end
         |