asciidoctor 1.5.5 → 1.5.6

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of asciidoctor might be problematic. Click here for more details.

Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +216 -1
  3. data/CONTRIBUTING.adoc +2 -2
  4. data/Gemfile +20 -1
  5. data/LICENSE.adoc +1 -1
  6. data/README-fr.adoc +4 -3
  7. data/README-jp.adoc +11 -10
  8. data/README-zh_CN.adoc +4 -3
  9. data/README.adoc +17 -202
  10. data/Rakefile +41 -25
  11. data/asciidoctor.gemspec +9 -10
  12. data/data/locale/attributes.adoc +216 -34
  13. data/data/stylesheets/asciidoctor-default.css +23 -16
  14. data/features/step_definitions.rb +15 -19
  15. data/features/xref.feature +584 -20
  16. data/lib/asciidoctor.rb +292 -278
  17. data/lib/asciidoctor/abstract_block.rb +155 -94
  18. data/lib/asciidoctor/abstract_node.rb +108 -94
  19. data/lib/asciidoctor/attribute_list.rb +30 -22
  20. data/lib/asciidoctor/block.rb +7 -7
  21. data/lib/asciidoctor/cli/invoker.rb +47 -34
  22. data/lib/asciidoctor/cli/options.rb +22 -11
  23. data/lib/asciidoctor/converter.rb +3 -3
  24. data/lib/asciidoctor/converter/base.rb +2 -2
  25. data/lib/asciidoctor/converter/composite.rb +1 -1
  26. data/lib/asciidoctor/converter/docbook45.rb +2 -2
  27. data/lib/asciidoctor/converter/docbook5.rb +132 -87
  28. data/lib/asciidoctor/converter/factory.rb +0 -1
  29. data/lib/asciidoctor/converter/html5.rb +116 -98
  30. data/lib/asciidoctor/converter/manpage.rb +51 -52
  31. data/lib/asciidoctor/converter/template.rb +47 -36
  32. data/lib/asciidoctor/core_ext.rb +8 -2
  33. data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +4 -0
  34. data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +6 -0
  35. data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +5 -0
  36. data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +1 -1
  37. data/lib/asciidoctor/core_ext/1.8.7/string/{limit.rb → limit_bytesize.rb} +7 -6
  38. data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +6 -0
  39. data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +1 -1
  40. data/lib/asciidoctor/core_ext/nil_or_empty.rb +5 -5
  41. data/lib/asciidoctor/core_ext/regexp/is_match.rb +3 -0
  42. data/lib/asciidoctor/core_ext/string/{limit.rb → limit_bytesize.rb} +2 -2
  43. data/lib/asciidoctor/document.rb +216 -213
  44. data/lib/asciidoctor/extensions.rb +318 -185
  45. data/lib/asciidoctor/helpers.rb +35 -35
  46. data/lib/asciidoctor/inline.rb +32 -1
  47. data/lib/asciidoctor/list.rb +22 -6
  48. data/lib/asciidoctor/parser.rb +1008 -1038
  49. data/lib/asciidoctor/path_resolver.rb +46 -50
  50. data/lib/asciidoctor/reader.rb +275 -251
  51. data/lib/asciidoctor/section.rb +86 -58
  52. data/lib/asciidoctor/stylesheets.rb +6 -6
  53. data/lib/asciidoctor/substitutors.rb +567 -649
  54. data/lib/asciidoctor/table.rb +163 -108
  55. data/lib/asciidoctor/version.rb +1 -1
  56. data/man/asciidoctor.1 +18 -16
  57. data/man/asciidoctor.adoc +15 -13
  58. data/test/attributes_test.rb +138 -22
  59. data/test/blocks_test.rb +377 -97
  60. data/test/converter_test.rb +13 -0
  61. data/test/document_test.rb +244 -34
  62. data/test/extensions_test.rb +409 -42
  63. data/test/fixtures/asciidoc_index.txt +521 -0
  64. data/test/fixtures/basic-docinfo-footer.html +6 -0
  65. data/test/fixtures/basic-docinfo-footer.xml +8 -0
  66. data/test/fixtures/basic-docinfo.html +1 -0
  67. data/test/fixtures/basic-docinfo.xml +4 -0
  68. data/test/fixtures/basic.asciidoc +5 -0
  69. data/test/fixtures/chapter-a.adoc +3 -0
  70. data/test/fixtures/child-include.adoc +5 -0
  71. data/test/fixtures/circle.svg +9 -0
  72. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
  73. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
  74. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
  75. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
  76. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
  77. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
  78. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
  79. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
  80. data/test/fixtures/custom-docinfodir/basic-docinfo.html +1 -0
  81. data/test/fixtures/custom-docinfodir/docinfo.html +1 -0
  82. data/test/fixtures/docinfo-footer.html +1 -0
  83. data/test/fixtures/docinfo-footer.xml +9 -0
  84. data/test/fixtures/docinfo.html +1 -0
  85. data/test/fixtures/docinfo.xml +3 -0
  86. data/test/fixtures/dot.gif +0 -0
  87. data/test/fixtures/encoding.asciidoc +13 -0
  88. data/test/fixtures/grandchild-include.adoc +3 -0
  89. data/test/fixtures/hello-asciidoctor.pdf +69 -0
  90. data/test/fixtures/include-file.asciidoc +24 -0
  91. data/test/fixtures/include-file.ml +3 -0
  92. data/test/fixtures/include-file.xml +5 -0
  93. data/test/fixtures/master.adoc +5 -0
  94. data/test/fixtures/mismatched-end-tag.adoc +7 -0
  95. data/test/fixtures/parent-include-restricted.adoc +5 -0
  96. data/test/fixtures/parent-include.adoc +5 -0
  97. data/test/fixtures/sample.asciidoc +26 -0
  98. data/test/fixtures/stylesheets/custom.css +3 -0
  99. data/test/fixtures/subs-docinfo.html +2 -0
  100. data/test/fixtures/subs.adoc +7 -0
  101. data/test/fixtures/tagged-class-enclosed.rb +26 -0
  102. data/test/fixtures/tagged-class.rb +23 -0
  103. data/test/fixtures/tip.gif +0 -0
  104. data/test/invoker_test.rb +82 -4
  105. data/test/links_test.rb +312 -37
  106. data/test/lists_test.rb +204 -25
  107. data/test/manpage_test.rb +191 -4
  108. data/test/options_test.rb +18 -1
  109. data/test/paragraphs_test.rb +32 -7
  110. data/test/parser_test.rb +150 -30
  111. data/test/paths_test.rb +47 -13
  112. data/test/preamble_test.rb +1 -1
  113. data/test/reader_test.rb +366 -126
  114. data/test/sections_test.rb +203 -56
  115. data/test/substitutions_test.rb +339 -131
  116. data/test/tables_test.rb +315 -15
  117. data/test/test_helper.rb +400 -0
  118. data/test/text_test.rb +5 -5
  119. metadata +110 -22
