asciidoctor 1.5.5 → 1.5.6
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 +216 -1
- data/CONTRIBUTING.adoc +2 -2
- data/Gemfile +20 -1
- data/LICENSE.adoc +1 -1
- data/README-fr.adoc +4 -3
- data/README-jp.adoc +11 -10
- data/README-zh_CN.adoc +4 -3
- data/README.adoc +17 -202
- data/Rakefile +41 -25
- data/asciidoctor.gemspec +9 -10
- data/data/locale/attributes.adoc +216 -34
- data/data/stylesheets/asciidoctor-default.css +23 -16
- data/features/step_definitions.rb +15 -19
- data/features/xref.feature +584 -20
- data/lib/asciidoctor.rb +292 -278
- data/lib/asciidoctor/abstract_block.rb +155 -94
- data/lib/asciidoctor/abstract_node.rb +108 -94
- data/lib/asciidoctor/attribute_list.rb +30 -22
- data/lib/asciidoctor/block.rb +7 -7
- data/lib/asciidoctor/cli/invoker.rb +47 -34
- data/lib/asciidoctor/cli/options.rb +22 -11
- data/lib/asciidoctor/converter.rb +3 -3
- data/lib/asciidoctor/converter/base.rb +2 -2
- data/lib/asciidoctor/converter/composite.rb +1 -1
- data/lib/asciidoctor/converter/docbook45.rb +2 -2
- data/lib/asciidoctor/converter/docbook5.rb +132 -87
- data/lib/asciidoctor/converter/factory.rb +0 -1
- data/lib/asciidoctor/converter/html5.rb +116 -98
- data/lib/asciidoctor/converter/manpage.rb +51 -52
- data/lib/asciidoctor/converter/template.rb +47 -36
- data/lib/asciidoctor/core_ext.rb +8 -2
- data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +4 -0
- data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +6 -0
- data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +5 -0
- data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +1 -1
- data/lib/asciidoctor/core_ext/1.8.7/string/{limit.rb → limit_bytesize.rb} +7 -6
- data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +6 -0
- data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +1 -1
- data/lib/asciidoctor/core_ext/nil_or_empty.rb +5 -5
- data/lib/asciidoctor/core_ext/regexp/is_match.rb +3 -0
- data/lib/asciidoctor/core_ext/string/{limit.rb → limit_bytesize.rb} +2 -2
- data/lib/asciidoctor/document.rb +216 -213
- data/lib/asciidoctor/extensions.rb +318 -185
- data/lib/asciidoctor/helpers.rb +35 -35
- data/lib/asciidoctor/inline.rb +32 -1
- data/lib/asciidoctor/list.rb +22 -6
- data/lib/asciidoctor/parser.rb +1008 -1038
- data/lib/asciidoctor/path_resolver.rb +46 -50
- data/lib/asciidoctor/reader.rb +275 -251
- data/lib/asciidoctor/section.rb +86 -58
- data/lib/asciidoctor/stylesheets.rb +6 -6
- data/lib/asciidoctor/substitutors.rb +567 -649
- data/lib/asciidoctor/table.rb +163 -108
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +18 -16
- data/man/asciidoctor.adoc +15 -13
- data/test/attributes_test.rb +138 -22
- data/test/blocks_test.rb +377 -97
- data/test/converter_test.rb +13 -0
- data/test/document_test.rb +244 -34
- data/test/extensions_test.rb +409 -42
- data/test/fixtures/asciidoc_index.txt +521 -0
- data/test/fixtures/basic-docinfo-footer.html +6 -0
- data/test/fixtures/basic-docinfo-footer.xml +8 -0
- data/test/fixtures/basic-docinfo.html +1 -0
- data/test/fixtures/basic-docinfo.xml +4 -0
- data/test/fixtures/basic.asciidoc +5 -0
- data/test/fixtures/chapter-a.adoc +3 -0
- data/test/fixtures/child-include.adoc +5 -0
- data/test/fixtures/circle.svg +9 -0
- data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
- data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
- data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
- data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
- data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
- data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
- data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
- data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
- data/test/fixtures/custom-docinfodir/basic-docinfo.html +1 -0
- data/test/fixtures/custom-docinfodir/docinfo.html +1 -0
- data/test/fixtures/docinfo-footer.html +1 -0
- data/test/fixtures/docinfo-footer.xml +9 -0
- data/test/fixtures/docinfo.html +1 -0
- data/test/fixtures/docinfo.xml +3 -0
- data/test/fixtures/dot.gif +0 -0
- data/test/fixtures/encoding.asciidoc +13 -0
- data/test/fixtures/grandchild-include.adoc +3 -0
- data/test/fixtures/hello-asciidoctor.pdf +69 -0
- data/test/fixtures/include-file.asciidoc +24 -0
- data/test/fixtures/include-file.ml +3 -0
- data/test/fixtures/include-file.xml +5 -0
- data/test/fixtures/master.adoc +5 -0
- data/test/fixtures/mismatched-end-tag.adoc +7 -0
- data/test/fixtures/parent-include-restricted.adoc +5 -0
- data/test/fixtures/parent-include.adoc +5 -0
- data/test/fixtures/sample.asciidoc +26 -0
- data/test/fixtures/stylesheets/custom.css +3 -0
- data/test/fixtures/subs-docinfo.html +2 -0
- data/test/fixtures/subs.adoc +7 -0
- data/test/fixtures/tagged-class-enclosed.rb +26 -0
- data/test/fixtures/tagged-class.rb +23 -0
- data/test/fixtures/tip.gif +0 -0
- data/test/invoker_test.rb +82 -4
- data/test/links_test.rb +312 -37
- data/test/lists_test.rb +204 -25
- data/test/manpage_test.rb +191 -4
- data/test/options_test.rb +18 -1
- data/test/paragraphs_test.rb +32 -7
- data/test/parser_test.rb +150 -30
- data/test/paths_test.rb +47 -13
- data/test/preamble_test.rb +1 -1
- data/test/reader_test.rb +366 -126
- data/test/sections_test.rb +203 -56
- data/test/substitutions_test.rb +339 -131
- data/test/tables_test.rb +315 -15
- data/test/test_helper.rb +400 -0
- data/test/text_test.rb +5 -5
- metadata +110 -22
    
        data/lib/asciidoctor/helpers.rb
    CHANGED
    
    | @@ -5,7 +5,7 @@ module Helpers | |
| 5 5 | 
             
              #
         | 
| 6 6 | 
             
              # Attempts to load the library specified in the first argument using the
         | 
| 7 7 | 
             
              # Kernel#require. Rescues the LoadError if the library is not available and
         | 
| 8 | 
            -
              # passes a message to Kernel# | 
| 8 | 
            +
              # passes a message to Kernel#raise if on_failure is :abort or Kernel#warn if
         | 
| 9 9 | 
             
              # on_failure is :warn to communicate to the user that processing is being
         | 
| 10 10 | 
             
              # aborted or functionality is disabled, respectively. If a gem_name is
         | 
| 11 11 | 
             
              # specified, the message communicates that a required gem is not installed.
         | 
| @@ -17,7 +17,7 @@ module Helpers | |
| 17 17 | 
             
              # on_failure - a Symbol that indicates how to handle a load failure (:abort, :warn, :ignore) (default: :abort)
         | 
| 18 18 | 
             
              #
         | 
| 19 19 | 
             
              # returns The return value of Kernel#require if the library is available and can be, or was previously, loaded.
         | 
| 20 | 
            -
              # Otherwise, Kernel# | 
| 20 | 
            +
              # Otherwise, Kernel#raise is called with an appropriate message if on_failure is :abort.
         | 
| 21 21 | 
             
              # Otherwise, Kernel#warn is called with an appropriate message and nil returned if on_failure is :warn.
         | 
| 22 22 | 
             
              # Otherwise, nil is returned.
         | 
| 23 23 | 
             
              def self.require_library name, gem_name = true, on_failure = :abort
         | 
| @@ -27,14 +27,14 @@ module Helpers | |
| 27 27 | 
             
                  gem_name = name if gem_name == true
         | 
| 28 28 | 
             
                  case on_failure
         | 
| 29 29 | 
             
                  when :abort
         | 
| 30 | 
            -
                     | 
| 30 | 
            +
                    raise ::LoadError, %(asciidoctor: FAILED: required gem '#{gem_name}' is not installed. Processing aborted.)
         | 
| 31 31 | 
             
                  when :warn
         | 
| 32 32 | 
             
                    warn %(asciidoctor: WARNING: optional gem '#{gem_name}' is not installed. Functionality disabled.)
         | 
| 33 33 | 
             
                  end
         | 
| 34 34 | 
             
                else
         | 
| 35 35 | 
             
                  case on_failure
         | 
| 36 36 | 
             
                  when :abort
         | 
| 37 | 
            -
                     | 