@@ -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#fail if on_failure is :abort or Kernel#warn if
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#fail is called with an appropriate message if on_failure is :abort.
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
- fail %(asciidoctor: FAILED: required gem '#{gem_name}' is not installed. Processing aborted.)
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
- fail %(asciidoctor: FAILED: #{e.message.chomp '.'}. Processing aborted.)
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 [] if data.empty?
65
+ return data if data.empty?
66
66
 
67
- # NOTE if data encoding is UTF-*, we only need 0..1
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[0..1]) == BOM_BYTES_UTF_16LE
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).lines.map {|line| line.rstrip }
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| "#{((line.force_encoding ::Encoding::UTF_16BE).encode utf8).rstrip}" }
77
- elsif leading_bytes[0..2] == BOM_BYTES_UTF_8
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
- # NOTE if data encoding is UTF-*, we only need 0..1
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[0..2] == BOM_BYTES_UTF_8
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 data[0..2].bytes.to_a == BOM_BYTES_UTF_8
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 =~ UriSniffRx
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? ':') && str =~ UriSniffRx ? $& : nil
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 string for inclusion in a URI
152
+ # Public: Encode a String for inclusion in a URI.
155
153
  #
156
- # str - the string to encode
154
+ # str - the String to URI encode
157
155
  #
158
- # returns an encoded version of the str
159
- def self.encode_uri(str)
160
- str.gsub(REGEXP_ENCODE_URI_CHARS) do
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
- # file_name - The String file name to process
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(file_name)
176
- (ext = ::File.extname(file_name)).empty? ? file_name : file_name[0...-ext.length]
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
- # file_name - The String file name to process
182
- # drop_extname - A Boolean flag indicating whether to drop the extension (default: false)
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(file_name, drop_extname = false)
191
- if drop_extname
192
- ::File.basename file_name, (::File.extname file_name)
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 file_name
194
+ ::File.basename filename
195
195
  end
196
196
  end
197
197
 
@@ -39,6 +39,37 @@ class Inline < AbstractNode
39
39
  end
40
40
 
41
41
  # Alias render to convert to maintain backwards compatibility
42
- alias :render :convert
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
@@ -4,11 +4,11 @@ module Asciidoctor
4
4
  class List < AbstractBlock
5
5
 
6
6
  # Public: Create alias for blocks
7
- alias :items :blocks
7
+ alias items blocks
8
8
  # Public: Get the items in this list as an Array
9
- alias :content :blocks
9
+ alias content blocks
10
10
  # Public: Create alias to check if this list has blocks
11
- alias :items? :blocks?
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 :render :convert
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 :list :parent
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).
@@ -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 =~ l) || (is_delimited_block? l) }
35
+ StartOfBlockProc = lambda {|l| ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (is_delimited_block? l) }
36
36
 
37
- StartOfListProc = lambda {|l| AnyListRx =~ l }
37
+ StartOfListProc = lambda {|l| AnyListRx.match? l }
38
38
 
39
- StartOfBlockOrListProc = lambda {|l| (is_delimited_block? l) || ((l.start_with? '[') && BlockAttributeLineRx =~ l) || AnyListRx =~ l }
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 lexer instances will be running around.'
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
- unless options[:header_only]
66
- while reader.has_more_lines?
67
- new_section, block_attributes = next_section(reader, document, block_attributes)
68
- document << new_section if new_section
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(reader, document)
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 (has_doctitle_line = is_next_line_document_title?(reader, block_attributes)) &&
94
- block_attributes.has_key?('title')
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 has_doctitle_line
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.set_attribute 'compat-mode', '' unless single_line
141
+ document.set_attr 'compat-mode' unless single_line || (document.attribute_locked? 'compat-mode')
115
142
  if (separator = block_attributes.delete 'separator')
116
- document.set_attribute 'title-separator', separator
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 (m = ManpageTitleVolnumRx.match(document.attributes['doctitle']))
155
- document.attributes['mantitle'] = document.sub_attributes(m[1].rstrip.downcase)
156
- document.attributes['manvolnum'] = m[2].strip
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).join(' ').tr_s(' ', ' ')
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).first.title
247
+ # Parser.next_section(reader, doc)[0].title
221
248
  # # => "Greetings"
222
249
  #
223
- # Parser.next_section(reader, doc).first.title
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(reader, parent, attributes = {})
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
- ((has_header = parent.has_header?) || attributes.delete('invalid-header') || !is_next_line_section?(reader, attributes))
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(parent, :preamble, :content_model => :compound)
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.has_key? 'fragment'
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(reader, parent, attributes)
261
- # clear attributes, except for title which carries over
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
- current_level = section.level
265
- if current_level == 0 && doctype == 'book'
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(reader, section, attributes)
302
+ parse_block_metadata_lines reader, document, attributes
291
303
 
292
- if (next_level = is_next_line_section? reader, attributes)
293
- next_level += section.document.attr('leveloffset', 0).to_i
294
- if next_level > current_level || (section.context == :document && next_level == 0)
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(reader, section, attributes)
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, (intro || section), attributes, :parse_metadata => false)
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: Return the next Section or Block object from the Reader.
407
+ # Public: Parse and return the next Block at the Reader's current location
399
408
  #
400
- # Begins by skipping over blank lines to find the start of the next Section
401
- # or Block. Processes each line of the reader in sequence until a Section or
402
- # Block is found or the reader has no more lines.
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
- # Uses regular expressions from the Asciidoctor module to match Section
405
- # and Block delimiters. The ensuing lines are then processed according
406
- # to the type of content.
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 - The Reader from which to retrieve the next block
409
- # parent - The Document, Section or Block to which the next block belongs
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 Section or Block object holding the parsed content of the processed lines
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(:text)
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
- block_macro_extensions = extensions.block_macros?
437
- else
438
- block_extensions = block_macro_extensions = false
439
- end
440
- #parent_context = Block === parent ? parent.context : nil
441
- in_list = ListItem === parent
442
- block = nil
443
- style = nil
444
- explicit_style = nil
445
- sourcemap = document.sourcemap
446
- source_location = nil
447
-
448
- while !block && reader.has_more_lines?
449
- # if parsing metadata, read until there is no more to read
450
- if parse_metadata && parse_block_metadata_line(reader, document, attributes, options)
451
- reader.advance
452
- next
453
- #elsif parse_sections && !parent_context && is_next_line_section?(reader, attributes)
454
- # block, attributes = next_section(reader, parent, attributes)
455
- # break
456
- end
457
-
458
- # QUESTION should we introduce a parsing context object?
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
- unless delimited_block
491
-
492
- # this loop only executes once; used for flow control
493
- # break once a block is found or at end of loop
494
- # returns nil if the line must be dropped
495
- # Implementation note - while(true) is twice as fast as loop
496
- while true
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
- # process lines verbatim
499
- if style && Compliance.strict_verbatim_paragraphs && VERBATIM_STYLES.include?(style)
500
- block_context = style.to_sym
501
- reader.unshift_line this_line
502
- # advance to block parsing =>
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
- # process lines normally
507
- unless text_only
508
- first_char = Compliance.markdown_syntax ? this_line.lstrip.chr : this_line.chr
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
- if (LAYOUT_BREAK_LINES.has_key? first_char) && this_line.length >= 3 &&
511
- (Compliance.markdown_syntax ? LayoutBreakLinePlusRx : LayoutBreakLineRx) =~ this_line
512
- block = Block.new(parent, LAYOUT_BREAK_LINES[first_char], :content_model => :empty)
513
- break
514
-
515
- elsif this_line.end_with?(']') && (match = MediaBlockMacroRx.match(this_line))
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
- if blk_ctx == :image
519
- posattrs = ['alt', 'width', 'height']
520
- elsif blk_ctx == :video
529
+ case blk_ctx
530
+ when :video
521
531
  posattrs = ['poster', 'width', 'height']
522
- else
532
+ when :audio
523
533
  posattrs = []
534
+ else # :image
535
+ posattrs = ['alt', 'width', 'height']
524
536
  end
525
-
526
- # QUESTION why did we make exception for explicit style?
527
- #if style && !explicit_style
528
- if style
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
- # NOTE we're letting the toc macro have attributes
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 = GenericBlockMacroRx.match(this_line)) &&
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
- raw_attributes = match[3]
572
+ content = match[3]
578
573
  if extension.config[:content_model] == :attributes