| 37 | 
            +
                    raise ::LoadError, %(asciidoctor: FAILED: #{e.message.chomp '.'}. Processing aborted.)
         | 
| 38 38 | 
             
                  when :warn
         | 
| 39 39 | 
             
                    warn %(asciidoctor: WARNING: #{e.message.chomp '.'}. Functionality disabled.)
         | 
| 40 40 | 
             
                  end
         | 
| @@ -62,19 +62,18 @@ module Helpers | |
| 62 62 | 
             
              #
         | 
| 63 63 | 
             
              # returns a String Array of normalized lines
         | 
| 64 64 | 
             
              def self.normalize_lines_array data
         | 
| 65 | 
            -
                return  | 
| 65 | 
            +
                return data if data.empty?
         | 
| 66 66 |  | 
| 67 | 
            -
                 | 
| 68 | 
            -
                leading_bytes = (first_line = data[0])[0..2].bytes.to_a
         | 
| 67 | 
            +
                leading_bytes = (first_line = data[0]).unpack 'C3'
         | 
| 69 68 | 
             
                if COERCE_ENCODING
         | 
| 70 69 | 
             
                  utf8 = ::Encoding::UTF_8
         | 
| 71 | 
            -
                  if (leading_2_bytes = leading_bytes | 
| 72 | 
            -
                    # Ruby messes up trailing whitespace on UTF-16LE, so take a different route
         | 
| 73 | 
            -
                    return ((data.join.force_encoding ::Encoding::UTF_16LE)[1..-1].encode utf8). | 
| 70 | 
            +
                  if (leading_2_bytes = leading_bytes.slice 0, 2) == BOM_BYTES_UTF_16LE
         | 
| 71 | 
            +
                    # HACK Ruby messes up trailing whitespace on UTF-16LE, so take a different route
         | 
| 72 | 
            +
                    return ((data.join.force_encoding ::Encoding::UTF_16LE)[1..-1].encode utf8).each_line.map {|line| line.rstrip }
         | 
| 74 73 | 
             
                  elsif leading_2_bytes == BOM_BYTES_UTF_16BE
         | 
| 75 74 | 
             
                    data[0] = (first_line.force_encoding ::Encoding::UTF_16BE)[1..-1]
         | 
| 76 | 
            -
                    return data.map {|line|  | 
| 77 | 
            -
                  elsif leading_bytes | 
| 75 | 
            +
                    return data.map {|line| %(#{((line.force_encoding ::Encoding::UTF_16BE).encode utf8).rstrip}) }
         | 
| 76 | 
            +
                  elsif leading_bytes == BOM_BYTES_UTF_8
         | 
| 78 77 | 
             
                    data[0] = (first_line.force_encoding utf8)[1..-1]
         | 
| 79 78 | 
             
                  end
         | 
| 80 79 |  | 
| @@ -102,22 +101,21 @@ module Helpers | |
| 102 101 | 
             
              def self.normalize_lines_from_string data
         | 
| 103 102 | 
             
                return [] if data.nil_or_empty?
         | 
| 104 103 |  | 
| 104 | 
            +
                leading_bytes = data.unpack 'C3'
         | 
| 105 105 | 
             
                if COERCE_ENCODING
         | 
| 106 106 | 
             
                  utf8 = ::Encoding::UTF_8
         | 
| 107 | 
            -
                   | 
| 108 | 
            -
                  leading_bytes = data[0..2].bytes.to_a
         | 
| 109 | 
            -
                  if (leading_2_bytes = leading_bytes[0..1]) == BOM_BYTES_UTF_16LE
         | 
| 107 | 
            +
                  if (leading_2_bytes = leading_bytes.slice 0, 2) == BOM_BYTES_UTF_16LE
         | 
| 110 108 | 
             
                    data = (data.force_encoding ::Encoding::UTF_16LE)[1..-1].encode utf8
         | 
| 111 109 | 
             
                  elsif leading_2_bytes == BOM_BYTES_UTF_16BE
         | 
| 112 110 | 
             
                    data = (data.force_encoding ::Encoding::UTF_16BE)[1..-1].encode utf8
         | 
| 113 | 
            -
                  elsif leading_bytes | 
| 111 | 
            +
                  elsif leading_bytes == BOM_BYTES_UTF_8
         | 
| 114 112 | 
             
                    data = data.encoding == utf8 ? data[1..-1] : (data.force_encoding utf8)[1..-1]
         | 
| 115 113 | 
             
                  else
         | 
| 116 114 | 
             
                    data = data.force_encoding utf8 unless data.encoding == utf8
         | 
| 117 115 | 
             
                  end
         | 
| 118 116 | 
             
                else
         | 
| 119 117 | 
             
                  # Ruby 1.8 has no built-in re-encoding, so no point in removing the UTF-16 BOMs
         | 
| 120 | 
            -
                  if  | 
| 118 | 
            +
                  if leading_bytes == BOM_BYTES_UTF_8
         | 
| 121 119 | 
             
                    data = data[3..-1]
         | 
| 122 120 | 
             
                  end
         | 
| 123 121 | 
             
                end
         | 
| @@ -133,7 +131,7 @@ module Helpers | |
| 133 131 | 
             
              #
         | 
| 134 132 | 
             
              # returns true if the String is a URI, false if it is not
         | 
| 135 133 | 
             
              def self.uriish? str
         | 
| 136 | 
            -
                (str.include? ':') && str | 
| 134 | 
            +
                (str.include? ':') && (UriSniffRx.match? str)
         | 
| 137 135 | 
             
              end
         | 
| 138 136 |  | 
| 139 137 | 
             
              # Public: Efficiently retrieves the URI prefix of the specified String
         | 
| @@ -145,26 +143,24 @@ module Helpers | |
| 145 143 | 
             
              #
         | 
| 146 144 | 
             
              # returns the string URI prefix if the string is a URI, otherwise nil
         | 
| 147 145 | 
             
              def self.uri_prefix str
         | 
| 148 | 
            -
                (str.include? ':') &&  | 
| 146 | 
            +
                (str.include? ':') && UriSniffRx =~ str ? $& : nil
         | 
| 149 147 | 
             
              end
         | 
| 150 148 |  | 
| 151 149 | 
             
              # Matches the characters in a URI to encode
         | 
| 152 150 | 
             
              REGEXP_ENCODE_URI_CHARS = /[^\w\-.!~*';:@=+$,()\[\]]/
         | 
| 153 151 |  | 
| 154 | 
            -
              # Public: Encode a  | 
| 152 | 
            +
              # Public: Encode a String for inclusion in a URI.
         | 
| 155 153 | 
             
              #
         | 
| 156 | 
            -
              # str - the  | 
| 154 | 
            +
              # str - the String to URI encode
         | 
| 157 155 | 
             
              #
         | 
| 158 | 
            -
              #  | 
| 159 | 
            -
              def self. | 
| 160 | 
            -
                str.gsub(REGEXP_ENCODE_URI_CHARS)  | 
| 161 | 
            -
                  $&.each_byte.map {|c| sprintf '%%%02X', c}.join
         | 
| 162 | 
            -
                end
         | 
| 156 | 
            +
              # Returns the String with all URI reserved characters encoded.
         | 
| 157 | 
            +
              def self.uri_encode str
         | 
| 158 | 
            +
                str.gsub(REGEXP_ENCODE_URI_CHARS) { $&.each_byte.map {|c| sprintf '%%%02X', c }.join }
         | 
| 163 159 | 
             
              end
         | 
| 164 160 |  | 
| 165 161 | 
             
              # Public: Removes the file extension from filename and returns the result
         | 
| 166 162 | 
             
              #
         | 
| 167 | 
            -
              #  | 
| 163 | 
            +
              # filename - The String file name to process
         | 
| 168 164 | 
             
              #
         | 
| 169 165 | 
             
              # Examples
         | 
| 170 166 | 
             
              #
         | 
| @@ -172,26 +168,30 @@ module Helpers | |
| 172 168 | 
             
              #   # => "part1/chapter1"
         | 
| 173 169 | 
             
              #
         | 
| 174 170 | 
             
              # Returns the String filename with the file extension removed
         | 
| 175 | 
            -
              def self.rootname | 
| 176 | 
            -
                 | 
| 171 | 
            +
              def self.rootname filename
         | 
| 172 | 
            +
                filename.slice 0, ((filename.rindex '.') || filename.length)
         | 
| 177 173 | 
             
              end
         | 
| 178 174 |  | 
| 179 175 | 
             
              # Public: Retrieves the basename of the filename, optionally removing the extension, if present
         | 
| 180 176 | 
             
              #
         | 
| 181 | 
            -
              #  | 
| 182 | 
            -
              #  | 
| 177 | 
            +
              # filename - The String file name to process.
         | 
| 178 | 
            +
              # drop_ext - A Boolean flag indicating whether to drop the extension
         | 
| 179 | 
            +
              #            or an explicit String extension to drop (default: nil).
         | 
| 183 180 | 
             
              #
         | 
| 184 181 | 
             
              # Examples
         | 
| 185 182 | 
             
              #
         | 
| 186 183 | 
             
              #   Helpers.basename('images/tiger.png', true)
         | 
| 187 184 | 
             
              #   # => "tiger"
         | 
| 188 185 | 
             
              #
         | 
| 186 | 
            +
              #   Helpers.basename('images/tiger.png', '.png')
         | 
| 187 | 
            +
              #   # => "tiger"
         | 
| 188 | 
            +
              #
         | 
| 189 189 | 
             
              # Returns the String filename with leading directories removed and, if specified, the extension removed
         | 
| 190 | 
            -
              def self.basename( | 
| 191 | 
            -
                if  | 
| 192 | 
            -
                  ::File.basename  | 
| 190 | 
            +
              def self.basename(filename, drop_ext = nil)
         | 
| 191 | 
            +
                if drop_ext
         | 
| 192 | 
            +
                  ::File.basename filename, (drop_ext == true ? (::File.extname filename) : drop_ext)
         | 
| 193 193 | 
             
                else
         | 
| 194 | 
            -
                  ::File.basename  | 
| 194 | 
            +
                  ::File.basename filename
         | 
| 195 195 | 
             
                end
         | 
| 196 196 | 
             
              end
         | 
| 197 197 |  | 
    
        data/lib/asciidoctor/inline.rb
    CHANGED
    
    | @@ -39,6 +39,37 @@ class Inline < AbstractNode | |
| 39 39 | 
             
              end
         | 
| 40 40 |  | 
| 41 41 | 
             
              # Alias render to convert to maintain backwards compatibility
         | 
| 42 | 
            -
              alias  | 
| 42 | 
            +
              alias render convert
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              # Public: Returns the converted alt text for this inline image.
         | 
| 45 | 
            +
              #
         | 
| 46 | 
            +
              # Returns the [String] value of the alt attribute.
         | 
| 47 | 
            +
              def alt
         | 
| 48 | 
            +
                attr 'alt'
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              # (see AbstractNode#reftext?)
         | 
| 52 | 
            +
              def reftext?
         | 
| 53 | 
            +
                @text && (@type == :ref || @type == :bibref)
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              # (see AbstractNode#reftext)
         | 
| 57 | 
            +
              def reftext
         | 
| 58 | 
            +
                (val = @text) ? (apply_reftext_subs val) : nil
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
              # Public: Generate cross reference text (xreftext) that can be used to refer
         | 
| 62 | 
            +
              # to this inline node.
         | 
| 63 | 
            +
              #
         | 
| 64 | 
            +
              # Use the explicit reftext for this inline node, if specified, retrieved by
         | 
| 65 | 
            +
              # calling the reftext method. Otherwise, returns nil.
         | 
| 66 | 
            +
              #
         | 
| 67 | 
            +
              # xrefstyle - Not currently used (default: nil).
         | 
| 68 | 
            +
              #
         | 
| 69 | 
            +
              # Returns the [String] reftext to refer to this inline node or nothing if no
         | 
| 70 | 
            +
              # reftext is defined.
         | 
| 71 | 
            +
              def xreftext xrefstyle = nil
         | 
| 72 | 
            +
                reftext
         | 
| 73 | 
            +
              end
         | 
| 43 74 | 
             
            end
         | 
| 44 75 | 
             
            end
         | 
    
        data/lib/asciidoctor/list.rb
    CHANGED
    
    | @@ -4,11 +4,11 @@ module Asciidoctor | |
| 4 4 | 
             
            class List < AbstractBlock
         | 
| 5 5 |  | 
| 6 6 | 
             
              # Public: Create alias for blocks
         | 
| 7 | 
            -
              alias  | 
| 7 | 
            +
              alias items blocks
         | 
| 8 8 | 
             
              # Public: Get the items in this list as an Array
         | 
| 9 | 
            -
              alias  | 
| 9 | 
            +
              alias content blocks
         | 
| 10 10 | 
             
              # Public: Create alias to check if this list has blocks
         | 
| 11 | 
            -
              alias  | 
| 11 | 
            +
              alias items? blocks?
         | 
| 12 12 |  | 
| 13 13 | 
             
              def initialize parent, context
         | 
| 14 14 | 
             
                super
         | 
| @@ -32,7 +32,7 @@ class List < AbstractBlock | |
| 32 32 | 
             
              end
         | 
| 33 33 |  | 
| 34 34 | 
             
              # Alias render to convert to maintain backwards compatibility
         | 
| 35 | 
            -
              alias  | 
| 35 | 
            +
              alias render convert
         | 
| 36 36 |  | 
| 37 37 | 
             
              def to_s
         | 
| 38 38 | 
             
                %(#<#{self.class}@#{object_id} {context: #{@context.inspect}, style: #{@style.inspect}, items: #{items.size}}>)
         | 
| @@ -44,7 +44,7 @@ end | |
| 44 44 | 
             
            class ListItem < AbstractBlock
         | 
| 45 45 |  | 
| 46 46 | 
             
              # A contextual alias for the list parent node; counterpart to the items alias on List
         | 
| 47 | 
            -
              alias  | 
| 47 | 
            +
              alias list parent
         | 
| 48 48 |  | 
| 49 49 | 
             
              # Public: Get/Set the String used to mark this list item
         | 
| 50 50 | 
             
              attr_accessor :marker
         | 
| @@ -57,14 +57,30 @@ class ListItem < AbstractBlock | |
| 57 57 | 
             
                super parent, :list_item
         | 
| 58 58 | 
             
                @text = text
         | 
| 59 59 | 
             
                @level = parent.level
         | 
| 60 | 
            +
                @subs = NORMAL_SUBS.dup
         | 
| 60 61 | 
             
              end
         | 
| 61 62 |  | 
| 63 | 
            +
              # Public: A convenience method that checks whether the text of this list item
         | 
| 64 | 
            +
              # is not blank (i.e., not nil or empty string).
         | 
| 62 65 | 
             
              def text?
         | 
| 63 66 | 
             
                !@text.nil_or_empty?
         | 
| 64 67 | 
             
              end
         | 
| 65 68 |  | 
| 69 | 
            +
              # Public: Get the String text of this ListItem with substitutions applied.
         | 
| 70 | 
            +
              #
         | 
| 71 | 
            +
              # By default, normal substitutions are applied to the text. The substitutions
         | 
| 72 | 
            +
              # can be modified by altering the subs property of this object.
         | 
| 73 | 
            +
              #
         | 
| 74 | 
            +
              # Returns the converted String text for this ListItem
         | 
| 66 75 | 
             
              def text
         | 
| 67 | 
            -
                apply_subs @text
         | 
| 76 | 
            +
                apply_subs @text, @subs
         | 
| 77 | 
            +
              end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
              # Public: Set the String text.
         | 
| 80 | 
            +
              #
         | 
| 81 | 
            +
              # Returns the new String text assigned to this ListItem
         | 
| 82 | 
            +
              def text= val
         | 
| 83 | 
            +
                @text = val
         | 
| 68 84 | 
             
              end
         | 
| 69 85 |  | 
| 70 86 | 
             
              # Check whether this list item has simple content (no nested blocks aside from a single outline list).
         | 
    
        data/lib/asciidoctor/parser.rb
    CHANGED
    
    | @@ -32,19 +32,48 @@ class Parser | |
| 32 32 | 
             
              # Regexp for leading tab indentation
         | 
| 33 33 | 
             
              TabIndentRx = /^\t+/
         | 
| 34 34 |  | 
| 35 | 
            -
              StartOfBlockProc = lambda {|l| ((l.start_with? '[') && BlockAttributeLineRx  | 
| 35 | 
            +
              StartOfBlockProc = lambda {|l| ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (is_delimited_block? l) }
         | 
| 36 36 |  | 
| 37 | 
            -
              StartOfListProc = lambda {|l| AnyListRx  | 
| 37 | 
            +
              StartOfListProc = lambda {|l| AnyListRx.match? l }
         | 
| 38 38 |  | 
| 39 | 
            -
              StartOfBlockOrListProc = lambda {|l| (is_delimited_block? l) || ((l.start_with? '[') && BlockAttributeLineRx  | 
| 39 | 
            +
              StartOfBlockOrListProc = lambda {|l| (is_delimited_block? l) || ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (AnyListRx.match? l) }
         | 
| 40 40 |  | 
| 41 41 | 
             
              NoOp = nil
         | 
| 42 42 |  | 
| 43 | 
            +
              # Internal: A Hash mapping horizontal alignment abbreviations to alignments
         | 
| 44 | 
            +
              # that can be applied to a table cell (or to all cells in a column)
         | 
| 45 | 
            +
              TableCellHorzAlignments = {
         | 
| 46 | 
            +
                '<' => 'left',
         | 
| 47 | 
            +
                '>' => 'right',
         | 
| 48 | 
            +
                '^' => 'center'
         | 
| 49 | 
            +
              }
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              # Internal: A Hash mapping vertical alignment abbreviations to alignments
         | 
| 52 | 
            +
              # that can be applied to a table cell (or to all cells in a column)
         | 
| 53 | 
            +
              TableCellVertAlignments = {
         | 
| 54 | 
            +
                '<' => 'top',
         | 
| 55 | 
            +
                '>' => 'bottom',
         | 
| 56 | 
            +
                '^' => 'middle'
         | 
| 57 | 
            +
              }
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              # Internal: A Hash mapping styles abbreviations to styles that can be applied
         | 
| 60 | 
            +
              # to a table cell (or to all cells in a column)
         | 
| 61 | 
            +
              TableCellStyles = {
         | 
| 62 | 
            +
                'd' => :none,
         | 
| 63 | 
            +
                's' => :strong,
         | 
| 64 | 
            +
                'e' => :emphasis,
         | 
| 65 | 
            +
                'm' => :monospaced,
         | 
| 66 | 
            +
                'h' => :header,
         | 
| 67 | 
            +
                'l' => :literal,
         | 
| 68 | 
            +
                'v' => :verse,
         | 
| 69 | 
            +
                'a' => :asciidoc
         | 
| 70 | 
            +
              }
         | 
| 71 | 
            +
             | 
| 43 72 | 
             
              # Public: Make sure the Parser object doesn't get initialized.
         | 
| 44 73 | 
             
              #
         | 
| 45 74 | 
             
              # Raises RuntimeError if this constructor is invoked.
         | 
| 46 75 | 
             
              def initialize
         | 
| 47 | 
            -
                raise 'Au contraire, mon frere. No  | 
| 76 | 
            +
                raise 'Au contraire, mon frere. No parser instances will be running around.'
         | 
| 48 77 | 
             
              end
         | 
| 49 78 |  | 
| 50 79 | 
             
              # Public: Parses AsciiDoc source read from the Reader into the Document
         | 
| @@ -62,12 +91,10 @@ class Parser | |
| 62 91 | 
             
              def self.parse(reader, document, options = {})
         | 
| 63 92 | 
             
                block_attributes = parse_document_header(reader, document)
         | 
| 64 93 |  | 
| 65 | 
            -
                 | 
| 66 | 
            -
                   | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            -
                  end
         | 
| 70 | 
            -
                end
         | 
| 94 | 
            +
                while reader.has_more_lines?
         | 
| 95 | 
            +
                  new_section, block_attributes = next_section(reader, document, block_attributes)
         | 
| 96 | 
            +
                  document << new_section if new_section
         | 
| 97 | 
            +
                end unless options[:header_only]
         | 
| 71 98 |  | 
| 72 99 | 
             
                document
         | 
| 73 100 | 
             
              end
         | 
| @@ -86,12 +113,12 @@ class Parser | |
| 86 113 | 
             
              # returns the Hash of orphan block attributes captured above the header
         | 
| 87 114 | 
             
              def self.parse_document_header(reader, document)
         | 
| 88 115 | 
             
                # capture lines of block-level metadata and plow away comment lines that precede first block
         | 
| 89 | 
            -
                block_attributes = parse_block_metadata_lines | 
| 116 | 
            +
                block_attributes = parse_block_metadata_lines reader, document
         | 
| 90 117 |  | 
| 91 118 | 
             
                # special case, block title is not allowed above document title,
         | 
| 92 119 | 
             
                # carry attributes over to the document body
         | 
| 93 | 
            -
                if ( | 
| 94 | 
            -
                    block_attributes. | 
| 120 | 
            +
                if (implicit_doctitle = is_next_line_doctitle? reader, block_attributes, document.attributes['leveloffset']) &&
         | 
| 121 | 
            +
                    (block_attributes.key? 'title')
         | 
| 95 122 | 
             
                  return document.finalize_header block_attributes, false
         | 
| 96 123 | 
             
                end
         | 
| 97 124 |  | 
| @@ -104,16 +131,16 @@ class Parser | |
| 104 131 |  | 
| 105 132 | 
             
                section_title = nil
         | 
| 106 133 | 
             
                # if the first line is the document title, add a header to the document and parse the header metadata
         | 
| 107 | 
            -
                if  | 
| 134 | 
            +
                if implicit_doctitle
         | 
| 108 135 | 
             
                  source_location = reader.cursor if document.sourcemap
         | 
| 109 136 | 
             
                  document.id, _, doctitle, _, single_line = parse_section_title reader, document
         | 
| 110 137 | 
             
                  unless assigned_doctitle
         | 
| 111 138 | 
             
                    document.title = assigned_doctitle = doctitle
         | 
| 112 139 | 
             
                  end
         | 
| 113 140 | 
             
                  # default to compat-mode if document uses atx-style doctitle
         | 
| 114 | 
            -
                  document. | 
| 141 | 
            +
                  document.set_attr 'compat-mode' unless single_line || (document.attribute_locked? 'compat-mode')
         | 
| 115 142 | 
             
                  if (separator = block_attributes.delete 'separator')
         | 
| 116 | 
            -
                    document. | 
| 143 | 
            +
                    document.set_attr 'title-separator', separator unless document.attribute_locked? 'title-separator'
         | 
| 117 144 | 
             
                  end
         | 
| 118 145 | 
             
                  document.header.source_location = source_location if source_location
         | 
| 119 146 | 
             
                  document.attributes['doctitle'] = section_title = doctitle
         | 
| @@ -151,9 +178,9 @@ class Parser | |
| 151 178 | 
             
              #
         | 
| 152 179 | 
             
              # returns Nothing
         | 
| 153 180 | 
             
              def self.parse_manpage_header(reader, document)
         | 
| 154 | 
            -
                if  | 
| 155 | 
            -
                  document.attributes['mantitle'] = document.sub_attributes | 
| 156 | 
            -
                  document.attributes['manvolnum'] =  | 
| 181 | 
            +
                if ManpageTitleVolnumRx =~ document.attributes['doctitle']
         | 
| 182 | 
            +
                  document.attributes['mantitle'] = document.sub_attributes $1.downcase
         | 
| 183 | 
            +
                  document.attributes['manvolnum'] = $2
         | 
| 157 184 | 
             
                else
         | 
| 158 185 | 
             
                  warn %(asciidoctor: ERROR: #{reader.prev_line_info}: malformed manpage title)
         | 
| 159 186 | 
             
                  # provide sensible fallbacks
         | 
| @@ -166,7 +193,7 @@ class Parser | |
| 166 193 | 
             
                if is_next_line_section?(reader, {})
         | 
| 167 194 | 
             
                  name_section = initialize_section(reader, document, {})
         | 
| 168 195 | 
             
                  if name_section.level == 1
         | 
| 169 | 
            -
                    name_section_buffer = reader.read_lines_until(:break_on_blank_lines => true) | 
| 196 | 
            +
                    name_section_buffer = reader.read_lines_until(:break_on_blank_lines => true) * ' '
         | 
| 170 197 | 
             
                    if (m = ManpageNamePurposeRx.match(name_section_buffer))
         | 
| 171 198 | 
             
                      document.attributes['manname'] = document.sub_attributes m[1]
         | 
| 172 199 | 
             
                      document.attributes['manpurpose'] = m[2]
         | 
| @@ -217,37 +244,33 @@ class Parser | |
| 217 244 | 
             
              #   # and hold attributes extracted from header
         | 
| 218 245 | 
             
              #   doc = Document.new
         | 
| 219 246 | 
             
              #
         | 
| 220 | 
            -
              #   Parser.next_section(reader, doc). | 
| 247 | 
            +
              #   Parser.next_section(reader, doc)[0].title
         | 
| 221 248 | 
             
              #   # => "Greetings"
         | 
| 222 249 | 
             
              #
         | 
| 223 | 
            -
              #   Parser.next_section(reader, doc). | 
| 250 | 
            +
              #   Parser.next_section(reader, doc)[0].title
         | 
| 224 251 | 
             
              #   # => "Salutations"
         | 
| 225 252 | 
             
              #
         | 
| 226 253 | 
             
              # returns a two-element Array containing the Section and Hash of orphaned attributes
         | 
| 227 | 
            -
              def self.next_section | 
| 228 | 
            -
                preamble = false
         | 
| 229 | 
            -
                part = false
         | 
| 230 | 
            -
                intro = false
         | 
| 254 | 
            +
              def self.next_section reader, parent, attributes = {}
         | 
| 255 | 
            +
                preamble = intro = part = false
         | 
| 231 256 |  | 
| 232 257 | 
             
                # FIXME if attributes[1] is a verbatim style, then don't check for section
         | 
| 233 258 |  | 
| 234 259 | 
             
                # check if we are at the start of processing the document
         | 
| 235 260 | 
             
                # NOTE we could drop a hint in the attributes to indicate
         | 
| 236 261 | 
             
                # that we are at a section title (so we don't have to check)
         | 
| 237 | 
            -
                if parent.context == :document && parent.blocks.empty? &&
         | 
| 238 | 
            -
                    ( | 
| 239 | 
            -
                  doctype = parent.doctype
         | 
| 262 | 
            +
                if parent.context == :document && parent.blocks.empty? && ((has_header = parent.has_header?) ||
         | 
| 263 | 
            +
                    (attributes.delete 'invalid-header') || !(is_next_line_section? reader, attributes))
         | 
| 264 | 
            +
                  doctype = (document = parent).doctype
         | 
| 240 265 | 
             
                  if has_header || (doctype == 'book' && attributes[1] != 'abstract')
         | 
| 241 | 
            -
                    preamble = intro = Block.new | 
| 242 | 
            -
                    if doctype == 'book' && (parent.attr? 'preface-title')
         | 
| 243 | 
            -
                      preamble.title = parent.attr 'preface-title'
         | 
| 244 | 
            -
                    end
         | 
| 266 | 
            +
                    preamble = intro = (Block.new parent, :preamble, :content_model => :compound)
         | 
| 267 | 
            +
                    preamble.title = parent.attr 'preface-title' if doctype == 'book' && (parent.attr? 'preface-title')
         | 
| 245 268 | 
             
                    parent << preamble
         | 
| 246 269 | 
             
                  end
         | 
| 247 270 | 
             
                  section = parent
         | 
| 248 271 |  | 
| 249 272 | 
             
                  current_level = 0
         | 
| 250 | 
            -
                  if parent.attributes. | 
| 273 | 
            +
                  if parent.attributes.key? 'fragment'
         | 
| 251 274 | 
             
                    expected_next_levels = nil
         | 
| 252 275 | 
             
                  # small tweak to allow subsequent level-0 sections for book doctype
         | 
| 253 276 | 
             
                  elsif doctype == 'book'
         | 
| @@ -256,23 +279,12 @@ class Parser | |
| 256 279 | 
             
                    expected_next_levels = [1]
         | 
| 257 280 | 
             
                  end
         | 
| 258 281 | 
             
                else
         | 
| 259 | 
            -
                  doctype = parent.document.doctype
         | 
| 260 | 
            -
                  section = initialize_section | 
| 261 | 
            -
                  # clear attributes | 
| 262 | 
            -
                  # section title to next block of content
         | 
| 282 | 
            +
                  doctype = (document = parent.document).doctype
         | 
| 283 | 
            +
                  section = initialize_section reader, parent, attributes
         | 
| 284 | 
            +
                  # clear attributes except for title attribute, which must be carried over to next content block
         | 
| 263 285 | 
             
                  attributes = (title = attributes['title']) ? { 'title' => title } : {}
         | 
| 264 | 
            -
                   | 
| 265 | 
            -
                   | 
| 266 | 
            -
                    part = !section.special
         | 
| 267 | 
            -
                    # subsections in preface & appendix in multipart books start at level 2
         | 
| 268 | 
            -
                    if section.special && (['preface', 'appendix'].include? section.sectname)
         | 
| 269 | 
            -
                      expected_next_levels = [current_level + 2]
         | 
| 270 | 
            -
                    else
         | 
| 271 | 
            -
                      expected_next_levels = [current_level + 1]
         | 
| 272 | 
            -
                    end
         | 
| 273 | 
            -
                  else
         | 
| 274 | 
            -
                    expected_next_levels = [current_level + 1]
         | 
| 275 | 
            -
                  end
         | 
| 286 | 
            +
                  part = section.sectname == 'part'
         | 
| 287 | 
            +
                  expected_next_levels = [(current_level = section.level) + 1]
         | 
| 276 288 | 
             
                end
         | 
| 277 289 |  | 
| 278 290 | 
             
                reader.skip_blank_lines
         | 
| @@ -287,20 +299,18 @@ class Parser | |
| 287 299 | 
             
                # We have to parse all the metadata lines before continuing with the loop,
         | 
| 288 300 | 
             
                # otherwise subsequent metadata lines get interpreted as block content
         | 
| 289 301 | 
             
                while reader.has_more_lines?
         | 
| 290 | 
            -
                  parse_block_metadata_lines | 
| 302 | 
            +
                  parse_block_metadata_lines reader, document, attributes
         | 
| 291 303 |  | 
| 292 | 
            -
                  if (next_level = is_next_line_section? | 
| 293 | 
            -
                    next_level +=  | 
| 294 | 
            -
                    if next_level > current_level || ( | 
| 304 | 
            +
                  if (next_level = is_next_line_section?(reader, attributes))
         | 
| 305 | 
            +
                    next_level += document.attr('leveloffset').to_i if document.attr?('leveloffset')
         | 
| 306 | 
            +
                    if next_level > current_level || (next_level == 0 && section.context == :document)
         | 
| 295 307 | 
             
                      if next_level == 0 && doctype != 'book'
         | 
| 296 308 | 
             
                        warn %(asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections)
         | 
| 297 309 | 
             
                      elsif expected_next_levels && !expected_next_levels.include?(next_level)
         | 
| 298 | 
            -
                        warn %(asciidoctor: WARNING: #{reader.line_info}: section title out of sequence: ) | 
| 299 | 
            -
                            %(expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, ) +
         | 
| 300 | 
            -
                            %(got level #{next_level})
         | 
| 310 | 
            +
                        warn %(asciidoctor: WARNING: #{reader.line_info}: section title out of sequence: expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, got level #{next_level})
         | 
| 301 311 | 
             
                      end
         | 
| 302 312 | 
             
                      # the attributes returned are those that are orphaned
         | 
| 303 | 
            -
                      new_section, attributes = next_section | 
| 313 | 
            +
                      new_section, attributes = next_section reader, section, attributes
         | 
| 304 314 | 
             
                      section << new_section
         | 
| 305 315 | 
             
                    else
         | 
| 306 316 | 
             
                      if next_level == 0 && doctype != 'book'
         | 
| @@ -312,7 +322,7 @@ class Parser | |
| 312 322 | 
             
                  else
         | 
| 313 323 | 
             
                    # just take one block or else we run the risk of overrunning section boundaries
         | 
| 314 324 | 
             
                    block_line_info = reader.line_info
         | 
| 315 | 
            -
                    if (new_block = next_block reader,  | 
| 325 | 
            +
                    if (new_block = next_block reader, intro || section, attributes, :parse_metadata => false)
         | 
| 316 326 | 
             
                      # REVIEW this may be doing too much
         | 
| 317 327 | 
             
                      if part
         | 
| 318 328 | 
             
                        if !section.blocks?
         | 
| @@ -372,7 +382,6 @@ class Parser | |
| 372 382 | 
             
                # that would require reworking assumptions in next_section since the preamble
         | 
| 373 383 | 
             
                # is treated like an untitled section
         | 
| 374 384 | 
             
                elsif preamble # implies parent == document
         | 
| 375 | 
            -
                  document = parent
         | 
| 376 385 | 
             
                  if preamble.blocks?
         | 
| 377 386 | 
             
                    # unwrap standalone preamble (i.e., no sections), if permissible
         | 
| 378 387 | 
             
                    if Compliance.unwrap_standalone_preamble && document.blocks.size == 1 && doctype != 'book'
         | 
| @@ -395,23 +404,26 @@ class Parser | |
| 395 404 | 
             
                [section != parent ? section : nil, attributes.dup]
         | 
| 396 405 | 
             
              end
         | 
| 397 406 |  | 
| 398 | 
            -
              # Public:  | 
| 407 | 
            +
              # Public: Parse and return the next Block at the Reader's current location
         | 
| 399 408 | 
             
              #
         | 
| 400 | 
            -
              #  | 
| 401 | 
            -
              #  | 
| 402 | 
            -
              #  | 
| 409 | 
            +
              # This method begins by skipping over blank lines to find the start of the
         | 
| 410 | 
            +
              # next block (paragraph, block macro, or delimited block). If a block is
         | 
| 411 | 
            +
              # found, that block is parsed, initialized as a Block object, and returned.
         | 
| 412 | 
            +
              # Otherwise, the method returns nothing.
         | 
| 403 413 | 
             
              #
         | 
| 404 | 
            -
              #  | 
| 405 | 
            -
              #  | 
| 406 | 
            -
              #  | 
| 414 | 
            +
              # Regular expressions from the Asciidoctor module are used to match block
         | 
| 415 | 
            +
              # boundaries. The ensuing lines are then processed according to the content
         | 
| 416 | 
            +
              # model.
         | 
| 407 417 | 
             
              #
         | 
| 408 | 
            -
              # reader | 
| 409 | 
            -
              # parent | 
| 418 | 
            +
              # reader     - The Reader from which to retrieve the next Block.
         | 
| 419 | 
            +
              # parent     - The Document, Section or Block to which the next Block belongs.
         | 
| 420 | 
            +
              # attributes - A Hash of attributes that will become the attributes
         | 
| 421 | 
            +
              #              associated with the parsed Block (default: {}).
         | 
| 422 | 
            +
              # options    - An options Hash to control parsing (default: {}):
         | 
| 423 | 
            +
              #              * :text indicates that the parser is only looking for text content
         | 
| 410 424 | 
             
              #
         | 
| 411 | 
            -
              # Returns a  | 
| 412 | 
            -
               | 
| 413 | 
            -
              # QUESTION should next_block have an option for whether it should keep looking until
         | 
| 414 | 
            -
              # a block is found? right now it bails when it encounters a line to be skipped
         | 
| 425 | 
            +
              # Returns a Block object built from the parsed content of the processed
         | 
| 426 | 
            +
              # lines, or nothing if no block is found.
         | 
| 415 427 | 
             
              def self.next_block(reader, parent, attributes = {}, options = {})
         | 
| 416 428 | 
             
                # Skip ahead to the block content
         | 
| 417 429 | 
             
                skipped = reader.skip_blank_lines
         | 
| @@ -423,121 +435,109 @@ class Parser | |
| 423 435 | 
             
                # if skipped a line, assume a list continuation was
         | 
| 424 436 | 
             
                # used and block content is acceptable
         | 
| 425 437 | 
             
                if (text_only = options[:text]) && skipped > 0
         | 
| 426 | 
            -
                  options.delete | 
| 438 | 
            +
                  options.delete :text
         | 
| 427 439 | 
             
                  text_only = false
         | 
| 428 440 | 
             
                end
         | 
| 429 441 |  | 
| 430 | 
            -
                parse_metadata = options.fetch(:parse_metadata, true)
         | 
| 431 | 
            -
                #parse_sections = options.fetch(:parse_sections, false)
         | 
| 432 | 
            -
             | 
| 433 442 | 
             
                document = parent.document
         | 
| 443 | 
            +
             | 
| 444 | 
            +
                if options.fetch :parse_metadata, true
         | 
| 445 | 
            +
                  # read lines until there are no more metadata lines to read
         | 
| 446 | 
            +
                  while parse_block_metadata_line reader, document, attributes, options
         | 
| 447 | 
            +
                    advanced = reader.advance
         | 
| 448 | 
            +
                  end
         | 
| 449 | 
            +
                  if advanced && !reader.has_more_lines?
         | 
| 450 | 
            +
                    # NOTE there are no cases when these attributes are used, but clear them anyway
         | 
| 451 | 
            +
                    attributes.clear
         | 
| 452 | 
            +
                    return
         | 
| 453 | 
            +
                  end
         | 
| 454 | 
            +
                end
         | 
| 455 | 
            +
             | 
| 434 456 | 
             
                if (extensions = document.extensions)
         | 
| 435 | 
            -
                  block_extensions = extensions.blocks?
         | 
| 436 | 
            -
             | 
| 437 | 
            -
             | 
| 438 | 
            -
             | 
| 439 | 
            -
                 | 
| 440 | 
            -
                 | 
| 441 | 
            -
                 | 
| 442 | 
            -
                 | 
| 443 | 
            -
             | 
| 444 | 
            -
                 | 
| 445 | 
            -
             | 
| 446 | 
            -
             | 
| 447 | 
            -
             | 
| 448 | 
            -
             | 
| 449 | 
            -
                   | 
| 450 | 
            -
             | 
| 451 | 
            -
             | 
| 452 | 
            -
                     | 
| 453 | 
            -
             | 
| 454 | 
            -
             | 
| 455 | 
            -
             | 
| 456 | 
            -
             | 
| 457 | 
            -
             | 
| 458 | 
            -
             | 
| 459 | 
            -
                  source_location = reader.cursor if sourcemap
         | 
| 460 | 
            -
                  this_line = reader.read_line
         | 
| 461 | 
            -
                  delimited_block = false
         | 
| 462 | 
            -
                  block_context = nil
         | 
| 463 | 
            -
                  cloaked_context = nil
         | 
| 464 | 
            -
                  terminator = nil
         | 
| 465 | 
            -
                  # QUESTION put this inside call to rekey attributes?
         | 
| 466 | 
            -
                  if attributes[1]
         | 
| 467 | 
            -
                    style, explicit_style = parse_style_attribute(attributes, reader)
         | 
| 468 | 
            -
                  end
         | 
| 469 | 
            -
             | 
| 470 | 
            -
                  if (delimited_blk_match = is_delimited_block? this_line, true)
         | 
| 471 | 
            -
                    delimited_block = true
         | 
| 472 | 
            -
                    block_context = cloaked_context = delimited_blk_match.context
         | 
| 473 | 
            -
                    terminator = delimited_blk_match.terminator
         | 
| 474 | 
            -
                    if !style
         | 
| 475 | 
            -
                      style = attributes['style'] = block_context.to_s
         | 
| 476 | 
            -
                    elsif style != block_context.to_s
         | 
| 477 | 
            -
                      if delimited_blk_match.masq.include? style
         | 
| 478 | 
            -
                        block_context = style.to_sym
         | 
| 479 | 
            -
                      elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
         | 
| 480 | 
            -
                        block_context = :admonition
         | 
| 481 | 
            -
                      elsif block_extensions && extensions.registered_for_block?(style, block_context)
         | 
| 482 | 
            -
                        block_context = style.to_sym
         | 
| 483 | 
            -
                      else
         | 
| 484 | 
            -
                        warn %(asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for #{block_context} block: #{style})
         | 
| 485 | 
            -
                        style = block_context.to_s
         | 
| 486 | 
            -
                      end
         | 
| 457 | 
            +
                  block_extensions, block_macro_extensions = extensions.blocks?, extensions.block_macros?
         | 
| 458 | 
            +
                end
         | 
| 459 | 
            +
             | 
| 460 | 
            +
                # QUESTION should we introduce a parsing context object?
         | 
| 461 | 
            +
                source_location = reader.cursor if document.sourcemap
         | 
| 462 | 
            +
                this_path, this_lineno, this_line, in_list = reader.path, reader.lineno, reader.read_line, ListItem === parent
         | 
| 463 | 
            +
                block = block_context = cloaked_context = terminator = nil
         | 
| 464 | 
            +
                style = attributes[1] ? (parse_style_attribute attributes, reader) : nil
         | 
| 465 | 
            +
             | 
| 466 | 
            +
                if (delimited_block = is_delimited_block? this_line, true)
         | 
| 467 | 
            +
                  block_context = cloaked_context = delimited_block.context
         | 
| 468 | 
            +
                  terminator = delimited_block.terminator
         | 
| 469 | 
            +
                  if !style
         | 
| 470 | 
            +
                    style = attributes['style'] = block_context.to_s
         | 
| 471 | 
            +
                  elsif style != block_context.to_s
         | 
| 472 | 
            +
                    if delimited_block.masq.include? style
         | 
| 473 | 
            +
                      block_context = style.to_sym
         | 
| 474 | 
            +
                    elsif delimited_block.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
         | 
| 475 | 
            +
                      block_context = :admonition
         | 
| 476 | 
            +
                    elsif block_extensions && extensions.registered_for_block?(style, block_context)
         | 
| 477 | 
            +
                      block_context = style.to_sym
         | 
| 478 | 
            +
                    else
         | 
| 479 | 
            +
                      warn %(asciidoctor: WARNING: #{this_path}: line #{this_lineno}: invalid style for #{block_context} block: #{style})
         | 
| 480 | 
            +
                      style = block_context.to_s
         | 
| 487 481 | 
             
                    end
         | 
| 488 482 | 
             
                  end
         | 
| 483 | 
            +
                end
         | 
| 489 484 |  | 
| 490 | 
            -
             | 
| 491 | 
            -
             | 
| 492 | 
            -
             | 
| 493 | 
            -
             | 
| 494 | 
            -
             | 
| 495 | 
            -
             | 
| 496 | 
            -
                     | 
| 485 | 
            +
                # this loop is used for flow control; it only executes once, and only when delimited_block is set
         | 
| 486 | 
            +
                # break once a block is found or at end of loop
         | 
| 487 | 
            +
                # returns nil if the line should be dropped
         | 
| 488 | 
            +
                while true
         | 
| 489 | 
            +
                  # process lines verbatim
         | 
| 490 | 
            +
                  if style && Compliance.strict_verbatim_paragraphs && VERBATIM_STYLES.include?(style)
         | 
| 491 | 
            +
                    block_context = style.to_sym
         | 
| 492 | 
            +
                    reader.unshift_line this_line
         | 
| 493 | 
            +
                    # advance to block parsing =>
         | 
| 494 | 
            +
                    break
         | 
| 495 | 
            +
                  end
         | 
| 497 496 |  | 
| 498 | 
            -
             | 
| 499 | 
            -
             | 
| 500 | 
            -
             | 
| 501 | 
            -
             | 
| 502 | 
            -
             | 
| 497 | 
            +
                  # process lines normally
         | 
| 498 | 
            +
                  if text_only
         | 
| 499 | 
            +
                    indented = this_line.start_with? ' ', TAB
         | 
| 500 | 
            +
                  else
         | 
| 501 | 
            +
                    # NOTE move this declaration up if we need it when text_only is false
         | 
| 502 | 
            +
                    md_syntax = Compliance.markdown_syntax
         | 
| 503 | 
            +
                    if this_line.start_with? ' '
         | 
| 504 | 
            +
                      indented, ch0 = true, ' '
         | 
| 505 | 
            +
                      # QUESTION should we test line length?
         | 
| 506 | 
            +
                      if md_syntax && this_line.lstrip.start_with?(*MARKDOWN_THEMATIC_BREAK_CHARS.keys) &&
         | 
| 507 | 
            +
                          #!(this_line.start_with? '    ') &&
         | 
| 508 | 
            +
                          (MarkdownThematicBreakRx.match? this_line)
         | 
| 509 | 
            +
                        # NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
         | 
| 510 | 
            +
                        block = Block.new(parent, :thematic_break, :content_model => :empty)
         | 
| 503 511 | 
             
                        break
         | 
| 504 512 | 
             
                      end
         | 
| 505 | 
            -
             | 
| 506 | 
            -
                       | 
| 507 | 
            -
             | 
| 508 | 
            -
             | 
| 513 | 
            +
                    elsif this_line.start_with? TAB
         | 
| 514 | 
            +
                      indented, ch0 = true, TAB
         | 
| 515 | 
            +
                    else
         | 
| 516 | 
            +
                      indented, ch0 = false, this_line.chr
         | 
| 517 | 
            +
                      layout_break_chars = md_syntax ? HYBRID_LAYOUT_BREAK_CHARS : LAYOUT_BREAK_CHARS
         | 
| 518 | 
            +
                      if (layout_break_chars.key? ch0) && (md_syntax ? (HybridLayoutBreakRx.match? this_line) :
         | 
| 519 | 
            +
                          (this_line == ch0 * (ll = this_line.length) && ll > 2))
         | 
| 509 520 | 
             
                        # NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
         | 
| 510 | 
            -
                         | 
| 511 | 
            -
             | 
| 512 | 
            -
             | 
| 513 | 
            -
             | 
| 514 | 
            -
             | 
| 515 | 
            -
                         | 
| 516 | 
            -
                          blk_ctx = match[1].to_sym
         | 
| 521 | 
            +
                        block = Block.new(parent, layout_break_chars[ch0], :content_model => :empty)
         | 
| 522 | 
            +
                        break
         | 
| 523 | 
            +
                      # NOTE very rare that a text-only line will end in ] (e.g., inline macro), so check that first
         | 
| 524 | 
            +
                      elsif (this_line.end_with? ']') && (this_line.include? '::')
         | 
| 525 | 
            +
                        #if (this_line.start_with? 'image', 'video', 'audio') && (match = BlockMediaMacroRx.match(this_line))
         | 
| 526 | 
            +
                        if (ch0 == 'i' || (this_line.start_with? 'video:', 'audio:')) && (match = BlockMediaMacroRx.match(this_line))
         | 
| 527 | 
            +
                          blk_ctx, target = match[1].to_sym, match[2]
         | 
| 517 528 | 
             
                          block = Block.new(parent, blk_ctx, :content_model => :empty)
         | 
| 518 | 
            -
                           | 
| 519 | 
            -
             | 
| 520 | 
            -
                          elsif blk_ctx == :video
         | 
| 529 | 
            +
                          case blk_ctx
         | 
| 530 | 
            +
                          when :video
         | 
| 521 531 | 
             
                            posattrs = ['poster', 'width', 'height']
         | 
| 522 | 
            -
                           | 
| 532 | 
            +
                          when :audio
         | 
| 523 533 | 
             
                            posattrs = []
         | 
| 534 | 
            +
                          else # :image
         | 
| 535 | 
            +
                            posattrs = ['alt', 'width', 'height']
         | 
| 524 536 | 
             
                          end
         | 
| 525 | 
            -
             | 
| 526 | 
            -
                          #  | 
| 527 | 
            -
                           | 
| 528 | 
            -
                          if  | 
| 529 | 
            -
                            attributes['alt'] = style if blk_ctx == :image
         | 
| 530 | 
            -
                            attributes.delete 'style'
         | 
| 531 | 
            -
                            style = nil
         | 
| 532 | 
            -
                          end
         | 
| 533 | 
            -
             | 
| 534 | 
            -
                          block.parse_attributes(match[3], posattrs,
         | 
| 535 | 
            -
                              :unescape_input => (blk_ctx == :image),
         | 
| 536 | 
            -
                              :sub_input => true,
         | 
| 537 | 
            -
                              :sub_result => false,
         | 
| 538 | 
            -
                              :into => attributes)
         | 
| 539 | 
            -
                          target = block.sub_attributes(match[2], :attribute_missing => 'drop-line')
         | 
| 540 | 
            -
                          if target.empty?
         | 
| 537 | 
            +
                          block.parse_attributes(match[3], posattrs, :sub_input => true, :sub_result => false, :into => attributes)
         | 
| 538 | 
            +
                          # style doesn't have special meaning for media macros
         | 
| 539 | 
            +
                          attributes.delete 'style' if attributes.key? 'style'
         | 
| 540 | 
            +
                          if (target.include? '{') && (target = block.sub_attributes target, :attribute_missing => 'drop-line').empty?
         | 
| 541 541 | 
             
                            # retain as unparsed if attribute-missing is skip
         | 
| 542 542 | 
             
                            if document.attributes.fetch('attribute-missing', Compliance.attribute_missing) == 'skip'
         | 
| 543 543 | 
             
                              return Block.new(parent, :paragraph, :content_model => :simple, :source => [this_line])
         | 
| @@ -547,414 +547,382 @@ class Parser | |
| 547 547 | 
             
                              return
         | 
| 548 548 | 
             
                            end
         | 
| 549 549 | 
             
                          end
         | 
| 550 | 
            -
             | 
| 550 | 
            +
                          if blk_ctx == :image
         | 
| 551 | 
            +
                            block.document.register :images, target
         | 
| 552 | 
            +
                            # NOTE style is the value of the first positional attribute in the block attribute line
         | 
| 553 | 
            +
                            attributes['alt'] ||= style || (attributes['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
         | 
| 554 | 
            +
                            unless (scaledwidth = attributes.delete 'scaledwidth').nil_or_empty?
         | 
| 555 | 
            +
                              # NOTE assume % units if not specified
         | 
| 556 | 
            +
                              attributes['scaledwidth'] = (TrailingDigitsRx.match? scaledwidth) ? %(#{scaledwidth}%) : scaledwidth
         | 
| 557 | 
            +
                            end
         | 
| 558 | 
            +
                            block.title = attributes.delete 'title'
         | 
| 559 | 
            +
                            block.assign_caption((attributes.delete 'caption'), 'figure')
         | 
| 560 | 
            +
                          end
         | 
| 551 561 | 
             
                          attributes['target'] = target
         | 
| 552 | 
            -
                          # now done down below
         | 
| 553 | 
            -
                          #block.title = attributes.delete('title') if attributes.has_key?('title')
         | 
| 554 | 
            -
                          #if blk_ctx == :image
         | 
| 555 | 
            -
                          #  if attributes.has_key? 'scaledwidth'
         | 
| 556 | 
            -
                          #    # append % to scaledwidth if ends in number (no units present)
         | 
| 557 | 
            -
                          #    if (48..57).include?((attributes['scaledwidth'][-1] || 0).ord)
         | 
| 558 | 
            -
                          #      attributes['scaledwidth'] = %(#{attributes['scaledwidth']}%)
         | 
| 559 | 
            -
                          #    end
         | 
| 560 | 
            -
                          #  end
         | 
| 561 | 
            -
                          #  document.register(:images, target)
         | 
| 562 | 
            -
                          #  attributes['alt'] ||= Helpers.basename(target, true).tr('_-', ' ')
         | 
| 563 | 
            -
                          #  # QUESTION should video or audio have an auto-numbered caption?
         | 
| 564 | 
            -
                          #  block.assign_caption attributes.delete('caption'), 'figure'
         | 
| 565 | 
            -
                          #end
         | 
| 566 562 | 
             
                          break
         | 
| 567 563 |  | 
| 568 | 
            -
                         | 
| 569 | 
            -
                        elsif first_char == 't' && (match = TocBlockMacroRx.match(this_line))
         | 
| 564 | 
            +
                        elsif ch0 == 't' && (this_line.start_with? 'toc:') && (match = BlockTocMacroRx.match(this_line))
         | 
| 570 565 | 
             
                          block = Block.new(parent, :toc, :content_model => :empty)
         | 
| 571 566 | 
             
                          block.parse_attributes(match[1], [], :sub_result => false, :into => attributes)
         | 
| 572 567 | 
             
                          break
         | 
| 573 568 |  | 
| 574 | 
            -
                        elsif block_macro_extensions && (match =  | 
| 569 | 
            +
                        elsif block_macro_extensions && (match = CustomBlockMacroRx.match(this_line)) &&
         | 
| 575 570 | 
             
                            (extension = extensions.registered_for_block_macro?(match[1]))
         | 
| 576 571 | 
             
                          target = match[2]
         | 
| 577 | 
            -
                           | 
| 572 | 
            +
                          content = match[3]
         | 
| 578 573 | 
             
                          if extension.config[:content_model] == :attributes
         | 
| 579 | 
            -
                            unless  | 
| 580 | 
            -
                              document.parse_attributes( | 
| 574 | 
            +
                            unless content.empty?
         | 
| 575 | 
            +
                              document.parse_attributes(content, extension.config[:pos_attrs] || [],
         | 
| 581 576 | 
             
                                  :sub_input => true, :sub_result => false, :into => attributes)
         | 
| 582 577 | 
             
                            end
         | 
| 583 578 | 
             
                          else
         | 
| 584 | 
            -
                            attributes['text'] =  | 
| 579 | 
            +
                            attributes['text'] = content
         | 
| 585 580 | 
             
                          end
         | 
| 586 581 | 
             
                          if (default_attrs = extension.config[:default_attrs])
         | 
| 587 | 
            -
                            default_attrs | 
| 582 | 
            +
                            attributes.update(default_attrs) {|_, old_v| old_v }
         | 
| 588 583 | 
             
                          end
         | 
| 589 | 
            -
                          if (block = extension.process_method[parent, target, attributes | 
| 584 | 
            +
                          if (block = extension.process_method[parent, target, attributes])
         | 
| 590 585 | 
             
                            attributes.replace block.attributes
         | 
| 586 | 
            +
                            break
         | 
| 591 587 | 
             
                          else
         | 
| 592 588 | 
             
                            attributes.clear
         | 
| 593 589 | 
             
                            return
         | 
| 594 590 | 
             
                          end
         | 
| 595 | 
            -
                          break
         | 
| 596 591 | 
             
                        end
         | 
| 597 592 | 
             
                      end
         | 
| 593 | 
            +
                    end
         | 
| 594 | 
            +
                  end
         | 
| 598 595 |  | 
| 599 | 
            -
             | 
| 600 | 
            -
             | 
| 601 | 
            -
             | 
| 602 | 
            -
             | 
| 603 | 
            -
             | 
| 604 | 
            -
             | 
| 605 | 
            -
             | 
| 606 | 
            -
             | 
| 607 | 
            -
             | 
| 608 | 
            -
             | 
| 609 | 
            -
             | 
| 610 | 
            -
             | 
| 611 | 
            -
             | 
| 612 | 
            -
             | 
| 613 | 
            -
             | 
| 614 | 
            -
             | 
| 615 | 
            -
             | 
| 616 | 
            -
             | 
| 617 | 
            -
                            if !coids.empty?
         | 
| 618 | 
            -
                              list_item.attributes['coids'] = coids
         | 
| 619 | 
            -
                            else
         | 
| 620 | 
            -
                              # FIXME this lineno - 2 hack means we need a proper look-behind cursor
         | 
| 621 | 
            -
                              warn %(asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: no callouts refer to list item #{block.items.size})
         | 
| 622 | 
            -
                            end
         | 
| 623 | 
            -
                          end
         | 
| 624 | 
            -
                          match = nil
         | 
| 625 | 
            -
                        end
         | 
| 626 | 
            -
             | 
| 627 | 
            -
                        document.callouts.next_list
         | 
| 628 | 
            -
                        break
         | 
| 629 | 
            -
             | 
| 630 | 
            -
                      elsif UnorderedListRx =~ this_line
         | 
| 631 | 
            -
                        reader.unshift_line this_line
         | 
| 632 | 
            -
                        block = next_outline_list(reader, :ulist, parent)
         | 
| 633 | 
            -
                        break
         | 
| 634 | 
            -
             | 
| 635 | 
            -
                      elsif (match = OrderedListRx.match(this_line))
         | 
| 636 | 
            -
                        reader.unshift_line this_line
         | 
| 637 | 
            -
                        block = next_outline_list(reader, :olist, parent)
         | 
| 638 | 
            -
                        # TODO move this logic into next_outline_list
         | 
| 639 | 
            -
                        if !attributes['style'] && !block.attributes['style']
         | 
| 640 | 
            -
                          marker = block.items[0].marker
         | 
| 641 | 
            -
                          if marker.start_with? '.'
         | 
| 642 | 
            -
                            # first one makes more sense, but second one is AsciiDoc-compliant
         | 
| 643 | 
            -
                            #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES[0]).to_s
         | 
| 644 | 
            -
                            attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES[0]).to_s
         | 
| 645 | 
            -
                          else
         | 
| 646 | 
            -
                            style = ORDERED_LIST_STYLES.find {|s| OrderedListMarkerRxMap[s] =~ marker }
         | 
| 647 | 
            -
                            attributes['style'] = (style || ORDERED_LIST_STYLES[0]).to_s
         | 
| 648 | 
            -
                          end
         | 
| 649 | 
            -
                        end
         | 
| 650 | 
            -
                        break
         | 
| 651 | 
            -
             | 
| 652 | 
            -
                      elsif (match = DescriptionListRx.match(this_line))
         | 
| 653 | 
            -
                        reader.unshift_line this_line
         | 
| 654 | 
            -
                        block = next_labeled_list(reader, match, parent)
         | 
| 655 | 
            -
                        break
         | 
| 656 | 
            -
             | 
| 657 | 
            -
                      elsif (style == 'float' || style == 'discrete') &&
         | 
| 658 | 
            -
                          is_section_title?(this_line, (Compliance.underline_style_section_titles ? reader.peek_line(true) : nil))
         | 
| 659 | 
            -
                        reader.unshift_line this_line
         | 
| 660 | 
            -
                        float_id, float_reftext, float_title, float_level, _ = parse_section_title(reader, document)
         | 
| 661 | 
            -
                        attributes['reftext'] = float_reftext if float_reftext
         | 
| 662 | 
            -
                        float_id ||= attributes['id'] if attributes.has_key?('id')
         | 
| 663 | 
            -
                        block = Block.new(parent, :floating_title, :content_model => :empty)
         | 
| 664 | 
            -
                        if float_id.nil_or_empty?
         | 
| 665 | 
            -
                          # FIXME remove hack of creating throwaway Section to get at the generate_id method
         | 
| 666 | 
            -
                          tmp_sect = Section.new(parent)
         | 
| 667 | 
            -
                          tmp_sect.title = float_title
         | 
| 668 | 
            -
                          block.id = tmp_sect.generate_id
         | 
| 669 | 
            -
                        else
         | 
| 670 | 
            -
                          block.id = float_id
         | 
| 671 | 
            -
                        end
         | 
| 672 | 
            -
                        block.level = float_level
         | 
| 673 | 
            -
                        block.title = float_title
         | 
| 674 | 
            -
                        break
         | 
| 675 | 
            -
             | 
| 676 | 
            -
                      # FIXME create another set for "passthrough" styles
         | 
| 677 | 
            -
                      # FIXME make this more DRY!
         | 
| 678 | 
            -
                      elsif style && style != 'normal'
         | 
| 679 | 
            -
                        if PARAGRAPH_STYLES.include?(style)
         | 
| 680 | 
            -
                          block_context = style.to_sym
         | 
| 681 | 
            -
                          cloaked_context = :paragraph
         | 
| 682 | 
            -
                          reader.unshift_line this_line
         | 
| 683 | 
            -
                          # advance to block parsing =>
         | 
| 684 | 
            -
                          break
         | 
| 685 | 
            -
                        elsif ADMONITION_STYLES.include?(style)
         | 
| 686 | 
            -
                          block_context = :admonition
         | 
| 687 | 
            -
                          cloaked_context = :paragraph
         | 
| 688 | 
            -
                          reader.unshift_line this_line
         | 
| 689 | 
            -
                          # advance to block parsing =>
         | 
| 690 | 
            -
                          break
         | 
| 691 | 
            -
                        elsif block_extensions && extensions.registered_for_block?(style, :paragraph)
         | 
| 692 | 
            -
                          block_context = style.to_sym
         | 
| 693 | 
            -
                          cloaked_context = :paragraph
         | 
| 694 | 
            -
                          reader.unshift_line this_line
         | 
| 695 | 
            -
                          # advance to block parsing =>
         | 
| 696 | 
            -
                          break
         | 
| 596 | 
            +
                  # haven't found anything yet, continue
         | 
| 597 | 
            +
                  if !indented && CALLOUT_LIST_LEADERS.include?(ch0 ||= this_line.chr) &&
         | 
| 598 | 
            +
                      (CalloutListSniffRx.match? this_line) && (match = CalloutListRx.match this_line)
         | 
| 599 | 
            +
                    block = List.new(parent, :colist)
         | 
| 600 | 
            +
                    attributes['style'] = 'arabic'
         | 
| 601 | 
            +
                    reader.unshift_line this_line
         | 
| 602 | 
            +
                    expected_index = 1
         | 
| 603 | 
            +
                    # NOTE skip the match on the first time through as we've already done it (emulates begin...while)
         | 
| 604 | 
            +
                    while match || (reader.has_more_lines? && (match = CalloutListRx.match(reader.peek_line)))
         | 
| 605 | 
            +
                      list_item_lineno = reader.lineno
         | 
| 606 | 
            +
                      # might want to move this check to a validate method
         | 
| 607 | 
            +
                      unless match[1] == expected_index.to_s
         | 
| 608 | 
            +
                        warn %(asciidoctor: WARNING: #{reader.path}: line #{list_item_lineno}: callout list item index: expected #{expected_index} got #{match[1]})
         | 
| 609 | 
            +
                      end
         | 
| 610 | 
            +
                      if (list_item = next_list_item reader, block, match)
         | 
| 611 | 
            +
                        block << list_item
         | 
| 612 | 
            +
                        if (coids = document.callouts.callout_ids block.items.size).empty?
         | 
| 613 | 
            +
                          warn %(asciidoctor: WARNING: #{reader.path}: line #{list_item_lineno}: no callouts refer to list item #{block.items.size})
         | 
| 697 614 | 
             
                        else
         | 
| 698 | 
            -
                           | 
| 699 | 
            -
                          style = nil
         | 
| 700 | 
            -
                          # continue to process paragraph
         | 
| 615 | 
            +
                          list_item.attributes['coids'] = coids
         | 
| 701 616 | 
             
                        end
         | 
| 702 617 | 
             
                      end
         | 
| 618 | 
            +
                      expected_index += 1
         | 
| 619 | 
            +
                      match = nil
         | 
| 620 | 
            +
                    end
         | 
| 703 621 |  | 
| 704 | 
            -
             | 
| 705 | 
            -
             | 
| 706 | 
            -
                      # a literal paragraph is contiguous lines starting at least one space
         | 
| 707 | 
            -
                      if style != 'normal' && LiteralParagraphRx =~ this_line
         | 
| 708 | 
            -
                        # So we need to actually include this one in the read_lines group
         | 
| 709 | 
            -
                        reader.unshift_line this_line
         | 
| 710 | 
            -
                        lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => text_only
         | 
| 711 | 
            -
             | 
| 712 | 
            -
                        adjust_indentation! lines
         | 
| 622 | 
            +
                    document.callouts.next_list
         | 
| 623 | 
            +
                    break
         | 
| 713 624 |  | 
| 714 | 
            -
             | 
| 715 | 
            -
             | 
| 716 | 
            -
             | 
| 717 | 
            -
             | 
| 625 | 
            +
                  elsif UnorderedListRx.match? this_line
         | 
| 626 | 
            +
                    reader.unshift_line this_line
         | 
| 627 | 
            +
                    block = next_item_list(reader, :ulist, parent)
         | 
| 628 | 
            +
                    if (style || (Section === parent && parent.sectname)) == 'bibliography'
         | 
| 629 | 
            +
                      attributes['style'] = 'bibliography' unless style
         | 
| 630 | 
            +
                      block.items.each {|item| catalog_inline_biblio_anchor item.instance_variable_get(:@text), item, document }
         | 
| 631 | 
            +
                    end
         | 
| 632 | 
            +
                    break
         | 
| 718 633 |  | 
| 719 | 
            -
             | 
| 634 | 
            +
                  elsif (match = OrderedListRx.match(this_line))
         | 
| 635 | 
            +
                    reader.unshift_line this_line
         | 
| 636 | 
            +
                    block = next_item_list(reader, :olist, parent)
         | 
| 637 | 
            +
                    # FIXME move this logic into next_item_list
         | 
| 638 | 
            +
                    unless style
         | 
| 639 | 
            +
                      marker = block.items[0].marker
         | 
| 640 | 
            +
                      if marker.start_with? '.'
         | 
| 641 | 
            +
                        # first one makes more sense, but second one is AsciiDoc-compliant
         | 
| 642 | 
            +
                        # TODO control behavior using a compliance setting
         | 
| 643 | 
            +
                        #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || 'arabic').to_s
         | 
| 644 | 
            +
                        attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || 'arabic').to_s
         | 
| 720 645 | 
             
                      else
         | 
| 721 | 
            -
                         | 
| 722 | 
            -
             | 
| 723 | 
            -
             | 
| 724 | 
            -
             | 
| 725 | 
            -
                        # line comments, which may leave us w/ an empty buffer if those
         | 
| 726 | 
            -
                        # were the only lines found
         | 
| 727 | 
            -
                        if lines.empty?
         | 
| 728 | 
            -
                          # call advance since the reader preserved the last line
         | 
| 729 | 
            -
                          reader.advance
         | 
| 730 | 
            -
                          return
         | 
| 731 | 
            -
                        end
         | 
| 732 | 
            -
             | 
| 733 | 
            -
                        catalog_inline_anchors(lines.join(EOL), document)
         | 
| 734 | 
            -
             | 
| 735 | 
            -
                        first_line = lines[0]
         | 
| 736 | 
            -
                        if !text_only && (admonition_match = AdmonitionParagraphRx.match(first_line))
         | 
| 737 | 
            -
                          lines[0] = admonition_match.post_match.lstrip
         | 
| 738 | 
            -
                          attributes['style'] = admonition_match[1]
         | 
| 739 | 
            -
                          attributes['name'] = admonition_name = admonition_match[1].downcase
         | 
| 740 | 
            -
                          attributes['caption'] ||= document.attributes[%(#{admonition_name}-caption)]
         | 
| 741 | 
            -
                          block = Block.new(parent, :admonition, :content_model => :simple, :source => lines, :attributes => attributes)
         | 
| 742 | 
            -
                        elsif !text_only && Compliance.markdown_syntax && first_line.start_with?('> ')
         | 
| 743 | 
            -
                          lines.map! {|line|
         | 
| 744 | 
            -
                            if line == '>'
         | 
| 745 | 
            -
                              line[1..-1]
         | 
| 746 | 
            -
                            elsif line.start_with? '> '
         | 
| 747 | 
            -
                              line[2..-1]
         | 
| 748 | 
            -
                            else
         | 
| 749 | 
            -
                              line
         | 
| 750 | 
            -
                            end
         | 
| 751 | 
            -
                          }
         | 
| 646 | 
            +
                        attributes['style'] = (ORDERED_LIST_STYLES.find {|s| OrderedListMarkerRxMap[s].match? marker } || 'arabic').to_s
         | 
| 647 | 
            +
                      end
         | 
| 648 | 
            +
                    end
         | 
| 649 | 
            +
                    break
         | 
| 752 650 |  | 
| 753 | 
            -
             | 
| 754 | 
            -
             | 
| 755 | 
            -
             | 
| 756 | 
            -
             | 
| 757 | 
            -
                            attribution, citetitle = nil
         | 
| 758 | 
            -
                          end
         | 
| 759 | 
            -
                          attributes['style'] = 'quote'
         | 
| 760 | 
            -
                          attributes['attribution'] = attribution if attribution
         | 
| 761 | 
            -
                          attributes['citetitle'] = citetitle if citetitle
         | 
| 762 | 
            -
                          # NOTE will only detect headings that are floating titles (not section titles)
         | 
| 763 | 
            -
                          # TODO could assume a floating title when inside a block context
         | 
| 764 | 
            -
                          # FIXME Reader needs to be created w/ line info
         | 
| 765 | 
            -
                          block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
         | 
| 766 | 
            -
                        elsif !text_only && (blockquote? lines, first_line)
         | 
| 767 | 
            -
                          lines[0] = first_line[1..-1]
         | 
| 768 | 
            -
                          attribution, citetitle = lines.pop[3..-1].split(', ', 2)
         | 
| 769 | 
            -
                          lines.pop while lines[-1].empty?
         | 
| 770 | 
            -
                          # strip trailing quote
         | 
| 771 | 
            -
                          lines[-1] = lines[-1].chop
         | 
| 772 | 
            -
                          attributes['style'] = 'quote'
         | 
| 773 | 
            -
                          attributes['attribution'] = attribution if attribution
         | 
| 774 | 
            -
                          attributes['citetitle'] = citetitle if citetitle
         | 
| 775 | 
            -
                          block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes)
         | 
| 776 | 
            -
                        else
         | 
| 777 | 
            -
                          # if [normal] is used over an indented paragraph, shift content to left margin
         | 
| 778 | 
            -
                          if style == 'normal'
         | 
| 779 | 
            -
                            # QUESTION do we even need to shift since whitespace is normalized by XML in this case?
         | 
| 780 | 
            -
                            adjust_indentation! lines
         | 
| 781 | 
            -
                          end
         | 
| 651 | 
            +
                  elsif (match = DescriptionListRx.match(this_line))
         | 
| 652 | 
            +
                    reader.unshift_line this_line
         | 
| 653 | 
            +
                    block = next_description_list(reader, match, parent)
         | 
| 654 | 
            +
                    break
         | 
| 782 655 |  | 
| 783 | 
            -
             | 
| 784 | 
            -
             | 
| 785 | 
            -
             | 
| 656 | 
            +
                  elsif (style == 'float' || style == 'discrete') && (Compliance.underline_style_section_titles ?
         | 
| 657 | 
            +
                      (is_section_title? this_line, (reader.peek_line true)) : !indented && (is_section_title? this_line))
         | 
| 658 | 
            +
                    reader.unshift_line this_line
         | 
| 659 | 
            +
                    float_id, float_reftext, float_title, float_level, _ = parse_section_title(reader, document)
         | 
| 660 | 
            +
                    attributes['reftext'] = float_reftext if float_reftext
         | 
| 661 | 
            +
                    block = Block.new(parent, :floating_title, :content_model => :empty)
         | 
| 662 | 
            +
                    block.title = float_title
         | 
| 663 | 
            +
                    attributes.delete 'title'
         | 
| 664 | 
            +
                    block.id = float_id || attributes['id'] ||
         | 
| 665 | 
            +
                        ((document.attributes.key? 'sectids') ? (Section.generate_id block.title, document) : nil)
         | 
| 666 | 
            +
                    block.level = float_level
         | 
| 667 | 
            +
                    break
         | 
| 786 668 |  | 
| 787 | 
            -
             | 
| 669 | 
            +
                  # FIXME create another set for "passthrough" styles
         | 
| 670 | 
            +
                  # FIXME make this more DRY!
         | 
| 671 | 
            +
                  elsif style && style != 'normal'
         | 
| 672 | 
            +
                    if PARAGRAPH_STYLES.include?(style)
         | 
| 673 | 
            +
                      block_context = style.to_sym
         | 
| 674 | 
            +
                      cloaked_context = :paragraph
         | 
| 675 | 
            +
                      reader.unshift_line this_line
         | 
| 676 | 
            +
                      # advance to block parsing =>
         | 
| 677 | 
            +
                      break
         | 
| 678 | 
            +
                    elsif ADMONITION_STYLES.include?(style)
         | 
| 679 | 
            +
                      block_context = :admonition
         | 
| 680 | 
            +
                      cloaked_context = :paragraph
         | 
| 681 | 
            +
                      reader.unshift_line this_line
         | 
| 682 | 
            +
                      # advance to block parsing =>
         | 
| 683 | 
            +
                      break
         | 
| 684 | 
            +
                    elsif block_extensions && extensions.registered_for_block?(style, :paragraph)
         | 
| 685 | 
            +
                      block_context = style.to_sym
         | 
| 686 | 
            +
                      cloaked_context = :paragraph
         | 
| 687 | 
            +
                      reader.unshift_line this_line
         | 
| 688 | 
            +
                      # advance to block parsing =>
         | 
| 788 689 | 
             
                      break
         | 
| 690 | 
            +
                    else
         | 
| 691 | 
            +
                      warn %(asciidoctor: WARNING: #{this_path}: line #{this_lineno}: invalid style for paragraph: #{style})
         | 
| 692 | 
            +
                      style = nil
         | 
| 693 | 
            +
                      # continue to process paragraph
         | 
| 789 694 | 
             
                    end
         | 
| 790 695 | 
             
                  end
         | 
| 791 696 |  | 
| 792 | 
            -
                   | 
| 793 | 
            -
                   | 
| 794 | 
            -
             | 
| 795 | 
            -
             | 
| 796 | 
            -
             | 
| 697 | 
            +
                  break_at_list = (skipped == 0 && in_list)
         | 
| 698 | 
            +
                  reader.unshift_line this_line
         | 
| 699 | 
            +
             | 
| 700 | 
            +
                  # a literal paragraph: contiguous lines starting with at least one whitespace character
         | 
| 701 | 
            +
                  # NOTE style can only be nil or "normal" at this point
         | 
| 702 | 
            +
                  if indented && !style
         | 
| 703 | 
            +
                    lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => text_only
         | 
| 797 704 |  | 
| 798 | 
            -
                     | 
| 799 | 
            -
                    when :admonition
         | 
| 800 | 
            -
                      attributes['name'] = admonition_name = style.downcase
         | 
| 801 | 
            -
                      attributes['caption'] ||= document.attributes[%(#{admonition_name}-caption)]
         | 
| 802 | 
            -
                      block = build_block(block_context, :compound, terminator, parent, reader, attributes)
         | 
| 705 | 
            +
                    adjust_indentation! lines
         | 
| 803 706 |  | 
| 804 | 
            -
                     | 
| 805 | 
            -
             | 
| 707 | 
            +
                    block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes)
         | 
| 708 | 
            +
                    # a literal gets special meaning inside of a description list
         | 
| 709 | 
            +
                    # TODO this feels hacky, better way to distinguish from explicit literal block?
         | 
| 710 | 
            +
                    block.set_option('listparagraph') if in_list
         | 
| 711 | 
            +
             | 
| 712 | 
            +
                  # a normal paragraph: contiguous non-blank/non-continuation lines (left-indented or normal style)
         | 
| 713 | 
            +
                  else
         | 
| 714 | 
            +
                    lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => true
         | 
| 715 | 
            +
             | 
| 716 | 
            +
                    # NOTE we need this logic because we've asked the reader to skip
         | 
| 717 | 
            +
                    # line comments, which may leave us w/ an empty buffer if those
         | 
| 718 | 
            +
                    # were the only lines found
         | 
| 719 | 
            +
                    if in_list && lines.empty?
         | 
| 720 | 
            +
                      # call advance since the reader preserved the last line
         | 
| 721 | 
            +
                      reader.advance
         | 
| 806 722 | 
             
                      return
         | 
| 723 | 
            +
                    end
         | 
| 807 724 |  | 
| 808 | 
            -
                     | 
| 809 | 
            -
             | 
| 810 | 
            -
             | 
| 811 | 
            -
             | 
| 812 | 
            -
                       | 
| 813 | 
            -
             | 
| 814 | 
            -
             | 
| 815 | 
            -
             | 
| 816 | 
            -
             | 
| 817 | 
            -
             | 
| 818 | 
            -
             | 
| 819 | 
            -
             | 
| 820 | 
            -
             | 
| 821 | 
            -
             | 
| 822 | 
            -
             | 
| 823 | 
            -
                         | 
| 824 | 
            -
                         | 
| 825 | 
            -
             | 
| 826 | 
            -
                         | 
| 827 | 
            -
                        unless attributes.key? 'language'
         | 
| 828 | 
            -
                          if (default_language = document.attributes['source-language'])
         | 
| 829 | 
            -
                            attributes['language'] = default_language
         | 
| 830 | 
            -
                          end
         | 
| 831 | 
            -
                        end
         | 
| 832 | 
            -
                        if !attributes.key?('indent') && document.attributes.key?('source-indent')
         | 
| 833 | 
            -
                          attributes['indent'] = document.attributes['source-indent']
         | 
| 834 | 
            -
                        end
         | 
| 725 | 
            +
                    # NOTE don't check indented here since it's extremely rare
         | 
| 726 | 
            +
                    #if text_only || indented
         | 
| 727 | 
            +
                    if text_only
         | 
| 728 | 
            +
                      # if [normal] is used over an indented paragraph, shift content to left margin
         | 
| 729 | 
            +
                      # QUESTION do we even need to shift since whitespace is normalized by XML in this case?
         | 
| 730 | 
            +
                      adjust_indentation! lines if indented && style == 'normal'
         | 
| 731 | 
            +
                      block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes)
         | 
| 732 | 
            +
                    elsif (ADMONITION_STYLE_LEADERS.include? ch0) && (this_line.include? ':') && (AdmonitionParagraphRx =~ this_line)
         | 
| 733 | 
            +
                      lines[0] = $' # string after match
         | 
| 734 | 
            +
                      attributes['name'] = admonition_name = (attributes['style'] = $1).downcase
         | 
| 735 | 
            +
                      attributes['textlabel'] = (attributes.delete 'caption') || document.attributes[%(#{admonition_name}-caption)]
         | 
| 736 | 
            +
                      block = Block.new(parent, :admonition, :content_model => :simple, :source => lines, :attributes => attributes)
         | 
| 737 | 
            +
                    elsif md_syntax && ch0 == '>' && this_line.start_with?('> ')
         | 
| 738 | 
            +
                      lines.map! {|line| line == '>' ? line[1..-1] : ((line.start_with? '> ') ? line[2..-1] : line) }
         | 
| 739 | 
            +
                      if lines[-1].start_with? '-- '
         | 
| 740 | 
            +
                        attribution, citetitle = lines.pop[3..-1].split(', ', 2)
         | 
| 741 | 
            +
                        attributes['attribution'] = attribution if attribution
         | 
| 742 | 
            +
                        attributes['citetitle'] = citetitle if citetitle
         | 
| 743 | 
            +
                        lines.pop while lines[-1].empty?
         | 
| 835 744 | 
             
                      end
         | 
| 836 | 
            -
                       | 
| 745 | 
            +
                      attributes['style'] = 'quote'
         | 
| 746 | 
            +
                      # NOTE will only detect headings that are floating titles (not section titles)
         | 
| 747 | 
            +
                      # TODO could assume a floating title when inside a block context
         | 
| 748 | 
            +
                      # FIXME Reader needs to be created w/ line info
         | 
| 749 | 
            +
                      block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
         | 
| 750 | 
            +
                    elsif ch0 == '"' && lines.size > 1 && (lines[-1].start_with? '-- ') && (lines[-2].end_with? '"')
         | 
| 751 | 
            +
                      lines[0] = this_line[1..-1] # strip leading quote
         | 
| 752 | 
            +
                      attribution, citetitle = lines.pop[3..-1].split(', ', 2)
         | 
| 753 | 
            +
                      attributes['attribution'] = attribution if attribution
         | 
| 754 | 
            +
                      attributes['citetitle'] = citetitle if citetitle
         | 
| 755 | 
            +
                      lines.pop while lines[-1].empty?
         | 
| 756 | 
            +
                      lines[-1] = lines[-1].chop # strip trailing quote
         | 
| 757 | 
            +
                      attributes['style'] = 'quote'
         | 
| 758 | 
            +
                      block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes)
         | 
| 759 | 
            +
                    else
         | 
| 760 | 
            +
                      # if [normal] is used over an indented paragraph, shift content to left margin
         | 
| 761 | 
            +
                      # QUESTION do we even need to shift since whitespace is normalized by XML in this case?
         | 
| 762 | 
            +
                      adjust_indentation! lines if indented && style == 'normal'
         | 
| 763 | 
            +
                      block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes)
         | 
| 764 | 
            +
                    end
         | 
| 837 765 |  | 
| 838 | 
            -
                     | 
| 839 | 
            -
             | 
| 766 | 
            +
                    catalog_inline_anchors lines * LF, block, document
         | 
| 767 | 
            +
                  end
         | 
| 840 768 |  | 
| 841 | 
            -
             | 
| 842 | 
            -
             | 
| 769 | 
            +
                  break # forbid loop from executing more than once
         | 
| 770 | 
            +
                end unless delimited_block
         | 
| 843 771 |  | 
| 844 | 
            -
             | 
| 845 | 
            -
             | 
| 846 | 
            -
             | 
| 847 | 
            -
             | 
| 848 | 
            -
             | 
| 849 | 
            -
             | 
| 850 | 
            -
             | 
| 851 | 
            -
             | 
| 852 | 
            -
             | 
| 772 | 
            +
                # either delimited block or styled paragraph
         | 
| 773 | 
            +
                unless block
         | 
| 774 | 
            +
                  # abstract and partintro should be handled by open block
         | 
| 775 | 
            +
                  # FIXME kind of hackish...need to sort out how to generalize this
         | 
| 776 | 
            +
                  block_context = :open if block_context == :abstract || block_context == :partintro
         | 
| 777 | 
            +
             | 
| 778 | 
            +
                  case block_context
         | 
| 779 | 
            +
                  when :admonition
         | 
| 780 | 
            +
                    attributes['name'] = admonition_name = style.downcase
         | 
| 781 | 
            +
                    attributes['textlabel'] = (attributes.delete 'caption') || document.attributes[%(#{admonition_name}-caption)]
         | 
| 782 | 
            +
                    block = build_block(block_context, :compound, terminator, parent, reader, attributes)
         | 
| 783 | 
            +
             | 
| 784 | 
            +
                  when :comment
         | 
| 785 | 
            +
                    build_block(block_context, :skip, terminator, parent, reader, attributes)
         | 
| 786 | 
            +
                    return
         | 
| 787 | 
            +
             | 
| 788 | 
            +
                  when :example
         | 
| 789 | 
            +
                    block = build_block(block_context, :compound, terminator, parent, reader, attributes)
         | 
| 790 | 
            +
             | 
| 791 | 
            +
                  when :listing, :literal
         | 
| 792 | 
            +
                    block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
         | 
| 793 | 
            +
             | 
| 794 | 
            +
                  when :source
         | 
| 795 | 
            +
                    AttributeList.rekey attributes, [nil, 'language', 'linenums']
         | 
| 796 | 
            +
                    if document.attributes.key? 'source-language'
         | 
| 797 | 
            +
                      attributes['language'] = document.attributes['source-language'] || 'text'
         | 
| 798 | 
            +
                    end unless attributes.key? 'language'
         | 
| 799 | 
            +
                    if (attributes.key? 'linenums-option') || (document.attributes.key? 'source-linenums-option')
         | 
| 800 | 
            +
                      attributes['linenums'] = ''
         | 
| 801 | 
            +
                    end unless attributes.key? 'linenums'
         | 
| 802 | 
            +
                    if document.attributes.key? 'source-indent'
         | 
| 803 | 
            +
                      attributes['indent'] = document.attributes['source-indent']
         | 
| 804 | 
            +
                    end unless attributes.key? 'indent'
         | 
| 805 | 
            +
                    block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
         | 
| 806 | 
            +
             | 
| 807 | 
            +
                  when :fenced_code
         | 
| 808 | 
            +
                    attributes['style'] = 'source'
         | 
| 809 | 
            +
                    if (ll = this_line.length) == 3
         | 
| 810 | 
            +
                      language = nil
         | 
| 811 | 
            +
                    elsif (comma_idx = (language = this_line.slice 3, ll).index ',')
         | 
| 812 | 
            +
                      if comma_idx > 0
         | 
| 813 | 
            +
                        language = (language.slice 0, comma_idx).strip
         | 
| 814 | 
            +
                        attributes['linenums'] = '' if comma_idx < ll - 4
         | 
| 815 | 
            +
                      else
         | 
| 816 | 
            +
                        language = nil
         | 
| 817 | 
            +
                        attributes['linenums'] = '' if ll > 4
         | 
| 853 818 | 
             
                      end
         | 
| 854 | 
            -
             | 
| 855 | 
            -
             | 
| 856 | 
            -
                     | 
| 857 | 
            -
             | 
| 858 | 
            -
             | 
| 859 | 
            -
             | 
| 860 | 
            -
                      cursor = reader.cursor
         | 
| 861 | 
            -
                      block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true), cursor
         | 
| 862 | 
            -
                      case terminator.chr
         | 
| 863 | 
            -
                        when ','
         | 
| 864 | 
            -
                          attributes['format'] = 'csv'
         | 
| 865 | 
            -
                        when ':'
         | 
| 866 | 
            -
                          attributes['format'] = 'dsv'
         | 
| 819 | 
            +
                    else
         | 
| 820 | 
            +
                      language = language.lstrip
         | 
| 821 | 
            +
                    end
         | 
| 822 | 
            +
                    if language.nil_or_empty?
         | 
| 823 | 
            +
                      if document.attributes.key? 'source-language'
         | 
| 824 | 
            +
                        attributes['language'] = document.attributes['source-language'] || 'text'
         | 
| 867 825 | 
             
                      end
         | 
| 868 | 
            -
             | 
| 826 | 
            +
                    else
         | 
| 827 | 
            +
                      attributes['language'] = language
         | 
| 828 | 
            +
                    end
         | 
| 829 | 
            +
                    if (attributes.key? 'linenums-option') || (document.attributes.key? 'source-linenums-option')
         | 
| 830 | 
            +
                      attributes['linenums'] = ''
         | 
| 831 | 
            +
                    end unless attributes.key? 'linenums'
         | 
| 832 | 
            +
                    if document.attributes.key? 'source-indent'
         | 
| 833 | 
            +
                      attributes['indent'] = document.attributes['source-indent']
         | 
| 834 | 
            +
                    end unless attributes.key? 'indent'
         | 
| 835 | 
            +
                    terminator = terminator.slice 0, 3
         | 
| 836 | 
            +
                    block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
         | 
| 837 | 
            +
             | 
| 838 | 
            +
                  when :pass
         | 
| 839 | 
            +
                    block = build_block(block_context, :raw, terminator, parent, reader, attributes)
         | 
| 840 | 
            +
             | 
| 841 | 
            +
                  when :stem, :latexmath, :asciimath
         | 
| 842 | 
            +
                    if block_context == :stem
         | 
| 843 | 
            +
                      attributes['style'] = if (explicit_stem_syntax = attributes[2])
         | 
| 844 | 
            +
                        explicit_stem_syntax.include?('tex') ? 'latexmath' : 'asciimath'
         | 
| 845 | 
            +
                      elsif (default_stem_syntax = document.attributes['stem']).nil_or_empty?
         | 
| 846 | 
            +
                        'asciimath'
         | 
| 847 | 
            +
                      else
         | 
| 848 | 
            +
                        default_stem_syntax
         | 
| 849 | 
            +
                      end
         | 
| 850 | 
            +
                    end
         | 
| 851 | 
            +
                    block = build_block(:stem, :raw, terminator, parent, reader, attributes)
         | 
| 869 852 |  | 
| 870 | 
            -
             | 
| 871 | 
            -
             | 
| 872 | 
            -
                      block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes)
         | 
| 853 | 
            +
                  when :open, :sidebar
         | 
| 854 | 
            +
                    block = build_block(block_context, :compound, terminator, parent, reader, attributes)
         | 
| 873 855 |  | 
| 874 | 
            -
             | 
| 875 | 
            -
             | 
| 876 | 
            -
             | 
| 877 | 
            -
             | 
| 878 | 
            -
             | 
| 879 | 
            -
             | 
| 880 | 
            -
             | 
| 881 | 
            -
             | 
| 882 | 
            -
             | 
| 883 | 
            -
             | 
| 856 | 
            +
                  when :table
         | 
| 857 | 
            +
                    block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true), reader.cursor
         | 
| 858 | 
            +
                    # NOTE it's very rare that format is set when using a format hint char, so short-circuit
         | 
| 859 | 
            +
                    unless terminator.start_with? '|', '!'
         | 
| 860 | 
            +
                      # NOTE infer dsv once all other format hint chars are ruled out
         | 
| 861 | 
            +
                      attributes['format'] ||= (terminator.start_with? ',') ? 'csv' : 'dsv'
         | 
| 862 | 
            +
                    end
         | 
| 863 | 
            +
                    block = next_table(block_reader, parent, attributes)
         | 
| 864 | 
            +
             | 
| 865 | 
            +
                  when :quote, :verse
         | 
| 866 | 
            +
                    AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
         | 
| 867 | 
            +
                    block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes)
         | 
| 868 | 
            +
             | 
| 869 | 
            +
                  else
         | 
| 870 | 
            +
                    if block_extensions && (extension = extensions.registered_for_block?(block_context, cloaked_context))
         | 
| 871 | 
            +
                      if (content_model = extension.config[:content_model]) != :skip
         | 
| 872 | 
            +
                        if !(pos_attrs = extension.config[:pos_attrs] || []).empty?
         | 
| 873 | 
            +
                          AttributeList.rekey(attributes, [nil].concat(pos_attrs))
         | 
| 884 874 | 
             
                        end
         | 
| 885 | 
            -
                         | 
| 886 | 
            -
             | 
| 887 | 
            -
                          attributes.clear
         | 
| 888 | 
            -
                          return
         | 
| 875 | 
            +
                        if (default_attrs = extension.config[:default_attrs])
         | 
| 876 | 
            +
                          default_attrs.each {|k, v| attributes[k] ||= v }
         | 
| 889 877 | 
             
                        end
         | 
| 890 | 
            -
             | 
| 891 | 
            -
                         | 
| 892 | 
            -
                        raise %(Unsupported block type #{block_context} at #{reader.line_info})
         | 
| 878 | 
            +
                        # QUESTION should we clone the extension for each cloaked context and set in config?
         | 
| 879 | 
            +
                        attributes['cloaked-context'] = cloaked_context
         | 
| 893 880 | 
             
                      end
         | 
| 881 | 
            +
                      block = build_block block_context, content_model, terminator, parent, reader, attributes, :extension => extension
         | 
| 882 | 
            +
                      unless block && content_model != :skip
         | 
| 883 | 
            +
                        attributes.clear
         | 
| 884 | 
            +
                        return
         | 
| 885 | 
            +
                      end
         | 
| 886 | 
            +
                    else
         | 
| 887 | 
            +
                      # this should only happen if there's a misconfiguration
         | 
| 888 | 
            +
                      raise %(Unsupported block type #{block_context} at #{reader.line_info})
         | 
| 894 889 | 
             
                    end
         | 
| 895 890 | 
             
                  end
         | 
| 896 891 | 
             
                end
         | 
| 897 892 |  | 
| 898 | 
            -
                # when looking for nested content, one or more line comments, comment
         | 
| 899 | 
            -
                # blocks or trailing attribute lists could leave us without a block,
         | 
| 900 | 
            -
                # so handle accordingly
         | 
| 901 | 
            -
                # REVIEW we may no longer need this nil check
         | 
| 902 893 | 
             
                # FIXME we've got to clean this up, it's horrible!
         | 
| 903 | 
            -
                if  | 
| 904 | 
            -
             | 
| 905 | 
            -
             | 
| 906 | 
            -
             | 
| 907 | 
            -
             | 
| 908 | 
            -
             | 
| 909 | 
            -
             | 
| 910 | 
            -
             | 
| 911 | 
            -
             | 
| 912 | 
            -
             | 
| 913 | 
            -
             | 
| 914 | 
            -
                     | 
| 915 | 
            -
             | 
| 916 | 
            -
             | 
| 917 | 
            -
             | 
| 918 | 
            -
             | 
| 919 | 
            -
             | 
| 920 | 
            -
             | 
| 921 | 
            -
             | 
| 922 | 
            -
             | 
| 923 | 
            -
             | 
| 924 | 
            -
             | 
| 925 | 
            -
             | 
| 926 | 
            -
             | 
| 927 | 
            -
             | 
| 928 | 
            -
                  if  | 
| 929 | 
            -
             | 
| 930 | 
            -
                    document.register(:ids, [block_id, (attributes['reftext'] || (block.title? ? block.title : nil))])
         | 
| 931 | 
            -
                  end
         | 
| 932 | 
            -
                  # FIXME remove the need for this update!
         | 
| 933 | 
            -
                  block.attributes.update(attributes) unless attributes.empty?
         | 
| 934 | 
            -
                  block.lock_in_subs
         | 
| 935 | 
            -
             | 
| 936 | 
            -
                  #if document.attributes.has_key? :pending_attribute_entries
         | 
| 937 | 
            -
                  #  document.attributes.delete(:pending_attribute_entries).each do |entry|
         | 
| 938 | 
            -
                  #    entry.save_to block.attributes
         | 
| 939 | 
            -
                  #  end
         | 
| 940 | 
            -
                  #end
         | 
| 941 | 
            -
             | 
| 942 | 
            -
                  if block.sub? :callouts
         | 
| 943 | 
            -
                    unless (catalog_callouts block.source, document)
         | 
| 944 | 
            -
                      # No need to sub callouts if they aren't there
         | 
| 945 | 
            -
                      block.remove_sub :callouts
         | 
| 946 | 
            -
                    end
         | 
| 947 | 
            -
                  end
         | 
| 894 | 
            +
                block.source_location = source_location if source_location
         | 
| 895 | 
            +
                # FIXME title should be assigned when block is constructed
         | 
| 896 | 
            +
                block.title = attributes.delete 'title' if attributes.key? 'title'
         | 
| 897 | 
            +
                #unless attributes.key? 'reftext'
         | 
| 898 | 
            +
                #  attributes['reftext'] = document.attributes['reftext'] if document.attributes.key? 'reftext'
         | 
| 899 | 
            +
                #end
         | 
| 900 | 
            +
                # TODO eventually remove the style attribute from the attributes hash
         | 
| 901 | 
            +
                #block.style = attributes.delete 'style'
         | 
| 902 | 
            +
                block.style = attributes['style']
         | 
| 903 | 
            +
                if (block_id = (block.id ||= attributes['id']))
         | 
| 904 | 
            +
                  unless document.register :refs, [block_id, block, attributes['reftext'] || (block.title? ? block.title : nil)]
         | 
| 905 | 
            +
                    warn %(asciidoctor: WARNING: #{this_path}: line #{this_lineno}: id assigned to block already in use: #{block_id})
         | 
| 906 | 
            +
                  end
         | 
| 907 | 
            +
                end
         | 
| 908 | 
            +
                # FIXME remove the need for this update!
         | 
| 909 | 
            +
                block.attributes.update(attributes) unless attributes.empty?
         | 
| 910 | 
            +
                block.lock_in_subs
         | 
| 911 | 
            +
             | 
| 912 | 
            +
                #if document.attributes.key? :pending_attribute_entries
         | 
| 913 | 
            +
                #  document.attributes.delete(:pending_attribute_entries).each do |entry|
         | 
| 914 | 
            +
                #    entry.save_to block.attributes
         | 
| 915 | 
            +
                #  end
         | 
| 916 | 
            +
                #end
         | 
| 917 | 
            +
             | 
| 918 | 
            +
                if block.sub? :callouts
         | 
| 919 | 
            +
                  # No need to sub callouts if none are found when cataloging
         | 
| 920 | 
            +
                  block.remove_sub :callouts unless catalog_callouts block.source, document
         | 
| 948 921 | 
             
                end
         | 
| 949 922 |  | 
| 950 923 | 
             
                block
         | 
| 951 924 | 
             
              end
         | 
| 952 925 |  | 
| 953 | 
            -
              def self.blockquote? lines, first_line = nil
         | 
| 954 | 
            -
                lines.size > 1 && ((first_line || lines[0]).start_with? '"') &&
         | 
| 955 | 
            -
                    (lines[-1].start_with? '-- ') && (lines[-2].end_with? '"')
         | 
| 956 | 
            -
              end
         | 
| 957 | 
            -
             | 
| 958 926 | 
             
              def self.read_paragraph_lines reader, break_at_list, opts = {}
         | 
| 959 927 | 
             
                opts[:break_on_blank_lines] = true
         | 
| 960 928 | 
             
                opts[:break_on_list_continuation] = true
         | 
| @@ -970,7 +938,7 @@ class Parser | |
| 970 938 | 
             
              # returns the match data if this line is the first line of a delimited block or nil if not
         | 
| 971 939 | 
             
              def self.is_delimited_block? line, return_match_data = false
         | 
| 972 940 | 
             
                # highly optimized for best performance
         | 
| 973 | 
            -
                return unless (line_len = line.length) > 1 &&  | 
| 941 | 
            +
                return unless (line_len = line.length) > 1 && DELIMITED_BLOCK_LEADERS.include?(line.slice 0, 2)
         | 
| 974 942 | 
             
                # catches open block
         | 
| 975 943 | 
             
                if line_len == 2
         | 
| 976 944 | 
             
                  tip = line
         | 
| @@ -981,7 +949,7 @@ class Parser | |
| 981 949 | 
             
                    tip = line
         | 
| 982 950 | 
             
                    tl = line_len
         | 
| 983 951 | 
             
                  else
         | 
| 984 | 
            -
                    tip = line | 
| 952 | 
            +
                    tip = line.slice 0, 4
         | 
| 985 953 | 
             
                    tl = 4
         | 
| 986 954 | 
             
                  end
         | 
| 987 955 |  | 
| @@ -1004,18 +972,18 @@ class Parser | |
| 1004 972 | 
             
                  return if tl == 3 && !fenced_code
         | 
| 1005 973 | 
             
                end
         | 
| 1006 974 |  | 
| 1007 | 
            -
                if DELIMITED_BLOCKS. | 
| 975 | 
            +
                if DELIMITED_BLOCKS.key? tip
         | 
| 1008 976 | 
             
                  # tip is the full line when delimiter is minimum length
         | 
| 1009 977 | 
             
                  if tl < 4 || tl == line_len
         | 
| 1010 978 | 
             
                    if return_match_data
         | 
| 1011 | 
            -
                      context, masq =  | 
| 979 | 
            +
                      context, masq = DELIMITED_BLOCKS[tip]
         | 
| 1012 980 | 
             
                      BlockMatchData.new(context, masq, tip, tip)
         | 
| 1013 981 | 
             
                    else
         | 
| 1014 982 | 
             
                      true
         | 
| 1015 983 | 
             
                    end
         | 
| 1016 984 | 
             
                  elsif %(#{tip}#{tip[-1..-1] * (line_len - tl)}) == line
         | 
| 1017 985 | 
             
                    if return_match_data
         | 
| 1018 | 
            -
                      context, masq =  | 
| 986 | 
            +
                      context, masq = DELIMITED_BLOCKS[tip]
         | 
| 1019 987 | 
             
                      BlockMatchData.new(context, masq, tip, line)
         | 
| 1020 988 | 
             
                    else
         | 
| 1021 989 | 
             
                      true
         | 
| @@ -1023,7 +991,7 @@ class Parser | |
| 1023 991 | 
             
                  # only enable if/when we decide to support non-congruent block delimiters
         | 
| 1024 992 | 
             
                  #elsif (match = BlockDelimiterRx.match(line))
         | 
| 1025 993 | 
             
                  #  if return_match_data
         | 
| 1026 | 
            -
                  #    context, masq =  | 
| 994 | 
            +
                  #    context, masq = DELIMITED_BLOCKS[tip]
         | 
| 1027 995 | 
             
                  #    BlockMatchData.new(context, masq, tip, match[0])
         | 
| 1028 996 | 
             
                  #  else
         | 
| 1029 997 | 
             
                  #    true
         | 
| @@ -1040,8 +1008,11 @@ class Parser | |
| 1040 1008 | 
             
              # if terminator is false, that means the all the lines in the reader should be parsed
         | 
| 1041 1009 | 
             
              # NOTE could invoke filter in here, before and after parsing
         | 
| 1042 1010 | 
             
              def self.build_block(block_context, content_model, terminator, parent, reader, attributes, options = {})
         | 
| 1043 | 
            -
                if content_model == :skip | 
| 1044 | 
            -
                  skip_processing =  | 
| 1011 | 
            +
                if content_model == :skip
         | 
| 1012 | 
            +
                  skip_processing = true
         | 
| 1013 | 
            +
                  parse_as_content_model = :simple
         | 
| 1014 | 
            +
                elsif content_model == :raw
         | 
| 1015 | 
            +
                  skip_processing = false
         | 
| 1045 1016 | 
             
                  parse_as_content_model = :simple
         | 
| 1046 1017 | 
             
                else
         | 
| 1047 1018 | 
             
                  skip_processing = false
         | 
| @@ -1050,15 +1021,16 @@ class Parser | |
| 1050 1021 |  | 
| 1051 1022 | 
             
                if terminator.nil?
         | 
| 1052 1023 | 
             
                  if parse_as_content_model == :verbatim
         | 
| 1053 | 
            -
                    lines = reader.read_lines_until | 
| 1024 | 
            +
                    lines = reader.read_lines_until :break_on_blank_lines => true, :break_on_list_continuation => true
         | 
| 1054 1025 | 
             
                  else
         | 
| 1055 1026 | 
             
                    content_model = :simple if content_model == :compound
         | 
| 1056 | 
            -
                     | 
| 1027 | 
            +
                    # TODO we could also skip processing if we're able to detect reader is a BlockReader
         | 
| 1028 | 
            +
                    lines = read_paragraph_lines reader, false, :skip_line_comments => true, :skip_processing => skip_processing
         | 
| 1057 1029 | 
             
                    # QUESTION check for empty lines after grabbing lines for simple content model?
         | 
| 1058 1030 | 
             
                  end
         | 
| 1059 1031 | 
             
                  block_reader = nil
         | 
| 1060 1032 | 
             
                elsif parse_as_content_model != :compound
         | 
| 1061 | 
            -
                  lines = reader.read_lines_until | 
| 1033 | 
            +
                  lines = reader.read_lines_until :terminator => terminator, :skip_processing => skip_processing
         | 
| 1062 1034 | 
             
                  block_reader = nil
         | 
| 1063 1035 | 
             
                # terminator is false when reader has already been prepared
         | 
| 1064 1036 | 
             
                elsif terminator == false
         | 
| @@ -1066,8 +1038,7 @@ class Parser | |
| 1066 1038 | 
             
                  block_reader = reader
         | 
| 1067 1039 | 
             
                else
         | 
| 1068 1040 | 
             
                  lines = nil
         | 
| 1069 | 
            -
                   | 
| 1070 | 
            -
                  block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing), cursor
         | 
| 1041 | 
            +
                  block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing), reader.cursor
         | 
| 1071 1042 | 
             
                end
         | 
| 1072 1043 |  | 
| 1073 1044 | 
             
                if content_model == :skip
         | 
| @@ -1105,17 +1076,16 @@ class Parser | |
| 1105 1076 | 
             
                end
         | 
| 1106 1077 |  | 
| 1107 1078 | 
             
                # QUESTION should we have an explicit map or can we rely on check for *-caption attribute?
         | 
| 1108 | 
            -
                if (attributes. | 
| 1079 | 
            +
                if (attributes.key? 'title') && block.context != :admonition &&
         | 
| 1080 | 
            +
                    (parent.document.attributes.key? %(#{block.context}-caption))
         | 
| 1109 1081 | 
             
                  block.title = attributes.delete 'title'
         | 
| 1110 | 
            -
                  block.assign_caption | 
| 1082 | 
            +
                  block.assign_caption(attributes.delete 'caption')
         | 
| 1111 1083 | 
             
                end
         | 
| 1112 1084 |  | 
| 1113 | 
            -
                 | 
| 1114 | 
            -
             | 
| 1115 | 
            -
             | 
| 1116 | 
            -
             | 
| 1117 | 
            -
                  parse_blocks block_reader, block
         | 
| 1118 | 
            -
                end
         | 
| 1085 | 
            +
                # reader is confined within boundaries of a delimited block, so look for
         | 
| 1086 | 
            +
                # blocks until there are no more lines
         | 
| 1087 | 
            +
                parse_blocks block_reader, block if content_model == :compound
         | 
| 1088 | 
            +
             | 
| 1119 1089 | 
             
                block
         | 
| 1120 1090 | 
             
              end
         | 
| 1121 1091 |  | 
| @@ -1130,20 +1100,19 @@ class Parser | |
| 1130 1100 | 
             
              #
         | 
| 1131 1101 | 
             
              # Returns nothing.
         | 
| 1132 1102 | 
             
              def self.parse_blocks(reader, parent)
         | 
| 1133 | 
            -
                while reader | 
| 1134 | 
            -
                   | 
| 1135 | 
            -
                  parent << block if block
         | 
| 1103 | 
            +
                while (block = next_block reader, parent)
         | 
| 1104 | 
            +
                  parent << block
         | 
| 1136 1105 | 
             
                end
         | 
| 1137 1106 | 
             
              end
         | 
| 1138 1107 |  | 
| 1139 | 
            -
              # Internal: Parse and construct an  | 
| 1108 | 
            +
              # Internal: Parse and construct an item list (ordered or unordered) from the current position of the Reader
         | 
| 1140 1109 | 
             
              #
         | 
| 1141 1110 | 
             
              # reader    - The Reader from which to retrieve the outline list
         | 
| 1142 1111 | 
             
              # list_type - A Symbol representing the list type (:olist for ordered, :ulist for unordered)
         | 
| 1143 1112 | 
             
              # parent    - The parent Block to which this outline list belongs
         | 
| 1144 1113 | 
             
              #
         | 
| 1145 1114 | 
             
              # Returns the Block encapsulating the parsed outline (unordered or ordered) list
         | 
| 1146 | 
            -
              def self. | 
| 1115 | 
            +
              def self.next_item_list(reader, list_type, parent)
         | 
| 1147 1116 | 
             
                list_block = List.new(parent, list_type)
         | 
| 1148 1117 | 
             
                if parent.context == list_type
         | 
| 1149 1118 | 
             
                  list_block.level = parent.level + 1
         | 
| @@ -1195,61 +1164,76 @@ class Parser | |
| 1195 1164 | 
             
              # Internal: Catalog any callouts found in the text, but don't process them
         | 
| 1196 1165 | 
             
              #
         | 
| 1197 1166 | 
             
              # text     - The String of text in which to look for callouts
         | 
| 1198 | 
            -
              # document - The current document  | 
| 1167 | 
            +
              # document - The current document in which the callouts are stored
         | 
| 1199 1168 | 
             
              #
         | 
| 1200 1169 | 
             
              # Returns A Boolean indicating whether callouts were found
         | 
| 1201 1170 | 
             
              def self.catalog_callouts(text, document)
         | 
| 1202 1171 | 
             
                found = false
         | 
| 1203 | 
            -
                 | 
| 1204 | 
            -
                   | 
| 1205 | 
            -
             | 
| 1206 | 
            -
             | 
| 1207 | 
            -
             | 
| 1208 | 
            -
             | 
| 1209 | 
            -
             | 
| 1210 | 
            -
                    # we have to mark as found even if it's escaped so it can be unescaped
         | 
| 1211 | 
            -
                    found = true
         | 
| 1212 | 
            -
                  }
         | 
| 1213 | 
            -
                end
         | 
| 1172 | 
            +
                text.scan(CalloutScanRx) {
         | 
| 1173 | 
            +
                  # lead with assignments for Ruby 1.8.7 compat
         | 
| 1174 | 
            +
                  captured, num = $&, $2
         | 
| 1175 | 
            +
                  document.callouts.register num unless captured.start_with? '\\'
         | 
| 1176 | 
            +
                  # we have to mark as found even if it's escaped so it can be unescaped
         | 
| 1177 | 
            +
                  found = true
         | 
| 1178 | 
            +
                } if text.include? '<'
         | 
| 1214 1179 | 
             
                found
         | 
| 1215 1180 | 
             
              end
         | 
| 1216 1181 |  | 
| 1217 | 
            -
              # Internal: Catalog any inline anchors found in the text | 
| 1182 | 
            +
              # Internal: Catalog any inline anchors found in the text (but don't convert)
         | 
| 1218 1183 | 
             
              #
         | 
| 1219 1184 | 
             
              # text     - The String text in which to look for inline anchors
         | 
| 1220 | 
            -
              #  | 
| 1185 | 
            +
              # block    - The block in which the references should be searched
         | 
| 1186 | 
            +
              # document - The current Document on which the references are stored
         | 
| 1221 1187 | 
             
              #
         | 
| 1222 1188 | 
             
              # Returns nothing
         | 
| 1223 | 
            -
              def self.catalog_inline_anchors | 
| 1224 | 
            -
                 | 
| 1225 | 
            -
                   | 
| 1226 | 
            -
                     | 
| 1227 | 
            -
             | 
| 1228 | 
            -
                     | 
| 1229 | 
            -
             | 
| 1230 | 
            -
                     | 
| 1231 | 
            -
                     | 
| 1232 | 
            -
             | 
| 1233 | 
            -
             | 
| 1234 | 
            -
                     | 
| 1235 | 
            -
             | 
| 1236 | 
            -
             | 
| 1237 | 
            -
             | 
| 1189 | 
            +
              def self.catalog_inline_anchors text, block, document
         | 
| 1190 | 
            +
                text.scan(InlineAnchorScanRx) do
         | 
| 1191 | 
            +
                  if (id = $1)
         | 
| 1192 | 
            +
                    if (reftext = $2)
         | 
| 1193 | 
            +
                      next if (reftext.include? '{') && (reftext = document.sub_attributes reftext).empty?
         | 
| 1194 | 
            +
                    end
         | 
| 1195 | 
            +
                  else
         | 
| 1196 | 
            +
                    id = $3
         | 
| 1197 | 
            +
                    if (reftext = $4)
         | 
| 1198 | 
            +
                      reftext = reftext.gsub '\]', ']' if reftext.include? ']'
         | 
| 1199 | 
            +
                      next if (reftext.include? '{') && (reftext = document.sub_attributes reftext).empty?
         | 
| 1200 | 
            +
                    end
         | 
| 1201 | 
            +
                  end
         | 
| 1202 | 
            +
                  unless document.register :refs, [id, (Inline.new block, :anchor, reftext, :type => :ref, :id => id), reftext]
         | 
| 1203 | 
            +
                    warn %(asciidoctor: WARNING: #{document.reader.path}: id assigned to anchor already in use: #{id})
         | 
| 1204 | 
            +
                  end
         | 
| 1205 | 
            +
                end if (text.include? '[[') || (text.include? 'or:')
         | 
| 1206 | 
            +
                nil
         | 
| 1207 | 
            +
              end
         | 
| 1208 | 
            +
             | 
| 1209 | 
            +
              # Internal: Catalog the bibliography inline anchor found in the start of the list item (but don't convert)
         | 
| 1210 | 
            +
              #
         | 
| 1211 | 
            +
              # text     - The String text in which to look for an inline bibliography anchor
         | 
| 1212 | 
            +
              # block    - The ListItem block in which the reference should be searched
         | 
| 1213 | 
            +
              # document - The current document in which the reference is stored
         | 
| 1214 | 
            +
              #
         | 
| 1215 | 
            +
              # Returns nothing
         | 
| 1216 | 
            +
              def self.catalog_inline_biblio_anchor text, block, document
         | 
| 1217 | 
            +
                if InlineBiblioAnchorRx =~ text
         | 
| 1218 | 
            +
                  # QUESTION should we sub attributes in reftext (like with regular anchors)?
         | 
| 1219 | 
            +
                  unless document.register :refs, [(id = $1), (Inline.new block, :anchor, (reftext = %([#{$2 || id}])), :type => :bibref, :id => id), reftext]
         | 
| 1220 | 
            +
                    warn %(asciidoctor: WARNING: #{document.reader.path}: id assigned to bibliography anchor already in use: #{id})
         | 
| 1221 | 
            +
                  end
         | 
| 1238 1222 | 
             
                end
         | 
| 1239 1223 | 
             
                nil
         | 
| 1240 1224 | 
             
              end
         | 
| 1241 1225 |  | 
| 1242 1226 | 
             
              # Internal: Parse and construct a description list Block from the current position of the Reader
         | 
| 1243 1227 | 
             
              #
         | 
| 1244 | 
            -
              # reader    - The Reader from which to retrieve the  | 
| 1228 | 
            +
              # reader    - The Reader from which to retrieve the description list
         | 
| 1245 1229 | 
             
              # match     - The Regexp match for the head of the list
         | 
| 1246 | 
            -
              # parent    - The parent Block to which this  | 
| 1230 | 
            +
              # parent    - The parent Block to which this description list belongs
         | 
| 1247 1231 | 
             
              #
         | 
| 1248 | 
            -
              # Returns the Block encapsulating the parsed  | 
| 1249 | 
            -
              def self. | 
| 1232 | 
            +
              # Returns the Block encapsulating the parsed description list
         | 
| 1233 | 
            +
              def self.next_description_list(reader, match, parent)
         | 
| 1250 1234 | 
             
                list_block = List.new(parent, :dlist)
         | 
| 1251 1235 | 
             
                previous_pair = nil
         | 
| 1252 | 
            -
                # allows us to capture until we find a  | 
| 1236 | 
            +
                # allows us to capture until we find a description item
         | 
| 1253 1237 | 
             
                # that uses the same delimiter (::, :::, :::: or ;;)
         | 
| 1254 1238 | 
             
                sibling_pattern = DescriptionListSiblingRx[match[2]]
         | 
| 1255 1239 |  | 
| @@ -1272,12 +1256,12 @@ class Parser | |
| 1272 1256 |  | 
| 1273 1257 | 
             
              # Internal: Parse and construct the next ListItem for the current bulleted
         | 
| 1274 1258 | 
             
              # (unordered or ordered) list Block, callout lists included, or the next
         | 
| 1275 | 
            -
              # term ListItem and description ListItem pair for the  | 
| 1259 | 
            +
              # term ListItem and description ListItem pair for the description list Block.
         | 
| 1276 1260 | 
             
              #
         | 
| 1277 1261 | 
             
              # First collect and process all the lines that constitute the next list
         | 
| 1278 1262 | 
             
              # item for the parent list (according to its type). Next, parse those lines
         | 
| 1279 1263 | 
             
              # into blocks and associate them with the ListItem (in the case of a
         | 
| 1280 | 
            -
              #  | 
| 1264 | 
            +
              # description list, the description ListItem). Finally, fold the first block
         | 
| 1281 1265 | 
             
              # into the item's text attribute according to rules described in ListItem.
         | 
| 1282 1266 | 
             
              #
         | 
| 1283 1267 | 
             
              # reader        - The Reader from which to retrieve the next list item
         | 
| @@ -1301,7 +1285,7 @@ class Parser | |
| 1301 1285 | 
             
                      checkbox = true
         | 
| 1302 1286 | 
             
                      checked = false
         | 
| 1303 1287 | 
             
                      text = text[3..-1].lstrip
         | 
| 1304 | 
            -
                    elsif text.start_with?('[x] ' | 
| 1288 | 
            +
                    elsif text.start_with?('[x] ', '[*] ')
         | 
| 1305 1289 | 
             
                      checkbox = true
         | 
| 1306 1290 | 
             
                      checked = true
         | 
| 1307 1291 | 
             
                      text = text[3..-1].lstrip
         | 
| @@ -1323,22 +1307,21 @@ class Parser | |
| 1323 1307 |  | 
| 1324 1308 | 
             
                # first skip the line with the marker / term
         | 
| 1325 1309 | 
             
                reader.advance
         | 
| 1326 | 
            -
                 | 
| 1327 | 
            -
                list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), cursor
         | 
| 1310 | 
            +
                list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), reader.cursor
         | 
| 1328 1311 | 
             
                if list_item_reader.has_more_lines?
         | 
| 1312 | 
            +
                  # NOTE peek on the other side of any comment lines
         | 
| 1329 1313 | 
             
                  comment_lines = list_item_reader.skip_line_comments
         | 
| 1330 | 
            -
                  subsequent_line = list_item_reader.peek_line
         | 
| 1331 | 
            -
             | 
| 1332 | 
            -
             | 
| 1333 | 
            -
             | 
| 1334 | 
            -
                     | 
| 1335 | 
            -
             | 
| 1336 | 
            -
             | 
| 1337 | 
            -
             | 
| 1338 | 
            -
                      has_text = false
         | 
| 1314 | 
            +
                  if (subsequent_line = list_item_reader.peek_line)
         | 
| 1315 | 
            +
                    list_item_reader.unshift_lines comment_lines unless comment_lines.empty?
         | 
| 1316 | 
            +
                    if (continuation_connects_first_block = subsequent_line.empty?)
         | 
| 1317 | 
            +
                      content_adjacent = false
         | 
| 1318 | 
            +
                    else
         | 
| 1319 | 
            +
                      content_adjacent = true
         | 
| 1320 | 
            +
                      # treat lines as paragraph text if continuation does not connect first block (i.e., has_text = false)
         | 
| 1321 | 
            +
                      has_text = false unless list_type == :dlist
         | 
| 1339 1322 | 
             
                    end
         | 
| 1340 | 
            -
                    content_adjacent = !continuation_connects_first_block && !subsequent_line.empty?
         | 
| 1341 1323 | 
             
                  else
         | 
| 1324 | 
            +
                    # NOTE we have no use for any trailing comment lines we might have found
         | 
| 1342 1325 | 
             
                    continuation_connects_first_block = false
         | 
| 1343 1326 | 
             
                    content_adjacent = false
         | 
| 1344 1327 | 
             
                  end
         | 
| @@ -1359,10 +1342,11 @@ class Parser | |
| 1359 1342 | 
             
                end
         | 
| 1360 1343 |  | 
| 1361 1344 | 
             
                if list_type == :dlist
         | 
| 1362 | 
            -
                   | 
| 1363 | 
            -
                    list_item | 
| 1345 | 
            +
                  if list_item.text? || list_item.blocks?
         | 
| 1346 | 
            +
                    [list_term, list_item]
         | 
| 1347 | 
            +
                  else
         | 
| 1348 | 
            +
                    [list_term, nil]
         | 
| 1364 1349 | 
             
                  end
         | 
| 1365 | 
            -
                  [list_term, list_item]
         | 
| 1366 1350 | 
             
                else
         | 
| 1367 1351 | 
             
                  list_item
         | 
| 1368 1352 | 
             
                end
         | 
| @@ -1379,7 +1363,7 @@ class Parser | |
| 1379 1363 | 
             
              # list_type       - The Symbol context of the list (:ulist, :olist, :colist or :dlist)
         | 
| 1380 1364 | 
             
              # sibling_trait   - A Regexp that matches a sibling of this list item or String list marker
         | 
| 1381 1365 | 
             
              #                   of the items in this list (default: nil)
         | 
| 1382 | 
            -
              # has_text        - Whether the list item has text defined inline (always true except for  | 
| 1366 | 
            +
              # has_text        - Whether the list item has text defined inline (always true except for description lists)
         | 
| 1383 1367 | 
             
              #
         | 
| 1384 1368 | 
             
              # Returns an Array of lines belonging to the current list item.
         | 
| 1385 1369 | 
             
              def self.read_lines_for_list_item(reader, list_type, sibling_trait = nil, has_text = true)
         | 
| @@ -1443,7 +1427,7 @@ class Parser | |
| 1443 1427 | 
             
                  # technically BlockAttributeLineRx only breaks if ensuing line is not a list item
         | 
| 1444 1428 | 
             
                  # which really means BlockAttributeLineRx only breaks if it's acting as a block delimiter
         | 
| 1445 1429 | 
             
                  # FIXME to be AsciiDoc compliant, we shouldn't break if style in attribute line is "literal" (i.e., [literal])
         | 
| 1446 | 
            -
                  elsif list_type == :dlist && continuation != :active && BlockAttributeLineRx  | 
| 1430 | 
            +
                  elsif list_type == :dlist && continuation != :active && (BlockAttributeLineRx.match? this_line)
         | 
| 1447 1431 | 
             
                    break
         | 
| 1448 1432 | 
             
                  else
         | 
| 1449 1433 | 
             
                    if continuation == :active && !this_line.empty?
         | 
| @@ -1451,7 +1435,7 @@ class Parser | |
| 1451 1435 | 
             
                      # two entry points into one)
         | 
| 1452 1436 | 
             
                      # if we don't process it as a whole, then a line in it that looks like a
         | 
| 1453 1437 | 
             
                      # list item will throw off the exit from it
         | 
| 1454 | 
            -
                      if LiteralParagraphRx  | 
| 1438 | 
            +
                      if LiteralParagraphRx.match? this_line
         | 
| 1455 1439 | 
             
                        reader.unshift_line this_line
         | 
| 1456 1440 | 
             
                        buffer.concat reader.read_lines_until(
         | 
| 1457 1441 | 
             
                            :preserve_last_line => true,
         | 
| @@ -1463,12 +1447,12 @@ class Parser | |
| 1463 1447 | 
             
                        }
         | 
| 1464 1448 | 
             
                        continuation = :inactive
         | 
| 1465 1449 | 
             
                      # let block metadata play out until we find the block
         | 
| 1466 | 
            -
                      elsif BlockTitleRx  | 
| 1450 | 
            +
                      elsif (BlockTitleRx.match? this_line) || (BlockAttributeLineRx.match? this_line) || (AttributeEntryRx.match? this_line)
         | 
| 1467 1451 | 
             
                        buffer << this_line
         | 
| 1468 1452 | 
             
                      else
         | 
| 1469 | 
            -
                        if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx]  | 
| 1453 | 
            +
                        if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx].match? this_line }
         | 
| 1470 1454 | 
             
                          within_nested_list = true
         | 
| 1471 | 
            -
                          if nested_list_type == :dlist &&  | 
| 1455 | 
            +
                          if nested_list_type == :dlist && $3.nil_or_empty?
         | 
| 1472 1456 | 
             
                            # get greedy again
         | 
| 1473 1457 | 
             
                            has_text = false
         | 
| 1474 1458 | 
             
                          end
         | 
| @@ -1476,13 +1460,13 @@ class Parser | |
| 1476 1460 | 
             
                        buffer << this_line
         | 
| 1477 1461 | 
             
                        continuation = :inactive
         | 
| 1478 1462 | 
             
                      end
         | 
| 1479 | 
            -
                    elsif  | 
| 1463 | 
            +
                    elsif prev_line && prev_line.empty?
         | 
| 1480 1464 | 
             
                      # advance to the next line of content
         | 
| 1481 1465 | 
             
                      if this_line.empty?
         | 
| 1482 1466 | 
             
                        reader.skip_blank_lines
         | 
| 1483 1467 | 
             
                        this_line = reader.read_line
         | 
| 1484 | 
            -
                        # if we hit eof or a sibling | 
| 1485 | 
            -
                        break  | 
| 1468 | 
            +
                        # stop reading if we hit eof or a sibling list item
         | 
| 1469 | 
            +
                        break unless this_line && !is_sibling_list_item?(this_line, list_type, sibling_trait)
         | 
| 1486 1470 | 
             
                      end
         | 
| 1487 1471 |  | 
| 1488 1472 | 
             
                      if this_line == LIST_CONTINUATION
         | 
| @@ -1499,13 +1483,13 @@ class Parser | |
| 1499 1483 | 
             
                          elsif nested_list_type = NESTABLE_LIST_CONTEXTS.find {|ctx| ListRxMap[ctx] =~ this_line }
         | 
| 1500 1484 | 
             
                            buffer << this_line
         | 
| 1501 1485 | 
             
                            within_nested_list = true
         | 
| 1502 | 
            -
                            if nested_list_type == :dlist &&  | 
| 1486 | 
            +
                            if nested_list_type == :dlist && $3.nil_or_empty?
         | 
| 1503 1487 | 
             
                              # get greedy again
         | 
| 1504 1488 | 
             
                              has_text = false
         | 
| 1505 1489 | 
             
                            end
         | 
| 1506 1490 | 
             
                          # slurp up any literal paragraph offset by blank lines
         | 
| 1507 1491 | 
             
                          # NOTE we have to check for indented list items first
         | 
| 1508 | 
            -
                          elsif LiteralParagraphRx  | 
| 1492 | 
            +
                          elsif LiteralParagraphRx.match? this_line
         | 
| 1509 1493 | 
             
                            reader.unshift_line this_line
         | 
| 1510 1494 | 
             
                            buffer.concat reader.read_lines_until(
         | 
| 1511 1495 | 
             
                                :preserve_last_line => true,
         | 
| @@ -1529,7 +1513,7 @@ class Parser | |
| 1529 1513 | 
             
                      has_text = true if !this_line.empty?
         | 
| 1530 1514 | 
             
                      if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx] =~ this_line }
         | 
| 1531 1515 | 
             
                        within_nested_list = true
         | 
| 1532 | 
            -
                        if nested_list_type == :dlist &&  | 
| 1516 | 
            +
                        if nested_list_type == :dlist && $3.nil_or_empty?
         | 
| 1533 1517 | 
             
                          # get greedy again
         | 
| 1534 1518 | 
             
                          has_text = false
         | 
| 1535 1519 | 
             
                        end
         | 
| @@ -1553,7 +1537,7 @@ class Parser | |
| 1553 1537 | 
             
                # a blank line would have served the same purpose in the document
         | 
| 1554 1538 | 
             
                buffer.pop if !buffer.empty? && buffer[-1] == LIST_CONTINUATION
         | 
| 1555 1539 |  | 
| 1556 | 
            -
                #warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer *  | 
| 1540 | 
            +
                #warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer * LF}<BUFFER"
         | 
| 1557 1541 | 
             
                #warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.inspect}<BUFFER"
         | 
| 1558 1542 |  | 
| 1559 1543 | 
             
                buffer
         | 
| @@ -1567,93 +1551,94 @@ class Parser | |
| 1567 1551 | 
             
              # reader     - the source reader
         | 
| 1568 1552 | 
             
              # parent     - the parent Section or Document of this Section
         | 
| 1569 1553 | 
             
              # attributes - a Hash of attributes to assign to this section (default: {})
         | 
| 1570 | 
            -
              def self.initialize_section | 
| 1554 | 
            +
              def self.initialize_section reader, parent, attributes = {}
         | 
| 1571 1555 | 
             
                document = parent.document
         | 
| 1572 1556 | 
             
                source_location = reader.cursor if document.sourcemap
         | 
| 1573 | 
            -
                sect_id, sect_reftext, sect_title, sect_level,  | 
| 1574 | 
            -
                 | 
| 1575 | 
            -
             | 
| 1576 | 
            -
                 | 
| 1577 | 
            -
             | 
| 1578 | 
            -
                 | 
| 1579 | 
            -
                #  | 
| 1580 | 
            -
                 | 
| 1581 | 
            -
             | 
| 1582 | 
            -
             | 
| 1583 | 
            -
             | 
| 1584 | 
            -
             | 
| 1585 | 
            -
             | 
| 1586 | 
            -
                     | 
| 1587 | 
            -
                    if section.sectname == 'abstract' && document.doctype == 'book'
         | 
| 1588 | 
            -
                      section.sectname = 'sect1'
         | 
| 1589 | 
            -
                      section.special = false
         | 
| 1590 | 
            -
                      section.level = 1
         | 
| 1591 | 
            -
                    end
         | 
| 1557 | 
            +
                sect_id, sect_reftext, sect_title, sect_level, single_line = parse_section_title reader, document
         | 
| 1558 | 
            +
                if sect_reftext
         | 
| 1559 | 
            +
                  attributes['reftext'] = sect_reftext
         | 
| 1560 | 
            +
                elsif attributes.key? 'reftext'
         | 
| 1561 | 
            +
                  sect_reftext = attributes['reftext']
         | 
| 1562 | 
            +
                #elsif document.attributes.key? 'reftext'
         | 
| 1563 | 
            +
                #  sect_reftext = attributes['reftext'] = document.attributes['reftext']
         | 
| 1564 | 
            +
                end
         | 
| 1565 | 
            +
             | 
| 1566 | 
            +
                # parse style, id, and role attributes from first positional attribute if present
         | 
| 1567 | 
            +
                style = attributes[1] ? (parse_style_attribute attributes, reader) : nil
         | 
| 1568 | 
            +
                if style
         | 
| 1569 | 
            +
                  if style == 'abstract' && document.doctype == 'book'
         | 
| 1570 | 
            +
                    sect_name, sect_level = 'chapter', 1
         | 
| 1592 1571 | 
             
                  else
         | 
| 1593 | 
            -
                     | 
| 1572 | 
            +
                    sect_name, sect_special = style, true
         | 
| 1573 | 
            +
                    sect_level = 1 if sect_level == 0
         | 
| 1574 | 
            +
                    sect_numbered_force = style == 'appendix'
         | 
| 1594 1575 | 
             
                  end
         | 
| 1595 | 
            -
                elsif sect_title.downcase == 'synopsis' && document.doctype == 'manpage'
         | 
| 1596 | 
            -
                  section.special = true
         | 
| 1597 | 
            -
                  section.sectname = 'synopsis'
         | 
| 1598 1576 | 
             
                else
         | 
| 1599 | 
            -
                   | 
| 1577 | 
            +
                  case document.doctype
         | 
| 1578 | 
            +
                  when 'book'
         | 
| 1579 | 
            +
                    sect_name = sect_level == 0 ? 'part' : (sect_level == 1 ? 'chapter' : 'section')
         | 
| 1580 | 
            +
                  when 'manpage'
         | 
| 1581 | 
            +
                    if (sect_title.casecmp 'synopsis') == 0
         | 
| 1582 | 
            +
                      sect_name, sect_special = 'synopsis', true
         | 
| 1583 | 
            +
                    else
         | 
| 1584 | 
            +
                      sect_name = 'section'
         | 
| 1585 | 
            +
                    end
         | 
| 1586 | 
            +
                  else
         | 
| 1587 | 
            +
                    sect_name = 'section'
         | 
| 1588 | 
            +
                  end
         | 
| 1600 1589 | 
             
                end
         | 
| 1601 1590 |  | 
| 1602 | 
            -
                 | 
| 1603 | 
            -
             | 
| 1604 | 
            -
                 | 
| 1605 | 
            -
             | 
| 1606 | 
            -
                   | 
| 1607 | 
            -
                  section. | 
| 1591 | 
            +
                section = Section.new parent, sect_level, false
         | 
| 1592 | 
            +
                section.id, section.title, section.sectname, section.source_location = sect_id, sect_title, sect_name, source_location
         | 
| 1593 | 
            +
                # TODO honor special section numbering option (#661)
         | 
| 1594 | 
            +
                if sect_special
         | 
| 1595 | 
            +
                  section.special = true
         | 
| 1596 | 
            +
                  section.numbered = true if sect_numbered_force
         | 
| 1597 | 
            +
                elsif sect_level > 0 && (document.attributes.key? 'sectnums')
         | 
| 1598 | 
            +
                  section.numbered = section.special ? (parent.context == :section && parent.numbered) : true
         | 
| 1608 1599 | 
             
                end
         | 
| 1609 1600 |  | 
| 1610 | 
            -
                if section | 
| 1611 | 
            -
             | 
| 1612 | 
            -
             | 
| 1601 | 
            +
                # generate an ID if one was not embedded or specified as anchor above section title
         | 
| 1602 | 
            +
                if (id = section.id ||= (attributes['id'] ||
         | 
| 1603 | 
            +
                    ((document.attributes.key? 'sectids') ? (Section.generate_id section.title, document) : nil)))
         | 
| 1604 | 
            +
                  unless document.register :refs, [id, section, sect_reftext || section.title]
         | 
| 1605 | 
            +
                    warn %(asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - (single_line ? 1 : 2)}: id assigned to section already in use: #{id})
         | 
| 1606 | 
            +
                  end
         | 
| 1613 1607 | 
             
                end
         | 
| 1608 | 
            +
             | 
| 1614 1609 | 
             
                section.update_attributes(attributes)
         | 
| 1615 1610 | 
             
                reader.skip_blank_lines
         | 
| 1616 1611 |  | 
| 1617 1612 | 
             
                section
         | 
| 1618 1613 | 
             
              end
         | 
| 1619 1614 |  | 
| 1620 | 
            -
              # Private: Get the Integer section level based on the characters
         | 
| 1621 | 
            -
              # used in the ASCII line under the section title.
         | 
| 1622 | 
            -
              #
         | 
| 1623 | 
            -
              # line - the String line from under the section title.
         | 
| 1624 | 
            -
              def self.section_level(line)
         | 
| 1625 | 
            -
                SECTION_LEVELS[line.chr]
         | 
| 1626 | 
            -
              end
         | 
| 1627 | 
            -
             | 
| 1628 | 
            -
              #--
         | 
| 1629 | 
            -
              # = is level 0, == is level 1, etc.
         | 
| 1630 | 
            -
              def self.single_line_section_level(marker)
         | 
| 1631 | 
            -
                marker.length - 1
         | 
| 1632 | 
            -
              end
         | 
| 1633 | 
            -
             | 
| 1634 1615 | 
             
              # Internal: Checks if the next line on the Reader is a section title
         | 
| 1635 1616 | 
             
              #
         | 
| 1636 1617 | 
             
              # reader     - the source Reader
         | 
| 1637 1618 | 
             
              # attributes - a Hash of attributes collected above the current line
         | 
| 1638 1619 | 
             
              #
         | 
| 1639 | 
            -
              #  | 
| 1640 | 
            -
              # false otherwise
         | 
| 1620 | 
            +
              # Returns the Integer section level if the Reader is positioned at a section title or nil otherwise
         | 
| 1641 1621 | 
             
              def self.is_next_line_section?(reader, attributes)
         | 
| 1642 | 
            -
                if  | 
| 1643 | 
            -
                  return | 
| 1622 | 
            +
                if attributes.key?(1) && (attr1 = attributes[1] || '').start_with?('float', 'discrete') && FloatingTitleStyleRx.match?(attr1)
         | 
| 1623 | 
            +
                  return
         | 
| 1624 | 
            +
                elsif reader.has_more_lines?
         | 
| 1625 | 
            +
                  Compliance.underline_style_section_titles ? is_section_title?(*reader.peek_lines(2)) : is_section_title?(reader.peek_line)
         | 
| 1644 1626 | 
             
                end
         | 
| 1645 | 
            -
                return false unless reader.has_more_lines?
         | 
| 1646 | 
            -
                Compliance.underline_style_section_titles ? is_section_title?(*reader.peek_lines(2)) : is_section_title?(reader.peek_line)
         | 
| 1647 1627 | 
             
              end
         | 
| 1648 1628 |  | 
| 1649 1629 | 
             
              # Internal: Convenience API for checking if the next line on the Reader is the document title
         | 
| 1650 1630 | 
             
              #
         | 
| 1651 | 
            -
              # reader | 
| 1652 | 
            -
              # attributes | 
| 1631 | 
            +
              # reader      - the source Reader
         | 
| 1632 | 
            +
              # attributes  - a Hash of attributes collected above the current line
         | 
| 1633 | 
            +
              # leveloffset - an Integer (or integer String value) the represents the current leveloffset
         | 
| 1653 1634 | 
             
              #
         | 
| 1654 1635 | 
             
              # returns true if the Reader is positioned at the document title, false otherwise
         | 
| 1655 | 
            -
              def self. | 
| 1656 | 
            -
                 | 
| 1636 | 
            +
              def self.is_next_line_doctitle? reader, attributes, leveloffset
         | 
| 1637 | 
            +
                if leveloffset
         | 
| 1638 | 
            +
                  (sect_level = is_next_line_section? reader, attributes) && (sect_level + leveloffset.to_i == 0)
         | 
| 1639 | 
            +
                else
         | 
| 1640 | 
            +
                  (is_next_line_section? reader, attributes) == 0
         | 
| 1641 | 
            +
                end
         | 
| 1657 1642 | 
             
              end
         | 
| 1658 1643 |  | 
| 1659 1644 | 
             
              # Public: Checks if these lines are a section title
         | 
| @@ -1661,36 +1646,24 @@ class Parser | |
| 1661 1646 | 
             
              # line1 - the first line as a String
         | 
| 1662 1647 | 
             
              # line2 - the second line as a String (default: nil)
         | 
| 1663 1648 | 
             
              #
         | 
| 1664 | 
            -
              #  | 
| 1665 | 
            -
              # false otherwise
         | 
| 1649 | 
            +
              # Returns the Integer section level if these lines are a section title or nil otherwise
         | 
| 1666 1650 | 
             
              def self.is_section_title?(line1, line2 = nil)
         | 
| 1667 | 
            -
                 | 
| 1668 | 
            -
                  level
         | 
| 1669 | 
            -
                elsif line2 && (level = is_two_line_section_title?(line1, line2))
         | 
| 1670 | 
            -
                  level
         | 
| 1671 | 
            -
                else
         | 
| 1672 | 
            -
                  false
         | 
| 1673 | 
            -
                end
         | 
| 1651 | 
            +
                is_single_line_section_title?(line1) || (line2.nil_or_empty? ? nil : is_two_line_section_title?(line1, line2))
         | 
| 1674 1652 | 
             
              end
         | 
| 1675 1653 |  | 
| 1676 1654 | 
             
              def self.is_single_line_section_title?(line1)
         | 
| 1677 | 
            -
                 | 
| 1678 | 
            -
                if ( | 
| 1679 | 
            -
             | 
| 1680 | 
            -
                   | 
| 1681 | 
            -
                else
         | 
| 1682 | 
            -
                  false
         | 
| 1655 | 
            +
                if (line1.start_with?('=') || (Compliance.markdown_syntax && line1.start_with?('#'))) && AtxSectionRx =~ line1
         | 
| 1656 | 
            +
                #if line1.start_with?('=', '#') && AtxSectionRx =~ line1 && (line1.start_with?('=') || Compliance.markdown_syntax)
         | 
| 1657 | 
            +
                  # NOTE level is 1 less than number of line markers
         | 
| 1658 | 
            +
                  $1.length - 1
         | 
| 1683 1659 | 
             
                end
         | 
| 1684 1660 | 
             
              end
         | 
| 1685 1661 |  | 
| 1686 1662 | 
             
              def self.is_two_line_section_title?(line1, line2)
         | 
| 1687 | 
            -
                if  | 
| 1688 | 
            -
                    line2  | 
| 1689 | 
            -
                     | 
| 1690 | 
            -
             | 
| 1691 | 
            -
                  section_level line2
         | 
| 1692 | 
            -
                else
         | 
| 1693 | 
            -
                  false
         | 
| 1663 | 
            +
                if (level = SETEXT_SECTION_LEVELS[line2_ch1 = line2.chr]) &&
         | 
| 1664 | 
            +
                    line2_ch1 * (line2_len = line2.length) == line2 && SetextSectionTitleRx.match?(line1) &&
         | 
| 1665 | 
            +
                    (line_length(line1) - line2_len).abs < 2
         | 
| 1666 | 
            +
                  level
         | 
| 1694 1667 | 
             
                end
         | 
| 1695 1668 | 
             
              end
         | 
| 1696 1669 |  | 
| @@ -1738,46 +1711,29 @@ class Parser | |
| 1738 1711 | 
             
              #--
         | 
| 1739 1712 | 
             
              # NOTE for efficiency, we don't reuse methods that check for a section title
         | 
| 1740 1713 | 
             
              def self.parse_section_title(reader, document)
         | 
| 1714 | 
            +
                sect_id = sect_reftext = nil
         | 
| 1741 1715 | 
             
                line1 = reader.read_line
         | 
| 1742 | 
            -
             | 
| 1743 | 
            -
                 | 
| 1744 | 
            -
                 | 
| 1745 | 
            -
             | 
| 1746 | 
            -
             | 
| 1747 | 
            -
             | 
| 1748 | 
            -
             | 
| 1749 | 
            -
             | 
| 1750 | 
            -
             | 
| 1751 | 
            -
             | 
| 1752 | 
            -
             | 
| 1753 | 
            -
             | 
| 1754 | 
            -
             | 
| 1755 | 
            -
             | 
| 1756 | 
            -
             | 
| 1757 | 
            -
                      sect_reftext = anchor_match[4]
         | 
| 1758 | 
            -
                    end
         | 
| 1759 | 
            -
                  end
         | 
| 1760 | 
            -
                elsif Compliance.underline_style_section_titles
         | 
| 1761 | 
            -
                  if (line2 = reader.peek_line(true)) && SECTION_LEVELS.has_key?(line2.chr) && line2 =~ SetextSectionLineRx &&
         | 
| 1762 | 
            -
                    (name_match = SetextSectionTitleRx.match(line1)) &&
         | 
| 1763 | 
            -
                    # chomp so that a (non-visible) endline does not impact calculation
         | 
| 1764 | 
            -
                    (line_length(line1) - line_length(line2)).abs <= 1
         | 
| 1765 | 
            -
                    sect_title = name_match[1]
         | 
| 1766 | 
            -
                    if sect_title.end_with?(']]') && (anchor_match = InlineSectionAnchorRx.match(sect_title))
         | 
| 1767 | 
            -
                      if anchor_match[2].nil?
         | 
| 1768 | 
            -
                        sect_title = anchor_match[1]
         | 
| 1769 | 
            -
                        sect_id = anchor_match[3]
         | 
| 1770 | 
            -
                        sect_reftext = anchor_match[4]
         | 
| 1771 | 
            -
                      end
         | 
| 1772 | 
            -
                    end
         | 
| 1773 | 
            -
                    sect_level = section_level line2
         | 
| 1774 | 
            -
                    single_line = false
         | 
| 1775 | 
            -
                    reader.advance
         | 
| 1716 | 
            +
             | 
| 1717 | 
            +
                #if line1.start_with?('=', '#') && AtxSectionRx =~ line1 && (line1.start_with?('=') || Compliance.markdown_syntax)
         | 
| 1718 | 
            +
                if (line1.start_with?('=') || (Compliance.markdown_syntax && line1.start_with?('#'))) && AtxSectionRx =~ line1
         | 
| 1719 | 
            +
                  # NOTE level is 1 less than number of line markers
         | 
| 1720 | 
            +
                  sect_level, sect_title, single_line = $1.length - 1, $2, true
         | 
| 1721 | 
            +
                  if sect_title.end_with?(']]') && InlineSectionAnchorRx =~ sect_title && !$1 # escaped
         | 
| 1722 | 
            +
                    sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
         | 
| 1723 | 
            +
                  end
         | 
| 1724 | 
            +
                elsif Compliance.underline_style_section_titles && (line2 = reader.peek_line(true)) &&
         | 
| 1725 | 
            +
                    (sect_level = SETEXT_SECTION_LEVELS[line2_ch1 = line2.chr]) &&
         | 
| 1726 | 
            +
                    line2_ch1 * (line2_len = line2.length) == line2 && (sect_title = SetextSectionTitleRx =~ line1 && $1) &&
         | 
| 1727 | 
            +
                    (line_length(line1) - line2_len).abs < 2
         | 
| 1728 | 
            +
                  single_line = false
         | 
| 1729 | 
            +
                  if sect_title.end_with?(']]') && InlineSectionAnchorRx =~ sect_title && !$1 # escaped
         | 
| 1730 | 
            +
                    sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
         | 
| 1776 1731 | 
             
                  end
         | 
| 1732 | 
            +
                  reader.advance
         | 
| 1733 | 
            +
                else
         | 
| 1734 | 
            +
                  raise %(Unrecognized section at #{reader.prev_line_info})
         | 
| 1777 1735 | 
             
                end
         | 
| 1778 | 
            -
                 | 
| 1779 | 
            -
                  sect_level += document.attr('leveloffset', 0).to_i
         | 
| 1780 | 
            -
                end
         | 
| 1736 | 
            +
                sect_level += document.attr('leveloffset').to_i if document.attr?('leveloffset')
         | 
| 1781 1737 | 
             
                [sect_id, sect_reftext, sect_title, sect_level, single_line]
         | 
| 1782 1738 | 
             
              end
         | 
| 1783 1739 |  | 
| @@ -1786,8 +1742,14 @@ class Parser | |
| 1786 1742 | 
             
              # line - the String to calculate
         | 
| 1787 1743 | 
             
              #
         | 
| 1788 1744 | 
             
              # returns the number of unicode characters in the line
         | 
| 1789 | 
            -
               | 
| 1790 | 
            -
                 | 
| 1745 | 
            +
              if FORCE_UNICODE_LINE_LENGTH
         | 
| 1746 | 
            +
                def self.line_length(line)
         | 
| 1747 | 
            +
                  line.scan(UnicodeCharScanRx).size
         | 
| 1748 | 
            +
                end
         | 
| 1749 | 
            +
              else
         | 
| 1750 | 
            +
                def self.line_length(line)
         | 
| 1751 | 
            +
                  line.length
         | 
| 1752 | 
            +
                end
         | 
| 1791 1753 | 
             
              end
         | 
| 1792 1754 |  | 
| 1793 1755 | 
             
              # Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info).
         | 
| @@ -1806,20 +1768,16 @@ class Parser | |
| 1806 1768 | 
             
              #  #       'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.'}
         | 
| 1807 1769 | 
             
              def self.parse_header_metadata(reader, document = nil)
         | 
| 1808 1770 | 
             
                # NOTE this will discard away any comment lines, but not skip blank lines
         | 
| 1809 | 
            -
                process_attribute_entries | 
| 1771 | 
            +
                process_attribute_entries reader, document
         | 
| 1810 1772 |  | 
| 1811 | 
            -
                metadata = {}
         | 
| 1812 | 
            -
                implicit_author = nil
         | 
| 1813 | 
            -
                implicit_authors = nil
         | 
| 1773 | 
            +
                metadata, implicit_author, implicit_authors = {}, nil, nil
         | 
| 1814 1774 |  | 
| 1815 1775 | 
             
                if reader.has_more_lines? && !reader.next_line_empty?
         | 
| 1816 | 
            -
                  author_metadata = process_authors reader.read_line
         | 
| 1817 | 
            -
             | 
| 1818 | 
            -
                  unless author_metadata.empty?
         | 
| 1776 | 
            +
                  unless (author_metadata = process_authors reader.read_line).empty?
         | 
| 1819 1777 | 
             
                    if document
         | 
| 1820 1778 | 
             
                      # apply header subs and assign to document
         | 
| 1821 1779 | 
             
                      author_metadata.each do |key, val|
         | 
| 1822 | 
            -
                        unless document.attributes. | 
| 1780 | 
            +
                        unless document.attributes.key? key
         | 
| 1823 1781 | 
             
                          document.attributes[key] = ::String === val ? (document.apply_header_subs val) : val
         | 
| 1824 1782 | 
             
                        end
         | 
| 1825 1783 | 
             
                      end
         | 
| @@ -1832,7 +1790,7 @@ class Parser | |
| 1832 1790 | 
             
                  end
         | 
| 1833 1791 |  | 
| 1834 1792 | 
             
                  # NOTE this will discard any comment lines, but not skip blank lines
         | 
| 1835 | 
            -
                  process_attribute_entries | 
| 1793 | 
            +
                  process_attribute_entries reader, document
         | 
| 1836 1794 |  | 
| 1837 1795 | 
             
                  rev_metadata = {}
         | 
| 1838 1796 |  | 
| @@ -1859,7 +1817,7 @@ class Parser | |
| 1859 1817 | 
             
                    if document
         | 
| 1860 1818 | 
             
                      # apply header subs and assign to document
         | 
| 1861 1819 | 
             
                      rev_metadata.each do |key, val|
         | 
| 1862 | 
            -
                        unless document.attributes. | 
| 1820 | 
            +
                        unless document.attributes.key? key
         | 
| 1863 1821 | 
             
                          document.attributes[key] = document.apply_header_subs(val)
         | 
| 1864 1822 | 
             
                        end
         | 
| 1865 1823 | 
             
                      end
         | 
| @@ -1869,43 +1827,56 @@ class Parser | |
| 1869 1827 | 
             
                  end
         | 
| 1870 1828 |  | 
| 1871 1829 | 
             
                  # NOTE this will discard any comment lines, but not skip blank lines
         | 
| 1872 | 
            -
                  process_attribute_entries | 
| 1830 | 
            +
                  process_attribute_entries reader, document
         | 
| 1873 1831 |  | 
| 1874 1832 | 
             
                  reader.skip_blank_lines
         | 
| 1875 1833 | 
             
                end
         | 
| 1876 1834 |  | 
| 1835 | 
            +
                # process author attribute entries that override (or stand in for) the implicit author line
         | 
| 1877 1836 | 
             
                if document
         | 
| 1878 | 
            -
                   | 
| 1879 | 
            -
                  author_metadata = nil
         | 
| 1880 | 
            -
                  if document.attributes.has_key?('author') &&
         | 
| 1881 | 
            -
                      (author_line = document.attributes['author']) != implicit_author
         | 
| 1837 | 
            +
                  if document.attributes.key?('author') && (author_line = document.attributes['author']) != implicit_author
         | 
| 1882 1838 | 
             
                    # do not allow multiple, process as names only
         | 
| 1883 1839 | 
             
                    author_metadata = process_authors author_line, true, false
         | 
| 1884 | 
            -
                  elsif document.attributes. | 
| 1885 | 
            -
                      (author_line = document.attributes['authors']) != implicit_authors
         | 
| 1840 | 
            +
                  elsif document.attributes.key?('authors') && (author_line = document.attributes['authors']) != implicit_authors
         | 
| 1886 1841 | 
             
                    # allow multiple, process as names only
         | 
| 1887 1842 | 
             
                    author_metadata = process_authors author_line, true
         | 
| 1888 1843 | 
             
                  else
         | 
| 1889 | 
            -
                    authors = []
         | 
| 1890 | 
            -
                     | 
| 1891 | 
            -
             | 
| 1892 | 
            -
                       | 
| 1893 | 
            -
                       | 
| 1844 | 
            +
                    authors, author_idx, author_key, explicit, sparse = [], 1, 'author_1', false, false
         | 
| 1845 | 
            +
                    while document.attributes.key? author_key
         | 
| 1846 | 
            +
                      # only use indexed author attribute if value is different
         | 
| 1847 | 
            +
                      # leaves corner case if line matches with underscores converted to spaces; use double space to force
         | 
| 1848 | 
            +
                      if (author_override = document.attributes[author_key]) == author_metadata[author_key]
         | 
| 1849 | 
            +
                        authors << nil
         | 
| 1850 | 
            +
                        sparse = true
         | 
| 1851 | 
            +
                      else
         | 
| 1852 | 
            +
                        authors << author_override
         | 
| 1853 | 
            +
                        explicit = true
         | 
| 1854 | 
            +
                      end
         | 
| 1855 | 
            +
                      author_key = %(author_#{author_idx += 1})
         | 
| 1894 1856 | 
             
                    end
         | 
| 1895 | 
            -
                    if  | 
| 1896 | 
            -
                      #  | 
| 1897 | 
            -
                       | 
| 1898 | 
            -
             | 
| 1899 | 
            -
             | 
| 1900 | 
            -
             | 
| 1857 | 
            +
                    if explicit
         | 
| 1858 | 
            +
                      # rebuild implicit author names to reparse
         | 
| 1859 | 
            +
                      authors.each_with_index do |author, idx|
         | 
| 1860 | 
            +
                        unless author
         | 
| 1861 | 
            +
                          authors[idx] = [
         | 
| 1862 | 
            +
                            author_metadata[%(firstname_#{name_idx = idx + 1})],
         | 
| 1863 | 
            +
                            author_metadata[%(middlename_#{name_idx})],
         | 
| 1864 | 
            +
                            author_metadata[%(lastname_#{name_idx})]
         | 
| 1865 | 
            +
                          ].compact.map {|it| it.tr ' ', '_' } * ' '
         | 
| 1866 | 
            +
                        end
         | 
| 1867 | 
            +
                      end if sparse
         | 
| 1868 | 
            +
                      # process as names only
         | 
| 1869 | 
            +
                      author_metadata = process_authors authors, true, false
         | 
| 1870 | 
            +
                    else
         | 
| 1871 | 
            +
                      author_metadata = {}
         | 
| 1901 1872 | 
             
                    end
         | 
| 1902 1873 | 
             
                  end
         | 
| 1903 1874 |  | 
| 1904 | 
            -
                   | 
| 1875 | 
            +
                  unless author_metadata.empty?
         | 
| 1905 1876 | 
             
                    document.attributes.update author_metadata
         | 
| 1906 1877 |  | 
| 1907 1878 | 
             
                    # special case
         | 
| 1908 | 
            -
                    if !document.attributes. | 
| 1879 | 
            +
                    if !document.attributes.key?('email') && document.attributes.key?('email_1')
         | 
| 1909 1880 | 
             
                      document.attributes['email'] = document.attributes['email_1']
         | 
| 1910 1881 | 
             
                    end
         | 
| 1911 1882 | 
             
                  end
         | 
| @@ -1923,10 +1894,10 @@ class Parser | |
| 1923 1894 | 
             
              #                semicolon-separated entries in the author line (default: true)
         | 
| 1924 1895 | 
             
              #
         | 
| 1925 1896 | 
             
              # returns a Hash of author metadata
         | 
| 1926 | 
            -
              def self.process_authors | 
| 1897 | 
            +
              def self.process_authors author_line, names_only = false, multiple = true
         | 
| 1927 1898 | 
             
                author_metadata = {}
         | 
| 1928 1899 | 
             
                keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
         | 
| 1929 | 
            -
                author_entries = multiple ? (author_line.split ';').map {| | 
| 1900 | 
            +
                author_entries = multiple ? (author_line.split ';').map {|it| it.strip } : Array(author_line)
         | 
| 1930 1901 | 
             
                author_entries.each_with_index do |author_entry, idx|
         | 
| 1931 1902 | 
             
                  next if author_entry.empty?
         | 
| 1932 1903 | 
             
                  key_map = {}
         | 
| @@ -1941,42 +1912,47 @@ class Parser | |
| 1941 1912 | 
             
                  end
         | 
| 1942 1913 |  | 
| 1943 1914 | 
             
                  segments = nil
         | 
| 1944 | 
            -
                  if names_only
         | 
| 1945 | 
            -
                    #  | 
| 1946 | 
            -
                     | 
| 1947 | 
            -
             | 
| 1948 | 
            -
                       | 
| 1915 | 
            +
                  if names_only # when parsing an attribute value
         | 
| 1916 | 
            +
                    # QUESTION should we rstrip author_entry?
         | 
| 1917 | 
            +
                    if author_entry.include? '<'
         | 
| 1918 | 
            +
                      author_metadata[key_map[:author]] = author_entry.tr('_', ' ')
         | 
| 1919 | 
            +
                      author_entry = author_entry.gsub XmlSanitizeRx, ''
         | 
| 1920 | 
            +
                    end
         | 
| 1921 | 
            +
                    # NOTE split names and collapse repeating whitespace (split drops any leading whitespace)
         | 
| 1922 | 
            +
                    if (segments = author_entry.split nil, 3).size == 3
         | 
| 1923 | 
            +
                      segments << (segments.pop.squeeze ' ')
         | 
| 1949 1924 | 
             
                    end
         | 
| 1950 1925 | 
             
                  elsif (match = AuthorInfoLineRx.match(author_entry))
         | 
| 1951 | 
            -
                    segments = match.to_a
         | 
| 1952 | 
            -
             | 
| 1953 | 
            -
             | 
| 1954 | 
            -
             | 
| 1955 | 
            -
             | 
| 1956 | 
            -
                    author_metadata[key_map[: | 
| 1957 | 
            -
                     | 
| 1958 | 
            -
             | 
| 1959 | 
            -
             | 
| 1960 | 
            -
             | 
| 1961 | 
            -
             | 
| 1962 | 
            -
             | 
| 1963 | 
            -
                       | 
| 1964 | 
            -
             | 
| 1965 | 
            -
             | 
| 1966 | 
            -
             | 
| 1967 | 
            -
                       | 
| 1926 | 
            +
                    (segments = match.to_a).shift
         | 
| 1927 | 
            +
                  end
         | 
| 1928 | 
            +
             | 
| 1929 | 
            +
                  if segments
         | 
| 1930 | 
            +
                    author = author_metadata[key_map[:firstname]] = fname = segments[0].tr('_', ' ')
         | 
| 1931 | 
            +
                    author_metadata[key_map[:authorinitials]] = fname.chr
         | 
| 1932 | 
            +
                    if segments[1]
         | 
| 1933 | 
            +
                      if segments[2]
         | 
| 1934 | 
            +
                        author_metadata[key_map[:middlename]] = mname = segments[1].tr('_', ' ')
         | 
| 1935 | 
            +
                        author_metadata[key_map[:lastname]] = lname = segments[2].tr('_', ' ')
         | 
| 1936 | 
            +
                        author = fname + ' ' + mname + ' ' + lname
         | 
| 1937 | 
            +
                        author_metadata[key_map[:authorinitials]] = %(#{fname.chr}#{mname.chr}#{lname.chr})
         | 
| 1938 | 
            +
                      else
         | 
| 1939 | 
            +
                        author_metadata[key_map[:lastname]] = lname = segments[1].tr('_', ' ')
         | 
| 1940 | 
            +
                        author = fname + ' ' + lname
         | 
| 1941 | 
            +
                        author_metadata[key_map[:authorinitials]] = %(#{fname.chr}#{lname.chr})
         | 
| 1942 | 
            +
                      end
         | 
| 1968 1943 | 
             
                    end
         | 
| 1969 | 
            -
                    author_metadata[key_map[: | 
| 1944 | 
            +
                    author_metadata[key_map[:author]] ||= author
         | 
| 1945 | 
            +
                    author_metadata[key_map[:email]] = segments[3] unless names_only || !segments[3]
         | 
| 1970 1946 | 
             
                  else
         | 
| 1971 | 
            -
                    author_metadata[key_map[:author]] = author_metadata[key_map[:firstname]] = fname = author_entry. | 
| 1972 | 
            -
                    author_metadata[key_map[:authorinitials]] = fname | 
| 1947 | 
            +
                    author_metadata[key_map[:author]] = author_metadata[key_map[:firstname]] = fname = author_entry.squeeze(' ').strip
         | 
| 1948 | 
            +
                    author_metadata[key_map[:authorinitials]] = fname.chr
         | 
| 1973 1949 | 
             
                  end
         | 
| 1974 1950 |  | 
| 1975 1951 | 
             
                  author_metadata['authorcount'] = idx + 1
         | 
| 1976 1952 | 
             
                  # only assign the _1 attributes if there are multiple authors
         | 
| 1977 1953 | 
             
                  if idx == 1
         | 
| 1978 1954 | 
             
                    keys.each do |key|
         | 
| 1979 | 
            -
                      author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata. | 
| 1955 | 
            +
                      author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key
         | 
| 1980 1956 | 
             
                    end
         | 
| 1981 1957 | 
             
                  end
         | 
| 1982 1958 | 
             
                  if idx == 0
         | 
| @@ -1995,15 +1971,15 @@ class Parser | |
| 1995 1971 | 
             
              # blank lines and comments.
         | 
| 1996 1972 | 
             
              #
         | 
| 1997 1973 | 
             
              # reader     - the source reader
         | 
| 1998 | 
            -
              #  | 
| 1974 | 
            +
              # document   - the current Document
         | 
| 1999 1975 | 
             
              # attributes - a Hash of attributes in which any metadata found will be stored (default: {})
         | 
| 2000 1976 | 
             
              # options    - a Hash of options to control processing: (default: {})
         | 
| 2001 | 
            -
              #              *  :text indicates that  | 
| 1977 | 
            +
              #              *  :text indicates that parser is only looking for text content
         | 
| 2002 1978 | 
             
              #                   and thus the block title should not be captured
         | 
| 2003 1979 | 
             
              #
         | 
| 2004 1980 | 
             
              # returns the Hash of attributes including any metadata found
         | 
| 2005 | 
            -
              def self.parse_block_metadata_lines | 
| 2006 | 
            -
                while parse_block_metadata_line | 
| 1981 | 
            +
              def self.parse_block_metadata_lines reader, document, attributes = {}, options = {}
         | 
| 1982 | 
            +
                while parse_block_metadata_line reader, document, attributes, options
         | 
| 2007 1983 | 
             
                  # discard the line just processed
         | 
| 2008 1984 | 
             
                  reader.advance
         | 
| 2009 1985 | 
             
                  reader.skip_blank_lines
         | 
| @@ -2024,124 +2000,128 @@ class Parser | |
| 2024 2000 | 
             
              # If the line contains block metadata, the method returns true, otherwise false.
         | 
| 2025 2001 | 
             
              #
         | 
| 2026 2002 | 
             
              # reader     - the source reader
         | 
| 2027 | 
            -
              #  | 
| 2003 | 
            +
              # document   - the current Document
         | 
| 2028 2004 | 
             
              # attributes - a Hash of attributes in which any metadata found will be stored
         | 
| 2029 2005 | 
             
              # options    - a Hash of options to control processing: (default: {})
         | 
| 2030 | 
            -
              #              *  :text indicates  | 
| 2031 | 
            -
              #                    | 
| 2006 | 
            +
              #              *  :text indicates the parser is only looking for text content,
         | 
| 2007 | 
            +
              #                   thus neither a block title or attribute entry should be captured
         | 
| 2032 2008 | 
             
              #
         | 
| 2033 2009 | 
             
              # returns true if the line contains metadata, otherwise false
         | 
| 2034 | 
            -
              def self.parse_block_metadata_line | 
| 2035 | 
            -
                 | 
| 2036 | 
            -
             | 
| 2037 | 
            -
             | 
| 2038 | 
            -
             | 
| 2039 | 
            -
             | 
| 2040 | 
            -
             | 
| 2041 | 
            -
             | 
| 2042 | 
            -
             | 
| 2043 | 
            -
             | 
| 2044 | 
            -
             | 
| 2045 | 
            -
             | 
| 2046 | 
            -
             | 
| 2047 | 
            -
                     | 
| 2048 | 
            -
             | 
| 2049 | 
            -
             | 
| 2050 | 
            -
                     | 
| 2051 | 
            -
                   | 
| 2052 | 
            -
             | 
| 2053 | 
            -
             | 
| 2054 | 
            -
             | 
| 2055 | 
            -
             | 
| 2056 | 
            -
             | 
| 2057 | 
            -
             | 
| 2058 | 
            -
             | 
| 2059 | 
            -
             | 
| 2010 | 
            +
              def self.parse_block_metadata_line reader, document, attributes, options = {}
         | 
| 2011 | 
            +
                if (next_line = reader.peek_line) &&
         | 
| 2012 | 
            +
                    (options[:text] ? (next_line.start_with? '[', '/') : (normal = next_line.start_with? '[', '.', '/', ':'))
         | 
| 2013 | 
            +
                  if next_line.start_with? '['
         | 
| 2014 | 
            +
                    if next_line.start_with? '[['
         | 
| 2015 | 
            +
                      if (next_line.end_with? ']]') && BlockAnchorRx =~ next_line
         | 
| 2016 | 
            +
                        # NOTE registration of id and reftext is deferred until block is processed
         | 
| 2017 | 
            +
                        attributes['id'] = $1
         | 
| 2018 | 
            +
                        if (reftext = $2)
         | 
| 2019 | 
            +
                          attributes['reftext'] = (reftext.include? '{') ? (document.sub_attributes reftext) : reftext
         | 
| 2020 | 
            +
                        end
         | 
| 2021 | 
            +
                        return true
         | 
| 2022 | 
            +
                      end
         | 
| 2023 | 
            +
                    elsif (next_line.end_with? ']') && BlockAttributeListRx =~ next_line
         | 
| 2024 | 
            +
                      document.parse_attributes $1, [], :sub_input => true, :into => attributes
         | 
| 2025 | 
            +
                      return true
         | 
| 2026 | 
            +
                    end
         | 
| 2027 | 
            +
                  elsif normal && (next_line.start_with? '.')
         | 
| 2028 | 
            +
                    if BlockTitleRx =~ next_line
         | 
| 2029 | 
            +
                      # NOTE title doesn't apply to section, but we need to stash it for the first block
         | 
| 2030 | 
            +
                      # TODO should issue an error if this is found above the document title
         | 
| 2031 | 
            +
                      attributes['title'] = $1
         | 
| 2032 | 
            +
                      return true
         | 
| 2033 | 
            +
                    end
         | 
| 2034 | 
            +
                  elsif !normal || (next_line.start_with? '/')
         | 
| 2035 | 
            +
                    if next_line == '//'
         | 
| 2036 | 
            +
                      return true
         | 
| 2037 | 
            +
                    elsif normal && '/' * (ll = next_line.length) == next_line
         | 
| 2038 | 
            +
                      unless ll == 3
         | 
| 2039 | 
            +
                        reader.read_lines_until :skip_first_line => true, :preserve_last_line => true, :terminator => next_line, :skip_processing => true
         | 
| 2040 | 
            +
                        return true
         | 
| 2041 | 
            +
                      end
         | 
| 2042 | 
            +
                    else
         | 
| 2043 | 
            +
                      return true unless next_line.start_with? '///'
         | 
| 2044 | 
            +
                    end if next_line.start_with? '//'
         | 
| 2045 | 
            +
                  # NOTE the final condition can be consolidated into single line
         | 
| 2046 | 
            +
                  elsif normal && (next_line.start_with? ':') && AttributeEntryRx =~ next_line
         | 
| 2047 | 
            +
                    process_attribute_entry reader, document, attributes, $~
         | 
| 2048 | 
            +
                    return true
         | 
| 2049 | 
            +
                  end
         | 
| 2060 2050 | 
             
                end
         | 
| 2061 | 
            -
             | 
| 2062 | 
            -
                true
         | 
| 2063 2051 | 
             
              end
         | 
| 2064 2052 |  | 
| 2065 | 
            -
              def self.process_attribute_entries | 
| 2053 | 
            +
              def self.process_attribute_entries reader, document, attributes = nil
         | 
| 2066 2054 | 
             
                reader.skip_comment_lines
         | 
| 2067 | 
            -
                while process_attribute_entry | 
| 2055 | 
            +
                while process_attribute_entry reader, document, attributes
         | 
| 2068 2056 | 
             
                  # discard line just processed
         | 
| 2069 2057 | 
             
                  reader.advance
         | 
| 2070 2058 | 
             
                  reader.skip_comment_lines
         | 
| 2071 2059 | 
             
                end
         | 
| 2072 2060 | 
             
              end
         | 
| 2073 2061 |  | 
| 2074 | 
            -
              def self.process_attribute_entry | 
| 2075 | 
            -
                match ||= (reader.has_more_lines? ? AttributeEntryRx.match | 
| 2076 | 
            -
             | 
| 2077 | 
            -
             | 
| 2078 | 
            -
                   | 
| 2079 | 
            -
                     | 
| 2080 | 
            -
             | 
| 2081 | 
            -
                       | 
| 2082 | 
            -
             | 
| 2083 | 
            -
                        break if (next_line = reader.peek_line.strip).empty?
         | 
| 2084 | 
            -
                        if (keep_open = next_line.end_with? line_continuation)
         | 
| 2085 | 
            -
                          next_line = next_line.chop.rstrip
         | 
| 2086 | 
            -
                        end
         | 
| 2087 | 
            -
                        separator = (value.end_with? LINE_BREAK) ? EOL : ' '
         | 
| 2088 | 
            -
                        value = %(#{value}#{separator}#{next_line})
         | 
| 2089 | 
            -
                        break unless keep_open
         | 
| 2062 | 
            +
              def self.process_attribute_entry reader, document, attributes = nil, match = nil
         | 
| 2063 | 
            +
                if (match ||= (reader.has_more_lines? ? (AttributeEntryRx.match reader.peek_line) : nil))
         | 
| 2064 | 
            +
                  if (value = match[2]).nil_or_empty?
         | 
| 2065 | 
            +
                    value = ''
         | 
| 2066 | 
            +
                  elsif value.end_with? LINE_CONTINUATION, LINE_CONTINUATION_LEGACY
         | 
| 2067 | 
            +
                    con, value = value.slice(-2, 2), value.slice(0, value.length - 2).rstrip
         | 
| 2068 | 
            +
                    while reader.advance && !(next_line = reader.peek_line.lstrip).empty?
         | 
| 2069 | 
            +
                      if (keep_open = next_line.end_with? con)
         | 
| 2070 | 
            +
                        next_line = (next_line.slice 0, next_line.length - 2).rstrip
         | 
| 2090 2071 | 
             
                      end
         | 
| 2072 | 
            +
                      value = %(#{value}#{(value.end_with? HARD_LINE_BREAK) ? LF : ' '}#{next_line})
         | 
| 2073 | 
            +
                      break unless keep_open
         | 
| 2091 2074 | 
             
                    end
         | 
| 2092 2075 | 
             
                  end
         | 
| 2093 2076 |  | 
| 2094 | 
            -
                  store_attribute | 
| 2077 | 
            +
                  store_attribute match[1], value, document, attributes
         | 
| 2095 2078 | 
             
                  true
         | 
| 2096 | 
            -
                else
         | 
| 2097 | 
            -
                  false
         | 
| 2098 2079 | 
             
                end
         | 
| 2099 2080 | 
             
              end
         | 
| 2100 2081 |  | 
| 2101 2082 | 
             
              # Public: Store the attribute in the document and register attribute entry if accessible
         | 
| 2102 2083 | 
             
              #
         | 
| 2103 | 
            -
              # name  - the String name of the attribute to store
         | 
| 2084 | 
            +
              # name  - the String name of the attribute to store;
         | 
| 2085 | 
            +
              #         if name begins or ends with !, it signals to remove the attribute with that root name
         | 
| 2104 2086 | 
             
              # value - the String value of the attribute to store
         | 
| 2105 2087 | 
             
              # doc   - the Document being parsed
         | 
| 2106 2088 | 
             
              # attrs - the attributes for the current context
         | 
| 2107 2089 | 
             
              #
         | 
| 2108 | 
            -
              # returns a 2-element array containing the attribute name and value
         | 
| 2109 | 
            -
              def self.store_attribute | 
| 2090 | 
            +
              # returns a 2-element array containing the resolved attribute name (minus the ! indicator) and value
         | 
| 2091 | 
            +
              def self.store_attribute name, value, doc = nil, attrs = nil
         | 
| 2110 2092 | 
             
                # TODO move processing of attribute value to utility method
         | 
| 2111 | 
            -
                if name.end_with? | 
| 2112 | 
            -
                  # a nil value signals the attribute should be deleted ( | 
| 2113 | 
            -
                  value = nil
         | 
| 2114 | 
            -
             | 
| 2115 | 
            -
             | 
| 2116 | 
            -
                   | 
| 2117 | 
            -
             | 
| 2118 | 
            -
             | 
| 2119 | 
            -
                 | 
| 2120 | 
            -
             | 
| 2121 | 
            -
                name =  | 
| 2122 | 
            -
             | 
| 2093 | 
            +
                if name.end_with? '!'
         | 
| 2094 | 
            +
                  # a nil value signals the attribute should be deleted (unset)
         | 
| 2095 | 
            +
                  name, value = name.chop, nil
         | 
| 2096 | 
            +
                elsif name.start_with? '!'
         | 
| 2097 | 
            +
                  # a nil value signals the attribute should be deleted (unset)
         | 
| 2098 | 
            +
                  name, value = (name.slice 1, name.length), nil
         | 
| 2099 | 
            +
                end
         | 
| 2100 | 
            +
             | 
| 2101 | 
            +
                name = sanitize_attribute_name name
         | 
| 2102 | 
            +
                # alias numbered attribute to sectnums
         | 
| 2103 | 
            +
                name = 'sectnums' if name == 'numbered'
         | 
| 2104 | 
            +
             | 
| 2123 2105 | 
             
                if doc
         | 
| 2124 | 
            -
                   | 
| 2125 | 
            -
             | 
| 2126 | 
            -
             | 
| 2127 | 
            -
             | 
| 2128 | 
            -
                  elsif name == 'leveloffset'
         | 
| 2129 | 
            -
                    if value
         | 
| 2130 | 
            -
                      case value.chr
         | 
| 2131 | 
            -
                      when '+'
         | 
| 2106 | 
            +
                  if value
         | 
| 2107 | 
            +
                    if name == 'leveloffset'
         | 
| 2108 | 
            +
                      # support relative leveloffset values
         | 
| 2109 | 
            +
                      if value.start_with? '+'
         | 
| 2132 2110 | 
             
                        value = ((doc.attr 'leveloffset', 0).to_i + (value[1..-1] || 0).to_i).to_s
         | 
| 2133 | 
            -
                       | 
| 2111 | 
            +
                      elsif value.start_with? '-'
         | 
| 2134 2112 | 
             
                        value = ((doc.attr 'leveloffset', 0).to_i - (value[1..-1] || 0).to_i).to_s
         | 
| 2135 2113 | 
             
                      end
         | 
| 2136 2114 | 
             
                    end
         | 
| 2115 | 
            +
                    # QUESTION should we set value to locked value if set_attribute returns false?
         | 
| 2116 | 
            +
                    if (resolved_value = doc.set_attribute name, value)
         | 
| 2117 | 
            +
                      value = resolved_value
         | 
| 2118 | 
            +
                      (Document::AttributeEntry.new name, value).save_to attrs if attrs
         | 
| 2119 | 
            +
                    end
         | 
| 2120 | 
            +
                  elsif (doc.delete_attribute name) && attrs
         | 
| 2121 | 
            +
                    (Document::AttributeEntry.new name, value).save_to attrs
         | 
| 2137 2122 | 
             
                  end
         | 
| 2138 | 
            -
             | 
| 2139 | 
            -
             | 
| 2140 | 
            -
             | 
| 2141 | 
            -
                if accessible && attrs
         | 
| 2142 | 
            -
                  # NOTE lookup resolved value (resolution occurs inside set_attribute)
         | 
| 2143 | 
            -
                  value = doc.attributes[name] if value
         | 
| 2144 | 
            -
                  Document::AttributeEntry.new(name, value).save_to(attrs)
         | 
| 2123 | 
            +
                elsif attrs
         | 
| 2124 | 
            +
                  (Document::AttributeEntry.new name, value).save_to attrs
         | 
| 2145 2125 | 
             
                end
         | 
| 2146 2126 |  | 
| 2147 2127 | 
             
                [name, value]
         | 
| @@ -2164,8 +2144,8 @@ class Parser | |
| 2164 2144 | 
             
              #
         | 
| 2165 2145 | 
             
              # Returns the String 0-index marker for this list item
         | 
| 2166 2146 | 
             
              def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil)
         | 
| 2167 | 
            -
                if list_type == :olist | 
| 2168 | 
            -
                   | 
| 2147 | 
            +
                if list_type == :olist
         | 
| 2148 | 
            +
                  (marker.start_with? '.') ? marker : (resolve_ordered_list_marker marker, ordinal, validate, reader)
         | 
| 2169 2149 | 
             
                elsif list_type == :colist
         | 
| 2170 2150 | 
             
                  '<1>'
         | 
| 2171 2151 | 
             
                else
         | 
| @@ -2195,41 +2175,40 @@ class Parser | |
| 2195 2175 | 
             
              #
         | 
| 2196 2176 | 
             
              # Returns the String of the first marker in this number series
         | 
| 2197 2177 | 
             
              def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false, reader = nil)
         | 
| 2198 | 
            -
                number_style = ORDERED_LIST_STYLES.find {|s| OrderedListMarkerRxMap[s] =~ marker }
         | 
| 2199 2178 | 
             
                expected = actual = nil
         | 
| 2200 | 
            -
                case  | 
| 2201 | 
            -
             | 
| 2202 | 
            -
             | 
| 2203 | 
            -
             | 
| 2204 | 
            -
             | 
| 2205 | 
            -
             | 
| 2206 | 
            -
             | 
| 2207 | 
            -
             | 
| 2208 | 
            -
             | 
| 2209 | 
            -
             | 
| 2210 | 
            -
             | 
| 2211 | 
            -
             | 
| 2212 | 
            -
             | 
| 2213 | 
            -
             | 
| 2214 | 
            -
             | 
| 2215 | 
            -
             | 
| 2216 | 
            -
             | 
| 2217 | 
            -
             | 
| 2218 | 
            -
             | 
| 2219 | 
            -
             | 
| 2220 | 
            -
             | 
| 2221 | 
            -
             | 
| 2222 | 
            -
             | 
| 2223 | 
            -
             | 
| 2224 | 
            -
             | 
| 2225 | 
            -
             | 
| 2226 | 
            -
             | 
| 2227 | 
            -
             | 
| 2228 | 
            -
             | 
| 2229 | 
            -
             | 
| 2230 | 
            -
             | 
| 2231 | 
            -
             | 
| 2232 | 
            -
             | 
| 2179 | 
            +
                case ORDERED_LIST_STYLES.find {|s| OrderedListMarkerRxMap[s].match? marker }
         | 
| 2180 | 
            +
                when :arabic
         | 
| 2181 | 
            +
                  if validate
         | 
| 2182 | 
            +
                    expected = ordinal + 1
         | 
| 2183 | 
            +
                    actual = marker.to_i # remove trailing . and coerce to int
         | 
| 2184 | 
            +
                  end
         | 
| 2185 | 
            +
                  marker = '1.'
         | 
| 2186 | 
            +
                when :loweralpha
         | 
| 2187 | 
            +
                  if validate
         | 
| 2188 | 
            +
                    expected = ('a'[0].ord + ordinal).chr
         | 
| 2189 | 
            +
                    actual = marker.chop # remove trailing .
         | 
| 2190 | 
            +
                  end
         | 
| 2191 | 
            +
                  marker = 'a.'
         | 
| 2192 | 
            +
                when :upperalpha
         | 
| 2193 | 
            +
                  if validate
         | 
| 2194 | 
            +
                    expected = ('A'[0].ord + ordinal).chr
         | 
| 2195 | 
            +
                    actual = marker.chop # remove trailing .
         | 
| 2196 | 
            +
                  end
         | 
| 2197 | 
            +
                  marker = 'A.'
         | 
| 2198 | 
            +
                when :lowerroman
         | 
| 2199 | 
            +
                  if validate
         | 
| 2200 | 
            +
                    # TODO report this in roman numerals; see https://github.com/jamesshipton/roman-numeral/blob/master/lib/roman_numeral.rb
         | 
| 2201 | 
            +
                    expected = ordinal + 1
         | 
| 2202 | 
            +
                    actual = roman_numeral_to_int(marker.chop) # remove trailing ) and coerce to int
         | 
| 2203 | 
            +
                  end
         | 
| 2204 | 
            +
                  marker = 'i)'
         | 
| 2205 | 
            +
                when :upperroman
         | 
| 2206 | 
            +
                  if validate
         | 
| 2207 | 
            +
                    # TODO report this in roman numerals; see https://github.com/jamesshipton/roman-numeral/blob/master/lib/roman_numeral.rb
         | 
| 2208 | 
            +
                    expected = ordinal + 1
         | 
| 2209 | 
            +
                    actual = roman_numeral_to_int(marker.chop) # remove trailing ) and coerce to int
         | 
| 2210 | 
            +
                  end
         | 
| 2211 | 
            +
                  marker = 'I)'
         | 
| 2233 2212 | 
             
                end
         | 
| 2234 2213 |  | 
| 2235 2214 | 
             
                if validate && expected != actual
         | 
| @@ -2277,106 +2256,112 @@ class Parser | |
| 2277 2256 | 
             
              # returns an instance of Asciidoctor::Table parsed from the provided reader
         | 
| 2278 2257 | 
             
              def self.next_table(table_reader, parent, attributes)
         | 
| 2279 2258 | 
             
                table = Table.new(parent, attributes)
         | 
| 2280 | 
            -
                if  | 
| 2259 | 
            +
                if attributes.key? 'title'
         | 
| 2281 2260 | 
             
                  table.title = attributes.delete 'title'
         | 
| 2282 | 
            -
                  table.assign_caption | 
| 2261 | 
            +
                  table.assign_caption(attributes.delete 'caption')
         | 
| 2283 2262 | 
             
                end
         | 
| 2284 2263 |  | 
| 2285 2264 | 
             
                if (attributes.key? 'cols') && !(colspecs = parse_colspecs attributes['cols']).empty?
         | 
| 2286 2265 | 
             
                  table.create_columns colspecs
         | 
| 2287 2266 | 
             
                  explicit_colspecs = true
         | 
| 2288 | 
            -
                else
         | 
| 2289 | 
            -
                  explicit_colspecs = false
         | 
| 2290 2267 | 
             
                end
         | 
| 2291 2268 |  | 
| 2292 2269 | 
             
                skipped = table_reader.skip_blank_lines
         | 
| 2293 2270 |  | 
| 2294 | 
            -
                parser_ctx = Table::ParserContext.new | 
| 2295 | 
            -
                 | 
| 2296 | 
            -
                 | 
| 2297 | 
            -
                while table_reader. | 
| 2298 | 
            -
                  loop_idx += 1
         | 
| 2299 | 
            -
             | 
| 2300 | 
            -
             | 
| 2301 | 
            -
                   | 
| 2302 | 
            -
                      !(next_line = table_reader.peek_line).nil? && next_line.empty?
         | 
| 2303 | 
            -
                    table.has_header_option = true
         | 
| 2304 | 
            -
                    attributes['header-option'] = ''
         | 
| 2305 | 
            -
                    attributes['options'] = (attributes.key? 'options') ? %(#{attributes['options']},header) : 'header'
         | 
| 2306 | 
            -
                  end
         | 
| 2307 | 
            -
             | 
| 2308 | 
            -
                  if parser_ctx.format == 'psv'
         | 
| 2271 | 
            +
                parser_ctx = Table::ParserContext.new table_reader, table, attributes
         | 
| 2272 | 
            +
                format, loop_idx, implicit_header_boundary = parser_ctx.format, -1, nil
         | 
| 2273 | 
            +
                implicit_header = true unless skipped > 0 || (attributes.key? 'header-option') || (attributes.key? 'noheader-option')
         | 
| 2274 | 
            +
                while (line = table_reader.read_line)
         | 
| 2275 | 
            +
                  if (loop_idx += 1) > 0 && line.empty?
         | 
| 2276 | 
            +
                    line = nil
         | 
| 2277 | 
            +
                    implicit_header_boundary += 1 if implicit_header_boundary
         | 
| 2278 | 
            +
                  elsif format == 'psv'
         | 
| 2309 2279 | 
             
                    if parser_ctx.starts_with_delimiter? line
         | 
| 2310 | 
            -
                      line = line | 
| 2311 | 
            -
                      # push  | 
| 2280 | 
            +
                      line = line.slice 1, line.length
         | 
| 2281 | 
            +
                      # push empty cell spec if cell boundary appears at start of line
         | 
| 2312 2282 | 
             
                      parser_ctx.close_open_cell
         | 
| 2283 | 
            +
                      implicit_header_boundary = nil if implicit_header_boundary
         | 
| 2313 2284 | 
             
                    else
         | 
| 2314 | 
            -
                      next_cellspec, line = parse_cellspec | 
| 2315 | 
            -
                      # if  | 
| 2316 | 
            -
                      if  | 
| 2285 | 
            +
                      next_cellspec, line = parse_cellspec line, :start, parser_ctx.delimiter
         | 
| 2286 | 
            +
                      # if cellspec is not nil, we're at a cell boundary
         | 
| 2287 | 
            +
                      if next_cellspec
         | 
| 2317 2288 | 
             
                        parser_ctx.close_open_cell next_cellspec
         | 
| 2318 | 
            -
             | 
| 2319 | 
            -
             | 
| 2289 | 
            +
                        implicit_header_boundary = nil if implicit_header_boundary
         | 
| 2290 | 
            +
                      # otherwise, the cell continues from previous line
         | 
| 2291 | 
            +
                      elsif implicit_header_boundary && implicit_header_boundary == loop_idx
         | 
| 2292 | 
            +
                        implicit_header, implicit_header_boundary = false, nil
         | 
| 2320 2293 | 
             
                      end
         | 
| 2321 2294 | 
             
                    end
         | 
| 2322 2295 | 
             
                  end
         | 
| 2323 2296 |  | 
| 2324 | 
            -
                   | 
| 2325 | 
            -
                   | 
| 2326 | 
            -
                     | 
| 2327 | 
            -
             | 
| 2328 | 
            -
             | 
| 2329 | 
            -
             | 
| 2330 | 
            -
             | 
| 2331 | 
            -
             | 
| 2332 | 
            -
             | 
| 2297 | 
            +
                  # NOTE implicit header is offset by at least one blank line; implicit_header_boundary tracks size of gap
         | 
| 2298 | 
            +
                  if loop_idx == 0 && implicit_header
         | 
| 2299 | 
            +
                    if table_reader.has_more_lines? && table_reader.peek_line.empty?
         | 
| 2300 | 
            +
                      implicit_header_boundary = 1
         | 
| 2301 | 
            +
                    else
         | 
| 2302 | 
            +
                      implicit_header = false
         | 
| 2303 | 
            +
                    end
         | 
| 2304 | 
            +
                  end
         | 
| 2305 | 
            +
             | 
| 2306 | 
            +
                  # this loop is used for flow control; internal logic controls how many times it executes
         | 
| 2307 | 
            +
                  while true
         | 
| 2308 | 
            +
                    if line && (m = parser_ctx.match_delimiter line)
         | 
| 2309 | 
            +
                      case format
         | 
| 2310 | 
            +
                      when 'csv'
         | 
| 2311 | 
            +
                        if parser_ctx.buffer_has_unclosed_quotes? m.pre_match
         | 
| 2312 | 
            +
                          break if (line = parser_ctx.skip_past_delimiter m).empty?
         | 
| 2313 | 
            +
                          redo
         | 
| 2333 2314 | 
             
                        end
         | 
| 2334 | 
            -
             | 
| 2315 | 
            +
                        parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match})
         | 
| 2316 | 
            +
                      when 'dsv'
         | 
| 2335 2317 | 
             
                        if m.pre_match.end_with? '\\'
         | 
| 2336 | 
            -
                           | 
| 2337 | 
            -
             | 
| 2338 | 
            -
                          if (line = parser_ctx.skip_matched_delimiter(m, true)).empty?
         | 
| 2339 | 
            -
                            parser_ctx.buffer = %(#{parser_ctx.buffer}#{EOL})
         | 
| 2318 | 
            +
                          if (line = parser_ctx.skip_past_escaped_delimiter m).empty?
         | 
| 2319 | 
            +
                            parser_ctx.buffer = %(#{parser_ctx.buffer}#{LF})
         | 
| 2340 2320 | 
             
                            parser_ctx.keep_cell_open
         | 
| 2341 2321 | 
             
                            break
         | 
| 2342 2322 | 
             
                          end
         | 
| 2343 | 
            -
                           | 
| 2323 | 
            +
                          redo
         | 
| 2344 2324 | 
             
                        end
         | 
| 2345 | 
            -
             | 
| 2346 | 
            -
             | 
| 2347 | 
            -
             | 
| 2348 | 
            -
             | 
| 2325 | 
            +
                        parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match})
         | 
| 2326 | 
            +
                      else # psv
         | 
| 2327 | 
            +
                        if m.pre_match.end_with? '\\'
         | 
| 2328 | 
            +
                          if (line = parser_ctx.skip_past_escaped_delimiter m).empty?
         | 
| 2329 | 
            +
                            parser_ctx.buffer = %(#{parser_ctx.buffer}#{LF})
         | 
| 2330 | 
            +
                            parser_ctx.keep_cell_open
         | 
| 2331 | 
            +
                            break
         | 
| 2332 | 
            +
                          end
         | 
| 2333 | 
            +
                          redo
         | 
| 2334 | 
            +
                        end
         | 
| 2335 | 
            +
                        next_cellspec, cell_text = parse_cellspec m.pre_match
         | 
| 2349 2336 | 
             
                        parser_ctx.push_cellspec next_cellspec
         | 
| 2350 2337 | 
             
                        parser_ctx.buffer = %(#{parser_ctx.buffer}#{cell_text})
         | 
| 2351 | 
            -
                      else
         | 
| 2352 | 
            -
                        parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match})
         | 
| 2353 | 
            -
                      end
         | 
| 2354 | 
            -
             | 
| 2355 | 
            -
                      if (line = m.post_match).empty?
         | 
| 2356 | 
            -
                        # hack to prevent dropping empty cell found at end of line (see issue #1106)
         | 
| 2357 | 
            -
                        seen = false
         | 
| 2358 2338 | 
             
                      end
         | 
| 2359 | 
            -
             | 
| 2339 | 
            +
                      # don't break if empty to preserve empty cell found at end of line (see issue #1106)
         | 
| 2340 | 
            +
                      line = nil if (line = m.post_match).empty?
         | 
| 2360 2341 | 
             
                      parser_ctx.close_cell
         | 
| 2361 2342 | 
             
                    else
         | 
| 2362 | 
            -
                      # no other delimiters to see here
         | 
| 2363 | 
            -
                       | 
| 2364 | 
            -
                       | 
| 2365 | 
            -
                       | 
| 2366 | 
            -
             | 
| 2343 | 
            +
                      # no other delimiters to see here; suck up this line into the buffer and move on
         | 
| 2344 | 
            +
                      parser_ctx.buffer = %(#{parser_ctx.buffer}#{line}#{LF})
         | 
| 2345 | 
            +
                      case format
         | 
| 2346 | 
            +
                      when 'csv'
         | 
| 2347 | 
            +
                        # QUESTION make stripping endlines in csv data an option? (unwrap-option?)
         | 
| 2367 2348 | 
             
                        parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} )
         | 
| 2368 | 
            -
             | 
| 2369 | 
            -
             | 
| 2370 | 
            -
             | 
| 2371 | 
            -
             | 
| 2372 | 
            -
             | 
| 2373 | 
            -
             | 
| 2349 | 
            +
                        if parser_ctx.buffer_has_unclosed_quotes?
         | 
| 2350 | 
            +
                          implicit_header, implicit_header_boundary = false, nil if implicit_header_boundary && loop_idx == 0
         | 
| 2351 | 
            +
                          parser_ctx.keep_cell_open
         | 
| 2352 | 
            +
                        else
         | 
| 2353 | 
            +
                          parser_ctx.close_cell true
         | 
| 2354 | 
            +
                        end
         | 
| 2355 | 
            +
                      when 'dsv'
         | 
| 2374 2356 | 
             
                        parser_ctx.close_cell true
         | 
| 2357 | 
            +
                      else # psv
         | 
| 2358 | 
            +
                        parser_ctx.keep_cell_open
         | 
| 2375 2359 | 
             
                      end
         | 
| 2360 | 
            +
                      break
         | 
| 2376 2361 | 
             
                    end
         | 
| 2377 2362 | 
             
                  end
         | 
| 2378 2363 |  | 
| 2379 | 
            -
                   | 
| 2364 | 
            +
                  table_reader.skip_blank_lines unless parser_ctx.cell_open?
         | 
| 2380 2365 |  | 
| 2381 2366 | 
             
                  unless table_reader.has_more_lines?
         | 
| 2382 2367 | 
             
                    # NOTE may have already closed cell in csv or dsv table (see previous call to parser_ctx.close_cell(true))
         | 
| @@ -2388,6 +2373,12 @@ class Parser | |
| 2388 2373 | 
             
                  table.assign_column_widths
         | 
| 2389 2374 | 
             
                end
         | 
| 2390 2375 |  | 
| 2376 | 
            +
                if implicit_header
         | 
| 2377 | 
            +
                  table.has_header_option = true
         | 
| 2378 | 
            +
                  attributes['header-option'] = ''
         | 
| 2379 | 
            +
                  attributes['options'] = (attributes.key? 'options') ? %(#{attributes['options']},header) : 'header'
         | 
| 2380 | 
            +
                end
         | 
| 2381 | 
            +
             | 
| 2391 2382 | 
             
                table.partition_header_footer attributes
         | 
| 2392 2383 |  | 
| 2393 2384 | 
             
                table
         | 
| @@ -2405,7 +2396,7 @@ class Parser | |
| 2405 2396 | 
             
              # returns a Hash of attributes that specify how to format
         | 
| 2406 2397 | 
             
              # and layout the cells in the table.
         | 
| 2407 2398 | 
             
              def self.parse_colspecs records
         | 
| 2408 | 
            -
                records = records. | 
| 2399 | 
            +
                records = records.delete ' ' if records.include? ' '
         | 
| 2409 2400 | 
             
                # check for deprecated syntax: single number, equal column spread
         | 
| 2410 2401 | 
             
                if records == records.to_i.to_s
         | 
| 2411 2402 | 
             
                  return ::Array.new(records.to_i) { { 'width' => 1 } }
         | 
| @@ -2422,11 +2413,11 @@ class Parser | |
| 2422 2413 | 
             
                    if m[2]
         | 
| 2423 2414 | 
             
                      # make this an operation
         | 
| 2424 2415 | 
             
                      colspec, rowspec = m[2].split '.'
         | 
| 2425 | 
            -
                      if !colspec.nil_or_empty? &&  | 
| 2426 | 
            -
                        spec['halign'] =  | 
| 2416 | 
            +
                      if !colspec.nil_or_empty? && TableCellHorzAlignments.key?(colspec)
         | 
| 2417 | 
            +
                        spec['halign'] = TableCellHorzAlignments[colspec]
         | 
| 2427 2418 | 
             
                      end
         | 
| 2428 | 
            -
                      if !rowspec.nil_or_empty? &&  | 
| 2429 | 
            -
                        spec['valign'] =  | 
| 2419 | 
            +
                      if !rowspec.nil_or_empty? && TableCellVertAlignments.key?(rowspec)
         | 
| 2420 | 
            +
                        spec['valign'] = TableCellVertAlignments[rowspec]
         | 
| 2430 2421 | 
             
                      end
         | 
| 2431 2422 | 
             
                    end
         | 
| 2432 2423 |  | 
| @@ -2435,8 +2426,8 @@ class Parser | |
| 2435 2426 | 
             
                    spec['width'] = (m[3] ? m[3].to_i : 1)
         | 
| 2436 2427 |  | 
| 2437 2428 | 
             
                    # make this an operation
         | 
| 2438 | 
            -
                    if m[4] &&  | 
| 2439 | 
            -
                      spec['style'] =  | 
| 2429 | 
            +
                    if m[4] && TableCellStyles.key?(m[4])
         | 
| 2430 | 
            +
                      spec['style'] = TableCellStyles[m[4]]
         | 
| 2440 2431 | 
             
                    end
         | 
| 2441 2432 |  | 
| 2442 2433 | 
             
                    if m[1]
         | 
| @@ -2462,12 +2453,10 @@ class Parser | |
| 2462 2453 | 
             
              #
         | 
| 2463 2454 | 
             
              # returns the Hash of attributes that indicate how to layout
         | 
| 2464 2455 | 
             
              # and style this cell in the table.
         | 
| 2465 | 
            -
              def self.parse_cellspec(line, pos = : | 
| 2466 | 
            -
                m = nil
         | 
| 2467 | 
            -
                rest = ''
         | 
| 2456 | 
            +
              def self.parse_cellspec(line, pos = :end, delimiter = nil)
         | 
| 2457 | 
            +
                m, rest = nil, ''
         | 
| 2468 2458 |  | 
| 2469 | 
            -
                 | 
| 2470 | 
            -
                when :start
         | 
| 2459 | 
            +
                if pos == :start
         | 
| 2471 2460 | 
             
                  if line.include? delimiter
         | 
| 2472 2461 | 
             
                    spec_part, rest = line.split delimiter, 2
         | 
| 2473 2462 | 
             
                    if (m = CellSpecStartRx.match spec_part)
         | 
| @@ -2478,7 +2467,7 @@ class Parser | |
| 2478 2467 | 
             
                  else
         | 
| 2479 2468 | 
             
                    return [nil, line]
         | 
| 2480 2469 | 
             
                  end
         | 
| 2481 | 
            -
                 | 
| 2470 | 
            +
                else # pos == :end
         | 
| 2482 2471 | 
             
                  if (m = CellSpecEndRx.match line)
         | 
| 2483 2472 | 
             
                    # NOTE return the line stripped of trailing whitespace if no cellspec is found in this case
         | 
| 2484 2473 | 
             
                    return [{}, line.rstrip] if m[0].lstrip.empty?
         | 
| @@ -2503,16 +2492,16 @@ class Parser | |
| 2503 2492 |  | 
| 2504 2493 | 
             
                if m[3]
         | 
| 2505 2494 | 
             
                  colspec, rowspec = m[3].split '.'
         | 
| 2506 | 
            -
                  if !colspec.nil_or_empty? &&  | 
| 2507 | 
            -
                    spec['halign'] =  | 
| 2495 | 
            +
                  if !colspec.nil_or_empty? && TableCellHorzAlignments.key?(colspec)
         | 
| 2496 | 
            +
                    spec['halign'] = TableCellHorzAlignments[colspec]
         | 
| 2508 2497 | 
             
                  end
         | 
| 2509 | 
            -
                  if !rowspec.nil_or_empty? &&  | 
| 2510 | 
            -
                    spec['valign'] =  | 
| 2498 | 
            +
                  if !rowspec.nil_or_empty? && TableCellVertAlignments.key?(rowspec)
         | 
| 2499 | 
            +
                    spec['valign'] = TableCellVertAlignments[rowspec]
         | 
| 2511 2500 | 
             
                  end
         | 
| 2512 2501 | 
             
                end
         | 
| 2513 2502 |  | 
| 2514 | 
            -
                if m[4] &&  | 
| 2515 | 
            -
                  spec['style'] =  | 
| 2503 | 
            +
                if m[4] && TableCellStyles.key?(m[4])
         | 
| 2504 | 
            +
                  spec['style'] = TableCellStyles[m[4]]
         | 
| 2516 2505 | 
             
                end
         | 
| 2517 2506 |  | 
| 2518 2507 | 
             
                [spec, rest]
         | 
| @@ -2522,48 +2511,40 @@ class Parser | |
| 2522 2511 | 
             
              #
         | 
| 2523 2512 | 
             
              # Parse the first positional attribute to extract the style, role and id
         | 
| 2524 2513 | 
             
              # parts, assign the values to their cooresponding attribute keys and return
         | 
| 2525 | 
            -
              #  | 
| 2526 | 
            -
              # positional attribute.
         | 
| 2514 | 
            +
              # the parsed style from the first positional attribute.
         | 
| 2527 2515 | 
             
              #
         | 
| 2528 2516 | 
             
              # attributes - The Hash of attributes to process and update
         | 
| 2529 2517 | 
             
              #
         | 
| 2530 2518 | 
             
              # Examples
         | 
| 2531 2519 | 
             
              #
         | 
| 2532 2520 | 
             
              #   puts attributes
         | 
| 2533 | 
            -
              #   => {1 => "abstract#intro.lead%fragment", "style" => "preamble"}
         | 
| 2521 | 
            +
              #   => { 1 => "abstract#intro.lead%fragment", "style" => "preamble" }
         | 
| 2534 2522 | 
             
              #
         | 
| 2535 2523 | 
             
              #   parse_style_attribute(attributes)
         | 
| 2536 | 
            -
              #   =>  | 
| 2524 | 
            +
              #   => "abstract"
         | 
| 2537 2525 | 
             
              #
         | 
| 2538 2526 | 
             
              #   puts attributes
         | 
| 2539 | 
            -
              #   => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro",
         | 
| 2540 | 
            -
              #         "role" => "lead", "options" =>  | 
| 2527 | 
            +
              #   => { 1 => "abstract#intro.lead%fragment", "style" => "abstract", "id" => "intro",
         | 
| 2528 | 
            +
              #         "role" => "lead", "options" => "fragment", "fragment-option" => '' }
         | 
| 2541 2529 | 
             
              #
         | 
| 2542 | 
            -
              # Returns  | 
| 2543 | 
            -
              # first positional attribute and the original style that was
         | 
| 2544 | 
            -
              # replaced
         | 
| 2530 | 
            +
              # Returns the String style parsed from the first positional attribute
         | 
| 2545 2531 | 
             
              def self.parse_style_attribute(attributes, reader = nil)
         | 
| 2546 | 
            -
                 | 
| 2547 | 
            -
                raw_style = attributes[1]
         | 
| 2548 | 
            -
             | 
| 2549 | 
            -
                if raw_style && !raw_style.include?(' ') && Compliance.shorthand_property_syntax
         | 
| 2550 | 
            -
                  type = :style
         | 
| 2551 | 
            -
                  collector = []
         | 
| 2552 | 
            -
                  parsed = {}
         | 
| 2532 | 
            +
                # NOTE spaces are not allowed in shorthand, so if we detect one, this ain't no shorthand
         | 
| 2533 | 
            +
                if (raw_style = attributes[1]) && !raw_style.include?(' ') && Compliance.shorthand_property_syntax
         | 
| 2534 | 
            +
                  type, collector, parsed = :style, [], {}
         | 
| 2553 2535 | 
             
                  # QUESTION should this be a private method? (though, it's never called if shorthand isn't used)
         | 
| 2554 2536 | 
             
                  save_current = lambda {
         | 
| 2555 2537 | 
             
                    if collector.empty?
         | 
| 2556 | 
            -
                       | 
| 2557 | 
            -
                        warn %(asciidoctor: WARNING:#{reader | 
| 2538 | 
            +
                      unless type == :style
         | 
| 2539 | 
            +
                        warn %(asciidoctor: WARNING:#{reader ? " #{reader.prev_line_info}:" : nil} invalid empty #{type} detected in style attribute)
         | 
| 2558 2540 | 
             
                      end
         | 
| 2559 2541 | 
             
                    else
         | 
| 2560 2542 | 
             
                      case type
         | 
| 2561 2543 | 
             
                      when :role, :option
         | 
| 2562 | 
            -
                        parsed[type] ||= []
         | 
| 2563 | 
            -
                        parsed[type].push collector.join
         | 
| 2544 | 
            +
                        (parsed[type] ||= []) << collector.join
         | 
| 2564 2545 | 
             
                      when :id
         | 
| 2565 | 
            -
                        if parsed. | 
| 2566 | 
            -
                          warn %(asciidoctor: WARNING:#{reader | 
| 2546 | 
            +
                        if parsed.key? :id
         | 
| 2547 | 
            +
                          warn %(asciidoctor: WARNING:#{reader ? " #{reader.prev_line_info}:" : nil} multiple ids detected in style attribute)
         | 
| 2567 2548 | 
             
                        end
         | 
| 2568 2549 | 
             
                        parsed[type] = collector.join
         | 
| 2569 2550 | 
             
                      else
         | 
| @@ -2585,46 +2566,35 @@ class Parser | |
| 2585 2566 | 
             
                        type = :option
         | 
| 2586 2567 | 
             
                      end
         | 
| 2587 2568 | 
             
                    else
         | 
| 2588 | 
            -
                      collector | 
| 2569 | 
            +
                      collector << c
         | 
| 2589 2570 | 
             
                    end
         | 
| 2590 2571 | 
             
                  end
         | 
| 2591 2572 |  | 
| 2592 2573 | 
             
                  # small optimization if no shorthand is found
         | 
| 2593 2574 | 
             
                  if type == :style
         | 
| 2594 | 
            -
                     | 
| 2575 | 
            +
                    attributes['style'] = raw_style
         | 
| 2595 2576 | 
             
                  else
         | 
| 2596 2577 | 
             
                    save_current.call
         | 
| 2597 2578 |  | 
| 2598 | 
            -
                    if parsed. | 
| 2599 | 
            -
                      parsed_style = attributes['style'] = parsed[:style]
         | 
| 2600 | 
            -
                    else
         | 
| 2601 | 
            -
                      parsed_style = nil
         | 
| 2602 | 
            -
                    end
         | 
| 2579 | 
            +
                    parsed_style = attributes['style'] = parsed[:style] if parsed.key? :style
         | 
| 2603 2580 |  | 
| 2604 | 
            -
                    if parsed. | 
| 2605 | 
            -
                      attributes['id'] = parsed[:id]
         | 
| 2606 | 
            -
                    end
         | 
| 2581 | 
            +
                    attributes['id'] = parsed[:id] if parsed.key? :id
         | 
| 2607 2582 |  | 
| 2608 | 
            -
                    if parsed. | 
| 2609 | 
            -
                      attributes['role'] = parsed[:role] * ' '
         | 
| 2610 | 
            -
                    end
         | 
| 2583 | 
            +
                    attributes['role'] = parsed[:role] * ' ' if parsed.key? :role
         | 
| 2611 2584 |  | 
| 2612 | 
            -
                    if parsed. | 
| 2613 | 
            -
                      (options = parsed[:option]).each  | 
| 2614 | 
            -
                        attributes[%(#{option}-option)] = ''
         | 
| 2615 | 
            -
                      end
         | 
| 2585 | 
            +
                    if parsed.key? :option
         | 
| 2586 | 
            +
                      (options = parsed[:option]).each {|option| attributes[%(#{option}-option)] = '' }
         | 
| 2616 2587 | 
             
                      if (existing_opts = attributes['options'])
         | 
| 2617 2588 | 
             
                        attributes['options'] = (options + existing_opts.split(',')) * ','
         | 
| 2618 2589 | 
             
                      else
         | 
| 2619 2590 | 
             
                        attributes['options'] = options * ','
         | 
| 2620 2591 | 
             
                      end
         | 
| 2621 2592 | 
             
                    end
         | 
| 2622 | 
            -
                  end
         | 
| 2623 2593 |  | 
| 2624 | 
            -
             | 
| 2594 | 
            +
                    parsed_style
         | 
| 2595 | 
            +
                  end
         | 
| 2625 2596 | 
             
                else
         | 
| 2626 2597 | 
             
                  attributes['style'] = raw_style
         | 
| 2627 | 
            -
                  [raw_style, original_style]
         | 
| 2628 2598 | 
             
                end
         | 
| 2629 2599 | 
             
              end
         | 
| 2630 2600 |  | 
| @@ -2644,16 +2614,16 @@ class Parser | |
| 2644 2614 | 
             
              #
         | 
| 2645 2615 | 
             
              #   source = <<EOS
         | 
| 2646 2616 | 
             
              #       def names
         | 
| 2647 | 
            -
              #         @name.split | 
| 2617 | 
            +
              #         @name.split
         | 
| 2648 2618 | 
             
              #       end
         | 
| 2649 2619 | 
             
              #   EOS
         | 
| 2650 2620 | 
             
              #
         | 
| 2651 2621 | 
             
              #   source.split "\n"
         | 
| 2652 | 
            -
              #   # => ["    def names", "      @names.split | 
| 2622 | 
            +
              #   # => ["    def names", "      @names.split", "    end"]
         | 
| 2653 2623 | 
             
              #
         | 
| 2654 2624 | 
             
              #   puts Parser.adjust_indentation!(source.split "\n") * "\n"
         | 
| 2655 2625 | 
             
              #   # => def names
         | 
| 2656 | 
            -
              #   # =>   @names.split | 
| 2626 | 
            +
              #   # =>   @names.split
         | 
| 2657 2627 | 
             
              #   # => end
         | 
| 2658 2628 | 
             
              #
         | 
| 2659 2629 | 
             
              # returns Nothing
         | 
| @@ -2670,7 +2640,7 @@ class Parser | |
| 2670 2640 | 
             
                    next line if line.empty?
         | 
| 2671 2641 |  | 
| 2672 2642 | 
             
                    # NOTE Opal has to patch this use of sub!
         | 
| 2673 | 
            -
                    line.sub!(TabIndentRx) { | 
| 2643 | 
            +
                    line.sub!(TabIndentRx) { full_tab_space * $&.length } if line.start_with? TAB
         | 
| 2674 2644 |  | 
| 2675 2645 | 
             
                    if line.include? TAB
         | 
| 2676 2646 | 
             
                      # keeps track of how many spaces were added to adjust offset in match data
         |