579
- unless raw_attributes.empty?
580
- document.parse_attributes(raw_attributes, (extension.config[:pos_attrs] || []),
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'] = raw_attributes
579
+ attributes['text'] = content
585
580
  end
586
581
  if (default_attrs = extension.config[:default_attrs])
587
- default_attrs.each {|k, v| attributes[k] ||= v }
582
+ attributes.update(default_attrs) {|_, old_v| old_v }
588
583
  end
589
- if (block = extension.process_method[parent, target, attributes.dup])
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
- # haven't found anything yet, continue
600
- if (match = CalloutListRx.match(this_line))
601
- block = List.new(parent, :colist)
602
- attributes['style'] = 'arabic'
603
- reader.unshift_line this_line
604
- expected_index = 1
605
- # NOTE skip the match on the first time through as we've already done it (emulates begin...while)
606
- while match || (reader.has_more_lines? && (match = CalloutListRx.match(reader.peek_line)))
607
- # might want to move this check to a validate method
608
- if match[1].to_i != expected_index
609
- # FIXME this lineno - 2 hack means we need a proper look-behind cursor
610
- warn %(asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: callout list item index: expected #{expected_index} got #{match[1]})
611
- end
612
- list_item = next_list_item(reader, block, match)
613
- expected_index += 1
614
- if list_item
615
- block << list_item
616
- coids = document.callouts.callout_ids(block.items.size)
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
- warn %(asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for paragraph: #{style})
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
- break_at_list = (skipped == 0 && in_list)
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
- block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes)
715
- # a literal gets special meaning inside of a description list
716
- # TODO this feels hacky, better way to distinguish from explicit literal block?
717
- block.set_option('listparagraph') if in_list
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
- # a paragraph is contiguous nonblank/noncontinuation lines
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
- reader.unshift_line this_line
722
- lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => true
723
-
724
- # NOTE we need this logic because we've asked the reader to skip
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
- if lines[-1].start_with? '-- '
754
- attribution, citetitle = lines.pop[3..-1].split(', ', 2)
755
- lines.pop while lines[-1].empty?
756
- else
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
- block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes)
784
- end
785
- end
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
- # forbid loop from executing more than once
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
- # either delimited block or styled paragraph
793
- if !block && block_context
794
- # abstract and partintro should be handled by open block
795
- # FIXME kind of hackish...need to sort out how to generalize this
796
- block_context = :open if block_context == :abstract || block_context == :partintro
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
- case block_context
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
- when :comment
805
- build_block(block_context, :skip, terminator, parent, reader, attributes)
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
- when :example
809
- block = build_block(block_context, :compound, terminator, parent, reader, attributes)
810
-
811
- when :listing, :fenced_code, :source
812
- if block_context == :fenced_code
813
- style = attributes['style'] = 'source'
814
- language, linenums = this_line[3..-1].tr(' ', '').split(',', 2)
815
- if !language.nil_or_empty?
816
- attributes['language'] = language
817
- attributes['linenums'] = '' unless linenums.nil_or_empty?
818
- elsif (default_language = document.attributes['source-language'])
819
- attributes['language'] = default_language
820
- end
821
- if !attributes.key?('indent') && document.attributes.key?('source-indent')
822
- attributes['indent'] = document.attributes['source-indent']
823
- end
824
- terminator = terminator[0..2]
825
- elsif block_context == :source
826
- AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
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
- block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
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
- when :literal
839
- block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
766
+ catalog_inline_anchors lines * LF, block, document
767
+ end
840
768
 
841
- when :pass
842
- block = build_block(block_context, :raw, terminator, parent, reader, attributes)
769
+ break # forbid loop from executing more than once
770
+ end unless delimited_block
843
771
 
844
- when :stem, :latexmath, :asciimath
845
- if block_context == :stem
846
- attributes['style'] = if (explicit_stem_syntax = attributes[2])
847
- explicit_stem_syntax.include?('tex') ? 'latexmath' : 'asciimath'
848
- elsif (default_stem_syntax = document.attributes['stem']).nil_or_empty?
849
- 'asciimath'
850
- else
851
- default_stem_syntax
852
- end
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
- block = build_block(:stem, :raw, terminator, parent, reader, attributes)
855
-
856
- when :open, :sidebar
857
- block = build_block(block_context, :compound, terminator, parent, reader, attributes)
858
-
859
- when :table
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
- block = next_table(block_reader, parent, attributes)
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
- when :quote, :verse
871
- AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
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
- else
875
- if block_extensions && (extension = extensions.registered_for_block?(block_context, cloaked_context))
876
- # TODO pass cloaked_context to extension somehow (perhaps a new instance for each cloaked_context?)
877
- if (content_model = extension.config[:content_model]) != :skip
878
- if !(pos_attrs = extension.config[:pos_attrs] || []).empty?
879
- AttributeList.rekey(attributes, [nil].concat(pos_attrs))
880
- end
881
- if (default_attrs = extension.config[:default_attrs])
882
- default_attrs.each {|k, v| attributes[k] ||= v }
883
- end
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
- block = build_block block_context, content_model, terminator, parent, reader, attributes, :extension => extension
886
- unless block && content_model != :skip
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
- else
891
- # this should only happen if there's a misconfiguration
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 block
904
- block.source_location = source_location if source_location
905
- # REVIEW seems like there is a better way to organize this wrap-up
906
- block.title = attributes['title'] unless block.title?
907
- # FIXME HACK don't hardcode logic for alt, caption and scaledwidth on images down here
908
- if block.context == :image
909
- resolved_target = attributes['target']
910
- block.document.register(:images, resolved_target)
911
- attributes['alt'] ||= Helpers.basename(resolved_target, true).tr('_-', ' ')
912
- attributes['alt'] = block.sub_specialchars attributes['alt']
913
- block.assign_caption attributes.delete('caption'), 'figure'
914
- if (scaledwidth = attributes['scaledwidth'])
915
- # append % to scaledwidth if ends in number (no units present)
916
- if (48..57).include?((scaledwidth[-1] || 0).ord)
917
- attributes['scaledwidth'] = %(#{scaledwidth}%)
918
- end
919
- end
920
- else
921
- block.caption ||= attributes.delete('caption')
922
- end
923
- # TODO eventualy remove the style attribute from the attributes hash
924
- #block.style = attributes.delete('style')
925
- block.style = attributes['style']
926
- # AsciiDoc always use [id] as the reftext in HTML output,
927
- # but I'd like to do better in Asciidoctor
928
- if (block_id = (block.id ||= attributes['id']))
929
- # TODO sub reftext
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 && (DELIMITED_BLOCK_LEADERS.include? line[0..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[0..3]
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.has_key? tip
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 = *DELIMITED_BLOCKS[tip]
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 = *DELIMITED_BLOCKS[tip]
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 = *DELIMITED_BLOCKS[tip]
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 || content_model == :raw
1044
- skip_processing = content_model == :skip
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(:break_on_blank_lines => true, :break_on_list_continuation => true)
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
- lines = read_paragraph_lines reader, false, :skip_line_comments => true, :skip_processing => true
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(:terminator => terminator, :skip_processing => skip_processing)
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
- cursor = reader.cursor
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.has_key? 'title') && (block.document.attr? %(#{block.context}-caption))
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 attributes.delete('caption')
1082
+ block.assign_caption(attributes.delete 'caption')
1111
1083
  end
1112
1084
 
1113
- if content_model == :compound
1114
- # we can look for blocks until there are no more lines (and not worry
1115
- # about sections) since the reader is confined within the boundaries of a
1116
- # delimited block
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.has_more_lines?
1134
- block = Parser.next_block(reader, parent)
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 outline list Block from the current position of the Reader
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.next_outline_list(reader, list_type, parent)
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 on which the callouts are stored
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
- if text.include? '<'
1204
- text.scan(CalloutQuickScanRx) {
1205
- # alias match for Ruby 1.8.7 compat
1206
- m = $~
1207
- if m[0].chr != '\\'
1208
- document.callouts.register(m[2])
1209
- end
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, but don't process them
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
- # document - The current document on which the references are stored
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(text, document)
1224
- if text.include? '['
1225
- text.scan(InlineAnchorRx) {
1226
- # alias match for Ruby 1.8.7 compat
1227
- m = $~
1228
- next if m[0].start_with? '\\'
1229
- id = m[1] || m[3]
1230
- reftext = m[2] || m[4]
1231
- # enable if we want to allow double quoted values
1232
- #id = id.sub(DoubleQuotedRx, '\2')
1233
- #if reftext
1234
- # reftext = reftext.sub(DoubleQuotedMultiRx, '\2')
1235
- #end
1236
- document.register(:ids, [id, reftext])
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 labeled list
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 labeled list belongs
1230
+ # parent - The parent Block to which this description list belongs
1247
1231
  #
1248
- # Returns the Block encapsulating the parsed labeled list
1249
- def self.next_labeled_list(reader, match, parent)
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 labeled item
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 labeled list Block.
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
- # labeled list, the description ListItem). Finally, fold the first block
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] ') || text.start_with?('[*] ')
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
- cursor = reader.cursor
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
- list_item_reader.unshift_lines comment_lines unless comment_lines.empty?
1332
-
1333
- if !subsequent_line.nil?
1334
- continuation_connects_first_block = subsequent_line.empty?
1335
- # if there's no continuation connecting the first block, then
1336
- # treat the lines as paragraph text (activated when has_text = false)
1337
- if !continuation_connects_first_block && list_type != :dlist
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
- unless list_item.text? || list_item.blocks?
1363
- list_item = nil
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 labeled lists)
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 =~ this_line
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 =~ this_line
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 =~ this_line || BlockAttributeLineRx =~ this_line || AttributeEntryRx =~ this_line
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] =~ this_line }
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 && $~[3].nil_or_empty?
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 !prev_line.nil? && prev_line.empty?
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, stop reading
1485
- break if this_line.nil? || is_sibling_list_item?(this_line, list_type, sibling_trait)
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 && $~[3].nil_or_empty?
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 =~ this_line
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 && $~[3].nil_or_empty?
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 * EOL}<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(reader, parent, attributes = {})
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, _ = parse_section_title(reader, document)
1574
- attributes['reftext'] = sect_reftext if sect_reftext
1575
- section = Section.new parent, sect_level, document.attributes.has_key?('sectnums')
1576
- section.source_location = source_location if source_location
1577
- section.id = sect_id
1578
- section.title = sect_title
1579
- # parse style, id and role from first positional attribute
1580
- if attributes[1]
1581
- style, _ = parse_style_attribute attributes, reader
1582
- # handle case where only id and/or role are given (e.g., #idname.rolename)
1583
- if style
1584
- section.sectname = style
1585
- section.special = true
1586
- # HACK needs to be refactored so it's driven by config
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
- section.sectname = %(sect#{section.level})
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
- section.sectname = %(sect#{section.level})
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
- if !section.id && (id = attributes['id'])
1603
- section.id = id
1604
- else
1605
- # generate an id if one was not *embedded* in the heading line
1606
- # or as an anchor above the section
1607
- section.id ||= section.generate_id
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.id
1611
- # TODO sub reftext
1612
- section.document.register(:ids, [section.id, (attributes['reftext'] || section.title)])
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
- # returns the section level if the Reader is positioned at a section title,
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 !(val = attributes[1]).nil? && ((ord_0 = val[0].ord) == 100 || ord_0 == 102) && val =~ FloatingTitleStyleRx
1643
- return false
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 - the source Reader
1652
- # attributes - a Hash of attributes collected above the current line
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.is_next_line_document_title?(reader, attributes)
1656
- is_next_line_section?(reader, attributes) == 0
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
- # returns the section level if these lines are a section title,
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
- if (level = is_single_line_section_title?(line1))
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
- first_char = line1 ? line1.chr : nil
1678
- if (first_char == '=' || (Compliance.markdown_syntax && first_char == '#')) &&
1679
- (match = AtxSectionRx.match(line1))
1680
- single_line_section_level match[1]
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 line1 && line2 && SECTION_LEVELS.has_key?(line2.chr) &&
1688
- line2 =~ SetextSectionLineRx && line1 =~ SetextSectionTitleRx &&
1689
- # chomp so that a (non-visible) endline does not impact calculation
1690
- (line_length(line1) - line_length(line2)).abs <= 1
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
- sect_id = nil
1743
- sect_title = nil
1744
- sect_level = -1
1745
- sect_reftext = nil
1746
- single_line = true
1747
-
1748
- first_char = line1.chr
1749
- if (first_char == '=' || (Compliance.markdown_syntax && first_char == '#')) &&
1750
- (match = AtxSectionRx.match(line1))
1751
- sect_level = single_line_section_level match[1]
1752
- sect_title = match[2]
1753
- if sect_title.end_with?(']]') && (anchor_match = InlineSectionAnchorRx.match(sect_title))
1754
- if anchor_match[2].nil?
1755
- sect_title = anchor_match[1]
1756
- sect_id = anchor_match[3]
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
- if sect_level >= 0
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
- def self.line_length(line)
1790
- FORCE_UNICODE_LINE_LENGTH ? line.scan(UnicodeCharScanRx).length : line.length
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(reader, document)
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.has_key? key
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(reader, document)
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.has_key? key
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(reader, document)
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
- # process author attribute entries that override (or stand in for) the implicit author line
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.has_key?('authors') &&
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
- author_key = %(author_#{authors.size + 1})
1891
- while document.attributes.has_key? author_key
1892
- authors << document.attributes[author_key]
1893
- author_key = %(author_#{authors.size + 1})
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 authors.size == 1
1896
- # do not allow multiple, process as names only
1897
- author_metadata = process_authors authors[0], true, false
1898
- elsif authors.size > 1
1899
- # allow multiple, process as names only
1900
- author_metadata = process_authors authors.join('; '), true
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
- if author_metadata
1875
+ unless author_metadata.empty?
1905
1876
  document.attributes.update author_metadata
1906
1877
 
1907
1878
  # special case
1908
- if !document.attributes.has_key?('email') && document.attributes.has_key?('email_1')
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(author_line, names_only = false, multiple = true)
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 {|line| line.strip } : [author_line]
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
- # splitting on ' ' collapses repeating spaces uniformly
1946
- # `split ' ', 3` causes odd behavior in Opal; see https://github.com/asciidoctor/asciidoctor.js/issues/159
1947
- if (segments = author_entry.split ' ').size > 3
1948
- segments = segments[0..1].push(segments[2..-1].join ' ')
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
- segments.shift
1953
- end
1954
-
1955
- unless segments.nil?
1956
- author_metadata[key_map[:firstname]] = fname = segments[0].tr('_', ' ')
1957
- author_metadata[key_map[:author]] = fname
1958
- author_metadata[key_map[:authorinitials]] = fname[0, 1]
1959
- if !segments[1].nil? && !segments[2].nil?
1960
- author_metadata[key_map[:middlename]] = mname = segments[1].tr('_', ' ')
1961
- author_metadata[key_map[:lastname]] = lname = segments[2].tr('_', ' ')
1962
- author_metadata[key_map[:author]] = [fname, mname, lname].join ' '
1963
- author_metadata[key_map[:authorinitials]] = [fname[0, 1], mname[0, 1], lname[0, 1]].join
1964
- elsif !segments[1].nil?
1965
- author_metadata[key_map[:lastname]] = lname = segments[1].tr('_', ' ')
1966
- author_metadata[key_map[:author]] = [fname, lname].join ' '
1967
- author_metadata[key_map[:authorinitials]] = [fname[0, 1], lname[0, 1]].join
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[:email]] = segments[3] unless names_only || segments[3].nil?
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.strip.tr_s(' ', ' ')
1972
- author_metadata[key_map[:authorinitials]] = fname[0, 1]
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.has_key? key
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
- # parent - the parent to which the lines belong
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 lexer is only looking for text content
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(reader, parent, attributes = {}, options = {})
2006
- while parse_block_metadata_line(reader, parent, attributes, options)
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
- # parent - the parent of the current line
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 that lexer is only looking for text content
2031
- # and thus the block title should not be captured
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(reader, parent, attributes, options = {})
2035
- return false unless reader.has_more_lines?
2036
- next_line = reader.peek_line
2037
- if (commentish = next_line.start_with?('//')) && (match = CommentBlockRx.match(next_line))
2038
- terminator = match[0]
2039
- reader.read_lines_until(:skip_first_line => true, :preserve_last_line => true, :terminator => terminator, :skip_processing => true)
2040
- elsif commentish && CommentLineRx =~ next_line
2041
- # do nothing, we'll skip it
2042
- elsif !options[:text] && next_line.start_with?(':') && (match = AttributeEntryRx.match(next_line))
2043
- process_attribute_entry(reader, parent, attributes, match)
2044
- elsif (in_square_brackets = next_line.start_with?('[') && next_line.end_with?(']')) && (match = BlockAnchorRx.match(next_line))
2045
- unless match[1].nil_or_empty?
2046
- attributes['id'] = match[1]
2047
- # AsciiDoc always uses [id] as the reftext in HTML output,
2048
- # but I'd like to do better in Asciidoctor
2049
- # registration is deferred until the block or section is processed
2050
- attributes['reftext'] = match[2] unless match[2].nil?
2051
- end
2052
- elsif in_square_brackets && (match = BlockAttributeListRx.match(next_line))
2053
- parent.document.parse_attributes(match[1], [], :sub_input => true, :into => attributes)
2054
- # NOTE title doesn't apply to section, but we need to stash it for the first block
2055
- # TODO should issue an error if this is found above the document title
2056
- elsif !options[:text] && (match = BlockTitleRx.match(next_line))
2057
- attributes['title'] = match[1]
2058
- else
2059
- return false
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(reader, parent, attributes = nil)
2053
+ def self.process_attribute_entries reader, document, attributes = nil
2066
2054
  reader.skip_comment_lines
2067
- while process_attribute_entry(reader, parent, attributes)
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(reader, parent, attributes = nil, match = nil)
2075
- match ||= (reader.has_more_lines? ? AttributeEntryRx.match(reader.peek_line) : nil)
2076
- if match
2077
- name = match[1]
2078
- unless (value = match[2] || '').empty?
2079
- if value.end_with?(line_continuation = LINE_CONTINUATION) ||
2080
- value.end_with?(line_continuation = LINE_CONTINUATION_LEGACY)
2081
- value = value.chop.rstrip
2082
- while reader.advance
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(name, value, (parent ? parent.document : nil), attributes)
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(name, value, doc = nil, attrs = nil)
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 (undefined)
2113
- value = nil
2114
- name = name.chop
2115
- elsif name.start_with?('!')
2116
- # a nil value signals the attribute should be deleted (undefined)
2117
- value = nil
2118
- name = name[1..-1]
2119
- end
2120
-
2121
- name = sanitize_attribute_name(name)
2122
- accessible = true
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
- # alias numbered attribute to sectnums
2125
- if name == 'numbered'
2126
- name = 'sectnums'
2127
- # support relative leveloffset values
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
- when '-'
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
- accessible = value ? doc.set_attribute(name, value) : doc.delete_attribute(name)
2139
- end
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 && !marker.start_with?('.')
2168
- resolve_ordered_list_marker(marker, ordinal, validate, reader)
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 number_style
2201
- when :arabic
2202
- if validate
2203
- expected = ordinal + 1
2204
- actual = marker.to_i
2205
- end
2206
- marker = '1.'
2207
- when :loweralpha
2208
- if validate
2209
- expected = ('a'[0].ord + ordinal).chr
2210
- actual = marker.chomp('.')
2211
- end
2212
- marker = 'a.'
2213
- when :upperalpha
2214
- if validate
2215
- expected = ('A'[0].ord + ordinal).chr
2216
- actual = marker.chomp('.')
2217
- end
2218
- marker = 'A.'
2219
- when :lowerroman
2220
- if validate
2221
- # TODO report this in roman numerals; see https://github.com/jamesshipton/roman-numeral/blob/master/lib/roman_numeral.rb
2222
- expected = ordinal + 1
2223
- actual = roman_numeral_to_int(marker.chomp(')'))
2224
- end
2225
- marker = 'i)'
2226
- when :upperroman
2227
- if validate
2228
- # TODO report this in roman numerals; see https://github.com/jamesshipton/roman-numeral/blob/master/lib/roman_numeral.rb
2229
- expected = ordinal + 1
2230
- actual = roman_numeral_to_int(marker.chomp(')'))
2231
- end
2232
- marker = 'I)'
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 (attributes.has_key? 'title')
2259
+ if attributes.key? 'title'
2281
2260
  table.title = attributes.delete 'title'
2282
- table.assign_caption attributes.delete('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(table_reader, table, attributes)
2295
- skip_implicit_header = (attributes.key? 'header-option') || (attributes.key? 'noheader-option')
2296
- loop_idx = -1
2297
- while table_reader.has_more_lines?
2298
- loop_idx += 1
2299
- line = table_reader.read_line
2300
-
2301
- if !skip_implicit_header && skipped == 0 && loop_idx == 0 &&
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[1..-1]
2311
- # push an empty cell spec if boundary at start of line
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(line, :start, parser_ctx.delimiter)
2315
- # if the cell spec is not null, then we're at a cell boundary
2316
- if !next_cellspec.nil?
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
- else
2319
- # QUESTION do we not advance to next line? if so, when will we if we came into this block?
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
- seen = false
2325
- while !seen || !line.empty?
2326
- seen = true
2327
- if (m = parser_ctx.match_delimiter(line))
2328
- if parser_ctx.format == 'csv'
2329
- if parser_ctx.buffer_has_unclosed_quotes?(m.pre_match)
2330
- # throw it back, it's too small
2331
- line = parser_ctx.skip_matched_delimiter(m)
2332
- next
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
- else
2315
+ parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match})
2316
+ when 'dsv'
2335
2317
  if m.pre_match.end_with? '\\'
2336
- # skip over escaped delimiter
2337
- # handle special case when end of line is reached (see issue #1306)
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
- next
2323
+ redo
2344
2324
  end
2345
- end
2346
-
2347
- if parser_ctx.format == 'psv'
2348
- next_cellspec, cell_text = parse_cellspec(m.pre_match, :end)
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
- # suck up this line into the buffer and move on
2364
- parser_ctx.buffer = %(#{parser_ctx.buffer}#{line}#{EOL})
2365
- # QUESTION make stripping endlines in csv data an option? (unwrap-option?)
2366
- if parser_ctx.format == 'csv'
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
- end
2369
- line = ''
2370
- if parser_ctx.format == 'psv' || (parser_ctx.format == 'csv' &&
2371
- parser_ctx.buffer_has_unclosed_quotes?)
2372
- parser_ctx.keep_cell_open
2373
- else
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
- skipped = table_reader.skip_blank_lines unless parser_ctx.cell_open?
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.tr ' ', '' if records.include? ' '
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? && Table::ALIGNMENTS[:h].has_key?(colspec)
2426
- spec['halign'] = Table::ALIGNMENTS[:h][colspec]
2416
+ if !colspec.nil_or_empty? && TableCellHorzAlignments.key?(colspec)
2417
+ spec['halign'] = TableCellHorzAlignments[colspec]
2427
2418
  end
2428
- if !rowspec.nil_or_empty? && Table::ALIGNMENTS[:v].has_key?(rowspec)
2429
- spec['valign'] = Table::ALIGNMENTS[:v][rowspec]
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] && Table::TEXT_STYLES.has_key?(m[4])
2439
- spec['style'] = Table::TEXT_STYLES[m[4]]
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 = :start, delimiter = nil)
2466
- m = nil
2467
- rest = ''
2456
+ def self.parse_cellspec(line, pos = :end, delimiter = nil)
2457
+ m, rest = nil, ''
2468
2458
 
2469
- case pos
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
- when :end
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? && Table::ALIGNMENTS[:h].has_key?(colspec)
2507
- spec['halign'] = Table::ALIGNMENTS[:h][colspec]
2495
+ if !colspec.nil_or_empty? && TableCellHorzAlignments.key?(colspec)
2496
+ spec['halign'] = TableCellHorzAlignments[colspec]
2508
2497
  end
2509
- if !rowspec.nil_or_empty? && Table::ALIGNMENTS[:v].has_key?(rowspec)
2510
- spec['valign'] = Table::ALIGNMENTS[:v][rowspec]
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] && Table::TEXT_STYLES.has_key?(m[4])
2515
- spec['style'] = Table::TEXT_STYLES[m[4]]
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
- # both the original style attribute and the parsed value from the first
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
- # => ["abstract", "preamble"]
2524
+ # => "abstract"
2537
2525
  #
2538
2526
  # puts attributes
2539
- # => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro",
2540
- # "role" => "lead", "options" => ["fragment"], "fragment-option" => ''}
2527
+ # => { 1 => "abstract#intro.lead%fragment", "style" => "abstract", "id" => "intro",
2528
+ # "role" => "lead", "options" => "fragment", "fragment-option" => '' }
2541
2529
  #
2542
- # Returns a two-element Array of the parsed style from the
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
- original_style = attributes['style']
2547
- raw_style = attributes[1]
2548
- # NOTE spaces are not allowed in shorthand, so if we find one, this ain't shorthand
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
- if type != :style
2557
- warn %(asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} invalid empty #{type} detected in style attribute)
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.has_key? :id
2566
- warn %(asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} multiple ids detected in style attribute)
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.push c
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
- parsed_style = attributes['style'] = raw_style
2575
+ attributes['style'] = raw_style
2595
2576
  else
2596
2577
  save_current.call
2597
2578
 
2598
- if parsed.has_key? :style
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.has_key? :id
2605
- attributes['id'] = parsed[:id]
2606
- end
2581
+ attributes['id'] = parsed[:id] if parsed.key? :id
2607
2582
 
2608
- if parsed.has_key? :role
2609
- attributes['role'] = parsed[:role] * ' '
2610
- end
2583
+ attributes['role'] = parsed[:role] * ' ' if parsed.key? :role
2611
2584
 
2612
- if parsed.has_key? :option
2613
- (options = parsed[:option]).each do |option|
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
- [parsed_style, original_style]
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 ' '", " end"]
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) {|tabs| full_tab_space * tabs.length } if line.start_with? TAB
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