asciidoctor 1.5.8 → 2.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +11 -0
  3. data/CHANGELOG.adoc +628 -45
  4. data/LICENSE +2 -1
  5. data/README-de.adoc +28 -38
  6. data/README-fr.adoc +30 -43
  7. data/README-jp.adoc +255 -201
  8. data/README-zh_CN.adoc +40 -44
  9. data/README.adoc +170 -143
  10. data/asciidoctor.gemspec +22 -34
  11. data/bin/asciidoctor +5 -4
  12. data/data/locale/attributes-ar.adoc +4 -3
  13. data/data/locale/attributes-be.adoc +23 -0
  14. data/data/locale/attributes-bg.adoc +4 -3
  15. data/data/locale/attributes-ca.adoc +6 -5
  16. data/data/locale/attributes-cs.adoc +4 -3
  17. data/data/locale/attributes-da.adoc +6 -5
  18. data/data/locale/attributes-de.adoc +6 -5
  19. data/data/locale/attributes-en.adoc +4 -4
  20. data/data/locale/attributes-es.adoc +6 -5
  21. data/data/locale/attributes-fa.adoc +4 -3
  22. data/data/locale/attributes-fi.adoc +4 -3
  23. data/data/locale/attributes-fr.adoc +8 -7
  24. data/data/locale/attributes-hu.adoc +4 -3
  25. data/data/locale/attributes-id.adoc +4 -3
  26. data/data/locale/attributes-it.adoc +6 -5
  27. data/data/locale/attributes-ja.adoc +4 -3
  28. data/data/locale/{attributes-kr.adoc → attributes-ko.adoc} +4 -3
  29. data/data/locale/attributes-nb.adoc +4 -3
  30. data/data/locale/attributes-nl.adoc +6 -5
  31. data/data/locale/attributes-nn.adoc +4 -3
  32. data/data/locale/attributes-pl.adoc +8 -7
  33. data/data/locale/attributes-pt.adoc +6 -5
  34. data/data/locale/attributes-pt_BR.adoc +6 -5
  35. data/data/locale/attributes-ro.adoc +4 -3
  36. data/data/locale/attributes-ru.adoc +6 -5
  37. data/data/locale/attributes-sr.adoc +4 -4
  38. data/data/locale/attributes-sr_Latn.adoc +4 -4
  39. data/data/locale/attributes-sv.adoc +4 -4
  40. data/data/locale/attributes-th.adoc +23 -0
  41. data/data/locale/attributes-tr.adoc +4 -3
  42. data/data/locale/attributes-uk.adoc +6 -5
  43. data/data/locale/attributes-vi.adoc +23 -0
  44. data/data/locale/attributes-zh_CN.adoc +4 -3
  45. data/data/locale/attributes-zh_TW.adoc +4 -3
  46. data/data/reference/syntax.adoc +296 -0
  47. data/data/stylesheets/asciidoctor-default.css +120 -114
  48. data/data/stylesheets/coderay-asciidoctor.css +15 -17
  49. data/lib/asciidoctor/abstract_block.rb +146 -140
  50. data/lib/asciidoctor/abstract_node.rb +152 -170
  51. data/lib/asciidoctor/attribute_list.rb +77 -89
  52. data/lib/asciidoctor/block.rb +29 -28
  53. data/lib/asciidoctor/callouts.rb +4 -2
  54. data/lib/asciidoctor/cli/invoker.rb +20 -24
  55. data/lib/asciidoctor/cli/options.rb +107 -96
  56. data/lib/asciidoctor/cli.rb +3 -2
  57. data/lib/asciidoctor/convert.rb +199 -0
  58. data/lib/asciidoctor/converter/composite.rb +40 -48
  59. data/lib/asciidoctor/converter/docbook5.rb +627 -644
  60. data/lib/asciidoctor/converter/html5.rb +1053 -951
  61. data/lib/asciidoctor/converter/manpage.rb +581 -532
  62. data/lib/asciidoctor/converter/template.rb +232 -271
  63. data/lib/asciidoctor/converter.rb +370 -185
  64. data/lib/asciidoctor/core_ext/float/truncate.rb +20 -0
  65. data/lib/asciidoctor/core_ext/hash/merge.rb +8 -0
  66. data/lib/asciidoctor/core_ext/match_data/names.rb +7 -0
  67. data/lib/asciidoctor/core_ext/nil_or_empty.rb +1 -0
  68. data/lib/asciidoctor/core_ext/regexp/is_match.rb +4 -2
  69. data/lib/asciidoctor/core_ext.rb +8 -17
  70. data/lib/asciidoctor/document.rb +503 -461
  71. data/lib/asciidoctor/extensions.rb +127 -174
  72. data/lib/asciidoctor/helpers.rb +184 -107
  73. data/lib/asciidoctor/inline.rb +9 -12
  74. data/lib/asciidoctor/list.rb +11 -29
  75. data/lib/asciidoctor/load.rb +119 -0
  76. data/lib/asciidoctor/logging.rb +22 -17
  77. data/lib/asciidoctor/parser.rb +673 -719
  78. data/lib/asciidoctor/path_resolver.rb +48 -33
  79. data/lib/asciidoctor/reader.rb +383 -338
  80. data/lib/asciidoctor/rouge_ext.rb +39 -0
  81. data/lib/asciidoctor/rx.rb +723 -0
  82. data/lib/asciidoctor/section.rb +17 -16
  83. data/lib/asciidoctor/stylesheets.rb +19 -37
  84. data/lib/asciidoctor/substitutors.rb +926 -1022
  85. data/lib/asciidoctor/syntax_highlighter/coderay.rb +88 -0
  86. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +34 -0
  87. data/lib/asciidoctor/syntax_highlighter/html_pipeline.rb +10 -0
  88. data/lib/asciidoctor/syntax_highlighter/prettify.rb +30 -0
  89. data/lib/asciidoctor/syntax_highlighter/pygments.rb +157 -0
  90. data/lib/asciidoctor/syntax_highlighter/rouge.rb +143 -0
  91. data/lib/asciidoctor/syntax_highlighter.rb +253 -0
  92. data/lib/asciidoctor/table.rb +152 -114
  93. data/lib/asciidoctor/timings.rb +7 -5
  94. data/lib/asciidoctor/version.rb +2 -1
  95. data/lib/asciidoctor/writer.rb +30 -0
  96. data/lib/asciidoctor.rb +266 -1340
  97. data/man/asciidoctor.1 +49 -47
  98. data/man/asciidoctor.adoc +54 -45
  99. metadata +50 -245
  100. data/CONTRIBUTING.adoc +0 -185
  101. data/Gemfile +0 -60
  102. data/Rakefile +0 -129
  103. data/bin/asciidoctor-safe +0 -15
  104. data/features/open_block.feature +0 -92
  105. data/features/pass_block.feature +0 -66
  106. data/features/step_definitions.rb +0 -49
  107. data/features/text_formatting.feature +0 -57
  108. data/features/xref.feature +0 -1039
  109. data/lib/asciidoctor/converter/base.rb +0 -59
  110. data/lib/asciidoctor/converter/docbook45.rb +0 -93
  111. data/lib/asciidoctor/converter/factory.rb +0 -226
  112. data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +0 -6
  113. data/lib/asciidoctor/core_ext/1.8.7/concurrent/hash.rb +0 -5
  114. data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +0 -4
  115. data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +0 -6
  116. data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +0 -5
  117. data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +0 -6
  118. data/lib/asciidoctor/core_ext/1.8.7/string/limit_bytesize.rb +0 -29
  119. data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +0 -6
  120. data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +0 -6
  121. data/lib/asciidoctor/core_ext/string/limit_bytesize.rb +0 -10
  122. data/test/api_test.rb +0 -1240
  123. data/test/attribute_list_test.rb +0 -242
  124. data/test/attributes_test.rb +0 -1623
  125. data/test/blocks_test.rb +0 -3870
  126. data/test/converter_test.rb +0 -470
  127. data/test/document_test.rb +0 -1853
  128. data/test/extensions_test.rb +0 -1560
  129. data/test/fixtures/asciidoc_index.txt +0 -521
  130. data/test/fixtures/basic-docinfo-footer.html +0 -6
  131. data/test/fixtures/basic-docinfo-footer.xml +0 -8
  132. data/test/fixtures/basic-docinfo.html +0 -1
  133. data/test/fixtures/basic-docinfo.xml +0 -4
  134. data/test/fixtures/basic.asciidoc +0 -5
  135. data/test/fixtures/chapter-a.adoc +0 -3
  136. data/test/fixtures/child-include.adoc +0 -5
  137. data/test/fixtures/circle.svg +0 -9
  138. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +0 -6
  139. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +0 -6
  140. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +0 -3
  141. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +0 -5
  142. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +0 -1
  143. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +0 -6
  144. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +0 -3
  145. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +0 -5
  146. data/test/fixtures/custom-docinfodir/basic-docinfo.html +0 -1
  147. data/test/fixtures/custom-docinfodir/docinfo.html +0 -1
  148. data/test/fixtures/docinfo-footer.html +0 -1
  149. data/test/fixtures/docinfo-footer.xml +0 -9
  150. data/test/fixtures/docinfo.html +0 -1
  151. data/test/fixtures/docinfo.xml +0 -3
  152. data/test/fixtures/doctime-localtime.adoc +0 -2
  153. data/test/fixtures/dot.gif +0 -0
  154. data/test/fixtures/encoding.asciidoc +0 -13
  155. data/test/fixtures/file-with-missing-include.adoc +0 -1
  156. data/test/fixtures/grandchild-include.adoc +0 -3
  157. data/test/fixtures/hello-asciidoctor.pdf +0 -69
  158. data/test/fixtures/include-file.asciidoc +0 -24
  159. data/test/fixtures/include-file.jsx +0 -8
  160. data/test/fixtures/include-file.ml +0 -3
  161. data/test/fixtures/include-file.xml +0 -5
  162. data/test/fixtures/lists.adoc +0 -96
  163. data/test/fixtures/master.adoc +0 -5
  164. data/test/fixtures/mismatched-end-tag.adoc +0 -7
  165. data/test/fixtures/other-chapters.adoc +0 -11
  166. data/test/fixtures/outer-include.adoc +0 -5
  167. data/test/fixtures/parent-include-restricted.adoc +0 -5
  168. data/test/fixtures/parent-include.adoc +0 -5
  169. data/test/fixtures/sample.asciidoc +0 -30
  170. data/test/fixtures/section-a.adoc +0 -4
  171. data/test/fixtures/stylesheets/custom.css +0 -3
  172. data/test/fixtures/subdir/index.adoc +0 -3
  173. data/test/fixtures/subdir/inner-include.adoc +0 -3
  174. data/test/fixtures/subdir/middle-include.adoc +0 -5
  175. data/test/fixtures/subs-docinfo.html +0 -2
  176. data/test/fixtures/subs.adoc +0 -6
  177. data/test/fixtures/tagged-class-enclosed.rb +0 -25
  178. data/test/fixtures/tagged-class.rb +0 -23
  179. data/test/fixtures/tip.gif +0 -0
  180. data/test/fixtures/unclosed-tag.adoc +0 -3
  181. data/test/fixtures/unexpected-end-tag.adoc +0 -4
  182. data/test/invoker_test.rb +0 -745
  183. data/test/links_test.rb +0 -855
  184. data/test/lists_test.rb +0 -5151
  185. data/test/logger_test.rb +0 -211
  186. data/test/manpage_test.rb +0 -660
  187. data/test/options_test.rb +0 -262
  188. data/test/paragraphs_test.rb +0 -562
  189. data/test/parser_test.rb +0 -742
  190. data/test/paths_test.rb +0 -395
  191. data/test/preamble_test.rb +0 -173
  192. data/test/reader_test.rb +0 -2161
  193. data/test/sections_test.rb +0 -3575
  194. data/test/substitutions_test.rb +0 -2066
  195. data/test/tables_test.rb +0 -2036
  196. data/test/test_helper.rb +0 -447
  197. data/test/text_test.rb +0 -309
@@ -1,6 +1,6 @@
1
- # encoding: UTF-8
1
+ # frozen_string_literal: true
2
2
  module Asciidoctor
3
- # Public: Methods to parse lines of AsciiDoc into an object hierarchy
3
+ # Internal: Methods to parse lines of AsciiDoc into an object hierarchy
4
4
  # representing the structure of the document. All methods are class methods and
5
5
  # should be invoked from the Parser class. The main entry point is ::next_block.
6
6
  # No Parser instances shall be discovered running around. (Any attempt to
@@ -27,20 +27,22 @@ class Parser
27
27
 
28
28
  BlockMatchData = Struct.new :context, :masq, :tip, :terminator
29
29
 
30
- # Regexp for replacing tab character
31
- TabRx = /\t/
30
+ # String for matching tab character
31
+ TAB = ?\t
32
32
 
33
33
  # Regexp for leading tab indentation
34
34
  TabIndentRx = /^\t+/
35
35
 
36
- StartOfBlockProc = lambda {|l| ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (is_delimited_block? l) }
36
+ StartOfBlockProc = proc {|l| ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (is_delimited_block? l) }
37
37
 
38
- StartOfListProc = lambda {|l| AnyListRx.match? l }
38
+ StartOfListProc = proc {|l| AnyListRx.match? l }
39
39
 
40
- StartOfBlockOrListProc = lambda {|l| (is_delimited_block? l) || ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (AnyListRx.match? l) }
40
+ StartOfBlockOrListProc = proc {|l| (is_delimited_block? l) || ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (AnyListRx.match? l) }
41
41
 
42
42
  NoOp = nil
43
43
 
44
+ AuthorKeys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
45
+
44
46
  # Internal: A Hash mapping horizontal alignment abbreviations to alignments
45
47
  # that can be applied to a table cell (or to all cells in a column)
46
48
  TableCellHorzAlignments = {
@@ -66,16 +68,13 @@ class Parser
66
68
  'm' => :monospaced,
67
69
  'h' => :header,
68
70
  'l' => :literal,
69
- 'v' => :verse,
70
71
  'a' => :asciidoc
71
72
  }
72
73
 
73
- # Public: Make sure the Parser object doesn't get initialized.
74
+ # Hide the default constructor to make sure this class doesn't get instantiated.
74
75
  #
75
- # Raises RuntimeError if this constructor is invoked.
76
- def initialize
77
- raise 'Au contraire, mon frere. No parser instances will be running around.'
78
- end
76
+ # Raises NoMethodError if an attempt is made to invoke the constructor.
77
+ private_class_method :new
79
78
 
80
79
  # Public: Parses AsciiDoc source read from the Reader into the Document
81
80
  #
@@ -90,10 +89,10 @@ class Parser
90
89
  #
91
90
  # returns the Document object
92
91
  def self.parse(reader, document, options = {})
93
- block_attributes = parse_document_header(reader, document)
92
+ block_attributes = parse_document_header(reader, document, (header_only = options[:header_only]))
94
93
 
95
94
  # NOTE don't use a postfix conditional here as it's known to confuse JRuby in certain circumstances
96
- unless options[:header_only]
95
+ unless header_only
97
96
  while reader.has_more_lines?
98
97
  new_section, block_attributes = next_section(reader, document, block_attributes)
99
98
  if new_section
@@ -118,61 +117,82 @@ class Parser
118
117
  # which are automatically removed by the reader.
119
118
  #
120
119
  # returns the Hash of orphan block attributes captured above the header
121
- def self.parse_document_header(reader, document)
120
+ def self.parse_document_header(reader, document, header_only = false)
122
121
  # capture lines of block-level metadata and plow away comment lines that precede first block
123
- block_attrs = parse_block_metadata_lines reader, document
122
+ block_attrs = reader.skip_blank_lines ? (parse_block_metadata_lines reader, document) : {}
124
123
  doc_attrs = document.attributes
125
124
 
126
125
  # special case, block title is not allowed above document title,
127
126
  # carry attributes over to the document body
128
127
  if (implicit_doctitle = is_next_line_doctitle? reader, block_attrs, doc_attrs['leveloffset']) && block_attrs['title']
128
+ doc_attrs['authorcount'] = 0
129
129
  return document.finalize_header block_attrs, false
130
130
  end
131
131
 
132
132
  # yep, document title logic in AsciiDoc is just insanity
133
133
  # definitely an area for spec refinement
134
- assigned_doctitle = nil
134
+
135
135
  unless (val = doc_attrs['doctitle']).nil_or_empty?
136
- document.title = assigned_doctitle = val
136
+ document.title = doctitle_attr_val = val
137
137
  end
138
138
 
139
139
  # if the first line is the document title, add a header to the document and parse the header metadata
140
140
  if implicit_doctitle
141
141
  source_location = reader.cursor if document.sourcemap
142
- document.id, _, doctitle, _, atx = parse_section_title reader, document
143
- document.title = assigned_doctitle = doctitle unless assigned_doctitle
142
+ document.id, _, l0_section_title, _, atx = parse_section_title reader, document
143
+ if doctitle_attr_val
144
+ # NOTE doctitle attribute (set above or below implicit doctitle) overrides implicit doctitle
145
+ l0_section_title = nil
146
+ else
147
+ document.title = l0_section_title
148
+ if (doc_attrs['doctitle'] = doctitle_attr_val = document.sub_specialchars l0_section_title).include? ATTR_REF_HEAD
149
+ # QUESTION should we defer substituting attributes until the end of the header? or should we substitute again if necessary?
150
+ doc_attrs['doctitle'] = doctitle_attr_val = document.sub_attributes doctitle_attr_val, attribute_missing: 'skip'
151
+ end
152
+ end
144
153
  document.header.source_location = source_location if source_location
145
- # default to compat-mode if document uses atx-style doctitle
154
+ # default to compat-mode if document has setext doctitle
146
155
  doc_attrs['compat-mode'] = '' unless atx || (document.attribute_locked? 'compat-mode')
147
156
  if (separator = block_attrs['separator'])
148
157
  doc_attrs['title-separator'] = separator unless document.attribute_locked? 'title-separator'
149
158
  end
150
- doc_attrs['doctitle'] = section_title = doctitle
151
159
  if (doc_id = block_attrs['id'])
152
160
  document.id = doc_id
153
161
  else
154
162
  doc_id = document.id
155
163
  end
156
- if (doc_role = block_attrs['role'])
157
- doc_attrs['docrole'] = doc_role
164
+ if (role = block_attrs['role'])
165
+ doc_attrs['role'] = role
166
+ end
167
+ if (reftext = block_attrs['reftext'])
168
+ doc_attrs['reftext'] = reftext
158
169
  end
159
- if (doc_reftext = block_attrs['reftext'])
160
- doc_attrs['reftext'] = doc_reftext
170
+ block_attrs.clear
171
+ (modified_attrs = document.instance_variable_get :@attributes_modified).delete 'doctitle'
172
+ parse_header_metadata reader, document, nil
173
+ if modified_attrs.include? 'doctitle'
174
+ if (val = doc_attrs['doctitle']).nil_or_empty? || val == doctitle_attr_val
175
+ doc_attrs['doctitle'] = doctitle_attr_val
176
+ else
177
+ document.title = val
178
+ end
179
+ elsif !l0_section_title
180
+ modified_attrs << 'doctitle'
161
181
  end
162
- block_attrs = {}
163
- parse_header_metadata reader, document
164
182
  document.register :refs, [doc_id, document] if doc_id
183
+ elsif (author = doc_attrs['author'])
184
+ author_metadata = process_authors author, true, false
185
+ author_metadata.delete 'authorinitials' if doc_attrs['authorinitials']
186
+ doc_attrs.update author_metadata
187
+ elsif (author = doc_attrs['authors'])
188
+ author_metadata = process_authors author, true
189
+ doc_attrs.update author_metadata
190
+ else
191
+ doc_attrs['authorcount'] = 0
165
192
  end
166
193
 
167
- unless (val = doc_attrs['doctitle']).nil_or_empty? || val == section_title
168
- document.title = assigned_doctitle = val
169
- end
170
-
171
- # restore doctitle attribute to original assignment
172
- doc_attrs['doctitle'] = assigned_doctitle if assigned_doctitle
173
-
174
194
  # parse title and consume name section of manpage document
175
- parse_manpage_header(reader, document, block_attrs) if document.doctype == 'manpage'
195
+ parse_manpage_header reader, document, block_attrs, header_only if document.doctype == 'manpage'
176
196
 
177
197
  # NOTE block_attrs are the block-level attributes (not document attributes) that
178
198
  # precede the first line of content (document title, first section or first block)
@@ -182,12 +202,12 @@ class Parser
182
202
  # Public: Parses the manpage header of the AsciiDoc source read from the Reader
183
203
  #
184
204
  # returns Nothing
185
- def self.parse_manpage_header(reader, document, block_attributes)
205
+ def self.parse_manpage_header(reader, document, block_attributes, header_only = false)
186
206
  if ManpageTitleVolnumRx =~ (doc_attrs = document.attributes)['doctitle']
187
207
  doc_attrs['manvolnum'] = manvolnum = $2
188
208
  doc_attrs['mantitle'] = (((mantitle = $1).include? ATTR_REF_HEAD) ? (document.sub_attributes mantitle) : mantitle).downcase
189
209
  else
190
- logger.error message_with_context 'non-conforming manpage title', :source_location => (reader.cursor_at_line 1)
210
+ logger.error message_with_context 'non-conforming manpage title', source_location: (reader.cursor_at_line 1)
191
211
  # provide sensible fallbacks
192
212
  doc_attrs['mantitle'] = doc_attrs['doctitle'] || doc_attrs['docname'] || 'command'
193
213
  doc_attrs['manvolnum'] = manvolnum = '1'
@@ -199,6 +219,8 @@ class Parser
199
219
  doc_attrs['docname'] = manname
200
220
  doc_attrs['outfilesuffix'] = %(.#{manvolnum})
201
221
  end
222
+ elsif header_only
223
+ # done
202
224
  else
203
225
  reader.skip_blank_lines
204
226
  reader.save
@@ -206,11 +228,8 @@ class Parser
206
228
  if (name_section_level = is_next_line_section? reader, {})
207
229
  if name_section_level == 1
208
230
  name_section = initialize_section reader, document, {}
209
- name_section_buffer = (reader.read_lines_until :break_on_blank_lines => true, :skip_line_comments => true).map(&:lstrip).join ' '
231
+ name_section_buffer = (reader.read_lines_until break_on_blank_lines: true, skip_line_comments: true).map {|l| l.lstrip }.join ' '
210
232
  if ManpageNamePurposeRx =~ name_section_buffer
211
- doc_attrs['manname-title'] ||= name_section.title
212
- doc_attrs['manname-id'] = name_section.id if name_section.id
213
- doc_attrs['manpurpose'] = $2
214
233
  if (manname = $1).include? ATTR_REF_HEAD
215
234
  manname = document.sub_attributes manname
216
235
  end
@@ -219,8 +238,14 @@ class Parser
219
238
  else
220
239
  mannames = [manname]
221
240
  end
241
+ if (manpurpose = $2).include? ATTR_REF_HEAD
242
+ manpurpose = document.sub_attributes manpurpose
243
+ end
244
+ doc_attrs['manname-title'] ||= name_section.title
245
+ doc_attrs['manname-id'] = name_section.id if name_section.id
222
246
  doc_attrs['manname'] = manname
223
247
  doc_attrs['mannames'] = mannames
248
+ doc_attrs['manpurpose'] = manpurpose
224
249
  if document.backend == 'manpage'
225
250
  doc_attrs['docname'] = manname
226
251
  doc_attrs['outfilesuffix'] = %(.#{manvolnum})
@@ -236,8 +261,8 @@ class Parser
236
261
  end
237
262
  if error_msg
238
263
  reader.restore_save
239
- logger.error message_with_context error_msg, :source_location => reader.cursor
240
- doc_attrs['manname'] = (manname = doc_attrs['docname'] || 'command')
264
+ logger.error message_with_context error_msg, source_location: reader.cursor
265
+ doc_attrs['manname'] = manname = doc_attrs['docname'] || 'command'
241
266
  doc_attrs['mannames'] = [manname]
242
267
  if document.backend == 'manpage'
243
268
  doc_attrs['docname'] = manname
@@ -275,7 +300,7 @@ class Parser
275
300
  # source
276
301
  # # => "= Greetings\n\nThis is my doc.\n\n== Salutations\n\nIt is awesome."
277
302
  #
278
- # reader = Reader.new source, nil, :normalize => true
303
+ # reader = Reader.new source, nil, normalize: true
279
304
  # # create empty document to parent the section
280
305
  # # and hold attributes extracted from header
281
306
  # doc = Document.new
@@ -293,11 +318,11 @@ class Parser
293
318
  # check if we are at the start of processing the document
294
319
  # NOTE we could drop a hint in the attributes to indicate
295
320
  # that we are at a section title (so we don't have to check)
296
- if parent.context == :document && parent.blocks.empty? && ((has_header = parent.has_header?) ||
321
+ if parent.context == :document && parent.blocks.empty? && ((has_header = parent.header?) ||
297
322
  (attributes.delete 'invalid-header') || !(is_next_line_section? reader, attributes))
298
323
  book = (document = parent).doctype == 'book'
299
324
  if has_header || (book && attributes[1] != 'abstract')
300
- preamble = intro = (Block.new parent, :preamble, :content_model => :compound)
325
+ preamble = intro = Block.new parent, :preamble, content_model: :compound
301
326
  preamble.title = parent.attr 'preface-title' if book && (parent.attr? 'preface-title')
302
327
  parent.blocks << preamble
303
328
  end
@@ -320,7 +345,7 @@ class Parser
320
345
  if current_level == 0
321
346
  part = book
322
347
  elsif current_level == 1 && section.special
323
- # NOTE technically preface and abstract sections are only permitted in the book doctype
348
+ # NOTE technically preface sections are only permitted in the book doctype
324
349
  unless (sectname = section.sectname) == 'appendix' || sectname == 'preface' || sectname == 'abstract'
325
350
  expected_next_level = nil
326
351
  end
@@ -341,21 +366,24 @@ class Parser
341
366
  while reader.has_more_lines?
342
367
  parse_block_metadata_lines reader, document, attributes
343
368
  if (next_level = is_next_line_section?(reader, attributes))
344
- next_level += document.attr('leveloffset').to_i if document.attr?('leveloffset')
369
+ if document.attr? 'leveloffset'
370
+ next_level += (document.attr 'leveloffset').to_i
371
+ next_level = 0 if next_level < 0
372
+ end
345
373
  if next_level > current_level
346
374
  if expected_next_level
347
375
  unless next_level == expected_next_level || (expected_next_level_alt && next_level == expected_next_level_alt) || expected_next_level < 0
348
376
  expected_condition = expected_next_level_alt ? %(expected levels #{expected_next_level_alt} or #{expected_next_level}) : %(expected level #{expected_next_level})
349
- logger.warn message_with_context %(section title out of sequence: #{expected_condition}, got level #{next_level}), :source_location => reader.cursor
377
+ logger.warn message_with_context %(section title out of sequence: #{expected_condition}, got level #{next_level}), source_location: reader.cursor
350
378
  end
351
379
  else
352
- logger.error message_with_context %(#{sectname} sections do not support nested sections), :source_location => reader.cursor
380
+ logger.error message_with_context %(#{sectname} sections do not support nested sections), source_location: reader.cursor
353
381
  end
354
382
  new_section, attributes = next_section reader, section, attributes
355
383
  section.assign_numeral new_section
356
384
  section.blocks << new_section
357
385
  elsif next_level == 0 && section == document
358
- logger.error message_with_context 'level 0 sections can only be used when doctype is book', :source_location => reader.cursor unless book
386
+ logger.error message_with_context 'level 0 sections can only be used when doctype is book', source_location: reader.cursor unless book
359
387
  new_section, attributes = next_section reader, section, attributes
360
388
  section.assign_numeral new_section
361
389
  section.blocks << new_section
@@ -366,7 +394,7 @@ class Parser
366
394
  else
367
395
  # just take one block or else we run the risk of overrunning section boundaries
368
396
  block_cursor = reader.cursor
369
- if (new_block = next_block reader, intro || section, attributes, :parse_metadata => false)
397
+ if (new_block = next_block reader, intro || section, attributes, parse_metadata: false)
370
398
  # REVIEW this may be doing too much
371
399
  if part
372
400
  if !section.blocks?
@@ -378,7 +406,7 @@ class Parser
378
406
  new_block.style = 'partintro'
379
407
  # emulate [partintro] open block
380
408
  else
381
- new_block.parent = (intro = Block.new section, :open, :content_model => :compound)
409
+ new_block.parent = (intro = Block.new section, :open, content_model: :compound)
382
410
  intro.style = 'partintro'
383
411
  section.blocks << intro
384
412
  end
@@ -387,10 +415,10 @@ class Parser
387
415
  first_block = section.blocks[0]
388
416
  # open the [partintro] open block for appending
389
417
  if !intro && first_block.content_model == :compound
390
- logger.error message_with_context 'illegal block content outside of partintro block', :source_location => block_cursor
418
+ logger.error message_with_context 'illegal block content outside of partintro block', source_location: block_cursor
391
419
  # rebuild [partintro] paragraph as an open block
392
420
  elsif first_block.content_model != :compound
393
- new_block.parent = (intro = Block.new section, :open, :content_model => :compound)
421
+ new_block.parent = (intro = Block.new section, :open, content_model: :compound)
394
422
  intro.style = 'partintro'
395
423
  section.blocks.shift
396
424
  if first_block.style == 'partintro'
@@ -404,10 +432,7 @@ class Parser
404
432
  end
405
433
 
406
434
  (intro || section).blocks << new_block
407
- attributes = {}
408
- #else
409
- # # don't clear attributes if we don't find a block because they may
410
- # # be trailing attributes that didn't get associated with a block
435
+ attributes.clear
411
436
  end
412
437
  end
413
438
 
@@ -416,15 +441,17 @@ class Parser
416
441
 
417
442
  if part
418
443
  unless section.blocks? && section.blocks[-1].context == :section
419
- logger.error message_with_context 'invalid part, must have at least one section (e.g., chapter, appendix, etc.)', :source_location => reader.cursor
444
+ logger.error message_with_context 'invalid part, must have at least one section (e.g., chapter, appendix, etc.)', source_location: reader.cursor
420
445
  end
421
446
  # NOTE we could try to avoid creating a preamble in the first place, though
422
447
  # that would require reworking assumptions in next_section since the preamble
423
448
  # is treated like an untitled section
424
449
  elsif preamble # implies parent == document
425
450
  if preamble.blocks?
451
+ if book || document.blocks[1] || !Compliance.unwrap_standalone_preamble
452
+ preamble.source_location = preamble.blocks[0].source_location if document.sourcemap
426
453
  # unwrap standalone preamble (i.e., document has no sections) except for books, if permissible
427
- unless book || document.blocks[1] || !Compliance.unwrap_standalone_preamble
454
+ else
428
455
  document.blocks.shift
429
456
  while (child_block = preamble.blocks.shift)
430
457
  document << child_block
@@ -437,10 +464,10 @@ class Parser
437
464
  end
438
465
 
439
466
  # The attributes returned here are orphaned attributes that fall at the end
440
- # of a section that need to get transfered to the next section
467
+ # of a section that need to get transferred to the next section
441
468
  # see "trailing block attributes transfer to the following section" in
442
469
  # test/attributes_test.rb for an example
443
- [section != parent ? section : nil, attributes.dup]
470
+ [section == parent ? nil : section, attributes.merge]
444
471
  end
445
472
 
446
473
  # Public: Parse and return the next Block at the Reader's current location
@@ -459,7 +486,8 @@ class Parser
459
486
  # attributes - A Hash of attributes that will become the attributes
460
487
  # associated with the parsed Block (default: {}).
461
488
  # options - An options Hash to control parsing (default: {}):
462
- # * :text indicates that the parser is only looking for text content
489
+ # * :text_only indicates that the parser is only looking for text content
490
+ # * :list_type indicates this block will be attached to a list item in a list of the specified type
463
491
  #
464
492
  # Returns a Block object built from the parsed content of the processed
465
493
  # lines, or nothing if no block is found.
@@ -470,15 +498,15 @@ class Parser
470
498
  # check for option to find list item text only
471
499
  # if skipped a line, assume a list continuation was
472
500
  # used and block content is acceptable
473
- if (text_only = options[:text]) && skipped > 0
474
- options.delete :text
475
- text_only = false
501
+ if (text_only = options[:text_only]) && skipped > 0
502
+ options.delete :text_only
503
+ text_only = nil
476
504
  end
477
505
 
478
506
  document = parent.document
479
507
 
480
508
  if options.fetch :parse_metadata, true
481
- # read lines until there are no more metadata lines to read
509
+ # read lines until there are no more metadata lines to read; note that :text_only option impacts parsing rules
482
510
  while parse_block_metadata_line reader, document, attributes, options
483
511
  # discard the line just processed
484
512
  reader.shift
@@ -499,19 +527,21 @@ class Parser
499
527
  if (delimited_block = is_delimited_block? this_line, true)
500
528
  block_context = cloaked_context = delimited_block.context
501
529
  terminator = delimited_block.terminator
502
- if !style
503
- style = attributes['style'] = block_context.to_s
504
- elsif style != block_context.to_s
505
- if delimited_block.masq.include? style
506
- block_context = style.to_sym
507
- elsif delimited_block.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
508
- block_context = :admonition
509
- elsif block_extensions && extensions.registered_for_block?(style, block_context)
510
- block_context = style.to_sym
511
- else
512
- logger.warn message_with_context %(invalid style for #{block_context} block: #{style}), :source_location => reader.cursor_at_mark
513
- style = block_context.to_s
530
+ if style
531
+ unless style == block_context.to_s
532
+ if delimited_block.masq.include? style
533
+ block_context = style.to_sym
534
+ elsif delimited_block.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
535
+ block_context = :admonition
536
+ elsif block_extensions && extensions.registered_for_block?(style, block_context)
537
+ block_context = style.to_sym
538
+ else
539
+ logger.debug message_with_context %(unknown style for #{block_context} block: #{style}), source_location: reader.cursor_at_mark if logger.debug?
540
+ style = block_context.to_s
541
+ end
514
542
  end
543
+ else
544
+ style = attributes['style'] = block_context.to_s
515
545
  end
516
546
  end
517
547
 
@@ -520,7 +550,7 @@ class Parser
520
550
  # returns nil if the line should be dropped
521
551
  while true
522
552
  # process lines verbatim
523
- if style && Compliance.strict_verbatim_paragraphs && VERBATIM_STYLES.include?(style)
553
+ if style && Compliance.strict_verbatim_paragraphs && (VERBATIM_STYLES.include? style)
524
554
  block_context = style.to_sym
525
555
  reader.unshift_line this_line
526
556
  # advance to block parsing =>
@@ -540,7 +570,7 @@ class Parser
540
570
  #!(this_line.start_with? ' ') &&
541
571
  (MarkdownThematicBreakRx.match? this_line)
542
572
  # NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
543
- block = Block.new(parent, :thematic_break, :content_model => :empty)
573
+ block = Block.new(parent, :thematic_break, content_model: :empty)
544
574
  break
545
575
  end
546
576
  elsif this_line.start_with? TAB
@@ -548,17 +578,17 @@ class Parser
548
578
  else
549
579
  indented, ch0 = false, this_line.chr
550
580
  layout_break_chars = md_syntax ? HYBRID_LAYOUT_BREAK_CHARS : LAYOUT_BREAK_CHARS
551
- if (layout_break_chars.key? ch0) && (md_syntax ? (ExtLayoutBreakRx.match? this_line) :
552
- (this_line == ch0 * (ll = this_line.length) && ll > 2))
581
+ if (layout_break_chars.key? ch0) &&
582
+ (md_syntax ? (ExtLayoutBreakRx.match? this_line) : (uniform? this_line, ch0, (ll = this_line.length)) && ll > 2)
553
583
  # NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
554
- block = Block.new(parent, layout_break_chars[ch0], :content_model => :empty)
584
+ block = Block.new(parent, layout_break_chars[ch0], content_model: :empty)
555
585
  break
556
586
  # NOTE very rare that a text-only line will end in ] (e.g., inline macro), so check that first
557
587
  elsif (this_line.end_with? ']') && (this_line.include? '::')
558
588
  #if (this_line.start_with? 'image', 'video', 'audio') && BlockMediaMacroRx =~ this_line
559
589
  if (ch0 == 'i' || (this_line.start_with? 'video:', 'audio:')) && BlockMediaMacroRx =~ this_line
560
590
  blk_ctx, target, blk_attrs = $1.to_sym, $2, $3
561
- block = Block.new parent, blk_ctx, :content_model => :empty
591
+ block = Block.new parent, blk_ctx, content_model: :empty
562
592
  if blk_attrs
563
593
  case blk_ctx
564
594
  when :video
@@ -568,63 +598,74 @@ class Parser
568
598
  else # :image
569
599
  posattrs = ['alt', 'width', 'height']
570
600
  end
571
- block.parse_attributes blk_attrs, posattrs, :sub_input => true, :into => attributes
601
+ block.parse_attributes blk_attrs, posattrs, sub_input: true, into: attributes
572
602
  end
573
603
  # style doesn't have special meaning for media macros
574
604
  attributes.delete 'style' if attributes.key? 'style'
575
- if (target.include? ATTR_REF_HEAD) && (target = block.sub_attributes target, :attribute_missing => 'drop-line').empty?
576
- # retain as unparsed if attribute-missing is skip
577
- if (doc_attrs['attribute-missing'] || Compliance.attribute_missing) == 'skip'
578
- return Block.new(parent, :paragraph, :content_model => :simple, :source => [this_line])
579
- # otherwise, drop the line
580
- else
605
+ if target.include? ATTR_REF_HEAD
606
+ if (expanded_target = block.sub_attributes target).empty? &&
607
+ (doc_attrs['attribute-missing'] || Compliance.attribute_missing) == 'drop-line' &&
608
+ (block.sub_attributes target + ' ', attribute_missing: 'drop-line', drop_line_severity: :ignore).empty?
581
609
  attributes.clear
582
610
  return
611
+ else
612
+ target = expanded_target
583
613
  end
584
614
  end
585
615
  if blk_ctx == :image
586
- document.register :images, [target, (attributes['imagesdir'] = doc_attrs['imagesdir'])]
616
+ document.register :images, target
617
+ attributes['imagesdir'] = doc_attrs['imagesdir']
587
618
  # NOTE style is the value of the first positional attribute in the block attribute line
588
619
  attributes['alt'] ||= style || (attributes['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
589
620
  unless (scaledwidth = attributes.delete 'scaledwidth').nil_or_empty?
590
621
  # NOTE assume % units if not specified
591
622
  attributes['scaledwidth'] = (TrailingDigitsRx.match? scaledwidth) ? %(#{scaledwidth}%) : scaledwidth
592
623
  end
593
- if attributes.key? 'title'
594
- block.title = attributes.delete 'title'
595
- block.assign_caption((attributes.delete 'caption'), 'figure')
624
+ if attributes['title']
625
+ block.title = block_title = attributes.delete 'title'
626
+ block.assign_caption (attributes.delete 'caption'), 'figure'
596
627
  end
597
628
  end
598
629
  attributes['target'] = target
599
630
  break
600
631
 
601
632
  elsif ch0 == 't' && (this_line.start_with? 'toc:') && BlockTocMacroRx =~ this_line
602
- block = Block.new parent, :toc, :content_model => :empty
603
- block.parse_attributes $1, [], :into => attributes if $1
633
+ block = Block.new parent, :toc, content_model: :empty
634
+ block.parse_attributes $1, [], into: attributes if $1
604
635
  break
605
636
 
606
- elsif block_macro_extensions && CustomBlockMacroRx =~ this_line &&
607
- (extension = extensions.registered_for_block_macro? $1)
608
- target, content = $2, $3
609
- if (target.include? ATTR_REF_HEAD) && (target = parent.sub_attributes target).empty? &&
610
- (doc_attrs['attribute-missing'] || Compliance.attribute_missing) == 'drop-line'
611
- attributes.clear
612
- return
613
- end
614
- if extension.config[:content_model] == :attributes
615
- document.parse_attributes content, extension.config[:pos_attrs] || [], :sub_input => true, :into => attributes if content
616
- else
617
- attributes['text'] = content || ''
618
- end
619
- if (default_attrs = extension.config[:default_attrs])
620
- attributes.update(default_attrs) {|_, old_v| old_v }
621
- end
622
- if (block = extension.process_method[parent, target, attributes])
623
- attributes.replace block.attributes
624
- break
637
+ elsif block_macro_extensions ? (CustomBlockMacroRx =~ this_line &&
638
+ (extension = extensions.registered_for_block_macro? $1) || (report_unknown_block_macro = logger.debug?)) :
639
+ (logger.debug? && (report_unknown_block_macro = CustomBlockMacroRx =~ this_line))
640
+ if report_unknown_block_macro
641
+ logger.debug message_with_context %(unknown name for block macro: #{$1}), source_location: reader.cursor_at_mark
625
642
  else
626
- attributes.clear
627
- return
643
+ content = $3
644
+ if (target = $2).include? ATTR_REF_HEAD
645
+ if (expanded_target = parent.sub_attributes target).empty? &&
646
+ (doc_attrs['attribute-missing'] || Compliance.attribute_missing) == 'drop-line' &&
647
+ (parent.sub_attributes target + ' ', attribute_missing: 'drop-line', drop_line_severity: :ignore).empty?
648
+ attributes.clear
649
+ return
650
+ else
651
+ target = expanded_target
652
+ end
653
+ end
654
+ if (ext_config = extension.config)[:content_model] == :attributes
655
+ document.parse_attributes content, ext_config[:positional_attrs] || ext_config[:pos_attrs] || [], sub_input: true, into: attributes if content
656
+ else
657
+ attributes['text'] = content || ''
658
+ end
659
+ if (default_attrs = ext_config[:default_attrs])
660
+ attributes.update(default_attrs) {|_, old_v| old_v }
661
+ end
662
+ if (block = extension.process_method[parent, target, attributes]) && block != parent
663
+ attributes.replace block.attributes
664
+ break
665
+ else
666
+ attributes.clear
667
+ return
668
+ end
628
669
  end
629
670
  end
630
671
  end
@@ -640,28 +681,28 @@ class Parser
640
681
 
641
682
  elsif UnorderedListRx.match? this_line
642
683
  reader.unshift_line this_line
643
- attributes['style'] = (style = 'bibliography') if !style && Section === parent && parent.sectname == 'bibliography'
684
+ attributes['style'] = style = 'bibliography' if !style && Section === parent && parent.sectname == 'bibliography'
644
685
  block = parse_list(reader, :ulist, parent, style)
645
686
  break
646
687
 
647
- elsif (match = OrderedListRx.match(this_line))
688
+ elsif OrderedListRx.match? this_line
648
689
  reader.unshift_line this_line
649
690
  block = parse_list(reader, :olist, parent, style)
650
691
  attributes['style'] = block.style if block.style
651
692
  break
652
693
 
653
- elsif (match = DescriptionListRx.match(this_line))
694
+ elsif ((this_line.include? '::') || (this_line.include? ';;')) && DescriptionListRx =~ this_line
654
695
  reader.unshift_line this_line
655
- block = parse_description_list(reader, match, parent)
696
+ block = parse_description_list(reader, $~, parent)
656
697
  break
657
698
 
658
699
  elsif (style == 'float' || style == 'discrete') && (Compliance.underline_style_section_titles ?
659
700
  (is_section_title? this_line, reader.peek_line) : !indented && (atx_section_title? this_line))
660
701
  reader.unshift_line this_line
661
- float_id, float_reftext, float_title, float_level = parse_section_title reader, document, attributes['id']
702
+ float_id, float_reftext, block_title, float_level = parse_section_title reader, document, attributes['id']
662
703
  attributes['reftext'] = float_reftext if float_reftext
663
- block = Block.new(parent, :floating_title, :content_model => :empty)
664
- block.title = float_title
704
+ block = Block.new(parent, :floating_title, content_model: :empty)
705
+ block.title = block_title
665
706
  attributes.delete 'title'
666
707
  block.id = float_id || ((doc_attrs.key? 'sectids') ? (Section.generate_id block.title, document) : nil)
667
708
  block.level = float_level
@@ -689,7 +730,7 @@ class Parser
689
730
  # advance to block parsing =>
690
731
  break
691
732
  else
692
- logger.warn message_with_context %(invalid style for paragraph: #{style}), :source_location => reader.cursor_at_mark
733
+ logger.debug message_with_context %(unknown style for paragraph: #{style}), source_location: reader.cursor_at_mark if logger.debug?
693
734
  style = nil
694
735
  # continue to process paragraph
695
736
  end
@@ -700,32 +741,36 @@ class Parser
700
741
  # a literal paragraph: contiguous lines starting with at least one whitespace character
701
742
  # NOTE style can only be nil or "normal" at this point
702
743
  if indented && !style
703
- lines = read_paragraph_lines reader, (in_list = ListItem === parent) && skipped == 0, :skip_line_comments => text_only
744
+ lines = read_paragraph_lines reader, (content_adjacent = skipped == 0 ? options[:list_type] : nil), skip_line_comments: text_only
704
745
  adjust_indentation! lines
705
- block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes)
706
- # a literal gets special meaning inside of a description list
707
- # TODO this feels hacky, better way to distinguish from explicit literal block?
708
- block.set_option('listparagraph') if in_list
746
+ if text_only || content_adjacent == :dlist
747
+ # this block gets folded into the list item text
748
+ block = Block.new(parent, :paragraph, content_model: :simple, source: lines, attributes: attributes)
749
+ else
750
+ block = Block.new(parent, :literal, content_model: :verbatim, source: lines, attributes: attributes)
751
+ end
709
752
  # a normal paragraph: contiguous non-blank/non-continuation lines (left-indented or normal style)
710
753
  else
711
- lines = read_paragraph_lines reader, skipped == 0 && ListItem === parent, :skip_line_comments => true
754
+ lines = read_paragraph_lines reader, skipped == 0 && options[:list_type], skip_line_comments: true
712
755
  # NOTE don't check indented here since it's extremely rare
713
756
  #if text_only || indented
714
757
  if text_only
715
758
  # if [normal] is used over an indented paragraph, shift content to left margin
716
759
  # QUESTION do we even need to shift since whitespace is normalized by XML in this case?
717
760
  adjust_indentation! lines if indented && style == 'normal'
718
- block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes)
761
+ block = Block.new(parent, :paragraph, content_model: :simple, source: lines, attributes: attributes)
719
762
  elsif (ADMONITION_STYLE_HEADS.include? ch0) && (this_line.include? ':') && (AdmonitionParagraphRx =~ this_line)
720
763
  lines[0] = $' # string after match
721
764
  attributes['name'] = admonition_name = (attributes['style'] = $1).downcase
722
765
  attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
723
- block = Block.new(parent, :admonition, :content_model => :simple, :source => lines, :attributes => attributes)
766
+ block = Block.new(parent, :admonition, content_model: :simple, source: lines, attributes: attributes)
724
767
  elsif md_syntax && ch0 == '>' && this_line.start_with?('> ')
725
768
  lines.map! {|line| line == '>' ? (line.slice 1, line.length) : ((line.start_with? '> ') ? (line.slice 2, line.length) : line) }
726
769
  if lines[-1].start_with? '-- '
727
770
  credit_line = (credit_line = lines.pop).slice 3, credit_line.length
728
- lines.pop while lines[-1].empty?
771
+ unless lines.empty?
772
+ lines.pop while lines[-1].empty?
773
+ end
729
774
  end
730
775
  attributes['style'] = 'quote'
731
776
  # NOTE will only detect discrete (aka free-floating) headings
@@ -743,7 +788,7 @@ class Parser
743
788
  lines.pop while lines[-1].empty?
744
789
  lines << lines.pop.chop # strip trailing quote
745
790
  attributes['style'] = 'quote'
746
- block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes)
791
+ block = Block.new(parent, :quote, content_model: :simple, source: lines, attributes: attributes)
747
792
  attribution, citetitle = (block.apply_subs credit_line).split ', ', 2
748
793
  attributes['attribution'] = attribution if attribution
749
794
  attributes['citetitle'] = citetitle if citetitle
@@ -751,7 +796,7 @@ class Parser
751
796
  # if [normal] is used over an indented paragraph, shift content to left margin
752
797
  # QUESTION do we even need to shift since whitespace is normalized by XML in this case?
753
798
  adjust_indentation! lines if indented && style == 'normal'
754
- block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes)
799
+ block = Block.new(parent, :paragraph, content_model: :simple, source: lines, attributes: attributes)
755
800
  end
756
801
 
757
802
  catalog_inline_anchors((lines.join LF), block, document, reader)
@@ -762,63 +807,47 @@ class Parser
762
807
 
763
808
  # either delimited block or styled paragraph
764
809
  unless block
765
- # abstract and partintro should be handled by open block
766
- # FIXME kind of hackish...need to sort out how to generalize this
767
- block_context = :open if block_context == :abstract || block_context == :partintro
768
-
769
810
  case block_context
770
- when :admonition
771
- attributes['name'] = admonition_name = style.downcase
772
- attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
773
- block = build_block(block_context, :compound, terminator, parent, reader, attributes)
774
-
775
- when :comment
776
- build_block(block_context, :skip, terminator, parent, reader, attributes)
777
- attributes.clear
778
- return
779
-
780
- when :example
781
- block = build_block(block_context, :compound, terminator, parent, reader, attributes)
782
-
783
- when :listing, :literal
784
- block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
785
-
786
- when :source
787
- AttributeList.rekey attributes, [nil, 'language', 'linenums']
788
- if doc_attrs.key? 'source-language'
789
- attributes['language'] = doc_attrs['source-language'] || 'text'
790
- end unless attributes.key? 'language'
791
- if (attributes.key? 'linenums-option') || (doc_attrs.key? 'source-linenums-option')
792
- attributes['linenums'] = ''
793
- end unless attributes.key? 'linenums'
794
- if doc_attrs.key? 'source-indent'
795
- attributes['indent'] = doc_attrs['source-indent']
796
- end unless attributes.key? 'indent'
811
+ when :listing, :source
812
+ if block_context == :source || (!attributes[1] && (language = attributes[2] || doc_attrs['source-language']))
813
+ if language
814
+ attributes['style'] = 'source'
815
+ attributes['language'] = language
816
+ AttributeList.rekey attributes, [nil, nil, 'linenums']
817
+ else
818
+ AttributeList.rekey attributes, [nil, 'language', 'linenums']
819
+ if doc_attrs.key? 'source-language'
820
+ attributes['language'] = doc_attrs['source-language']
821
+ end unless attributes.key? 'language'
822
+ end
823
+ if attributes['linenums-option'] || doc_attrs['source-linenums-option']
824
+ attributes['linenums'] = ''
825
+ end unless attributes.key? 'linenums'
826
+ if doc_attrs.key? 'source-indent'
827
+ attributes['indent'] = doc_attrs['source-indent']
828
+ end unless attributes.key? 'indent'
829
+ end
797
830
  block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
798
-
799
831
  when :fenced_code
800
832
  attributes['style'] = 'source'
801
- if (ll = this_line.length) == 3
802
- language = nil
803
- elsif (comma_idx = (language = this_line.slice 3, ll).index ',')
804
- if comma_idx > 0
805
- language = (language.slice 0, comma_idx).strip
806
- attributes['linenums'] = '' if comma_idx < ll - 4
833
+ if (ll = this_line.length) > 3
834
+ if (comma_idx = (language = this_line.slice 3, ll).index ',')
835
+ if comma_idx > 0
836
+ language = (language.slice 0, comma_idx).strip
837
+ attributes['linenums'] = '' if comma_idx < ll - 4
838
+ elsif ll > 4
839
+ attributes['linenums'] = ''
840
+ end
807
841
  else
808
- language = nil
809
- attributes['linenums'] = '' if ll > 4
842
+ language = language.lstrip
810
843
  end
811
- else
812
- language = language.lstrip
813
844
  end
814
845
  if language.nil_or_empty?
815
- if doc_attrs.key? 'source-language'
816
- attributes['language'] = doc_attrs['source-language'] || 'text'
817
- end
846
+ attributes['language'] = doc_attrs['source-language'] if doc_attrs.key? 'source-language'
818
847
  else
819
848
  attributes['language'] = language
820
849
  end
821
- if (attributes.key? 'linenums-option') || (doc_attrs.key? 'source-linenums-option')
850
+ if attributes['linenums-option'] || doc_attrs['source-linenums-option']
822
851
  attributes['linenums'] = ''
823
852
  end unless attributes.key? 'linenums'
824
853
  if doc_attrs.key? 'source-indent'
@@ -826,45 +855,53 @@ class Parser
826
855
  end unless attributes.key? 'indent'
827
856
  terminator = terminator.slice 0, 3
828
857
  block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
829
-
830
- when :pass
831
- block = build_block(block_context, :raw, terminator, parent, reader, attributes)
832
-
833
- when :stem, :latexmath, :asciimath
834
- attributes['style'] = STEM_TYPE_ALIASES[attributes[2] || doc_attrs['stem']] if block_context == :stem
835
- block = build_block(:stem, :raw, terminator, parent, reader, attributes)
836
-
837
- when :open, :sidebar
838
- block = build_block(block_context, :compound, terminator, parent, reader, attributes)
839
-
840
858
  when :table
841
859
  block_cursor = reader.cursor
842
- block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true, :context => :table, :cursor => :at_mark), block_cursor
860
+ block_reader = Reader.new reader.read_lines_until(terminator: terminator, skip_line_comments: true, context: :table, cursor: :at_mark), block_cursor
843
861
  # NOTE it's very rare that format is set when using a format hint char, so short-circuit
844
862
  unless terminator.start_with? '|', '!'
845
863
  # NOTE infer dsv once all other format hint chars are ruled out
846
864
  attributes['format'] ||= (terminator.start_with? ',') ? 'csv' : 'dsv'
847
865
  end
848
866
  block = parse_table(block_reader, parent, attributes)
849
-
867
+ when :sidebar
868
+ block = build_block(block_context, :compound, terminator, parent, reader, attributes)
869
+ when :admonition
870
+ attributes['name'] = admonition_name = style.downcase
871
+ attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
872
+ block = build_block(block_context, :compound, terminator, parent, reader, attributes)
873
+ when :open, :abstract, :partintro
874
+ block = build_block(:open, :compound, terminator, parent, reader, attributes)
875
+ when :literal
876
+ block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
877
+ when :example
878
+ attributes['caption'] = '' if attributes['collapsible-option']
879
+ block = build_block(block_context, :compound, terminator, parent, reader, attributes)
850
880
  when :quote, :verse
851
881
  AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
852
882
  block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes)
853
-
883
+ when :stem, :latexmath, :asciimath
884
+ attributes['style'] = STEM_TYPE_ALIASES[attributes[2] || doc_attrs['stem']] if block_context == :stem
885
+ block = build_block(:stem, :raw, terminator, parent, reader, attributes)
886
+ when :pass
887
+ block = build_block(block_context, :raw, terminator, parent, reader, attributes)
888
+ when :comment
889
+ build_block(block_context, :skip, terminator, parent, reader, attributes)
890
+ attributes.clear
891
+ return
854
892
  else
855
- if block_extensions && (extension = extensions.registered_for_block?(block_context, cloaked_context))
856
- if (content_model = extension.config[:content_model]) != :skip
857
- if !(pos_attrs = extension.config[:pos_attrs] || []).empty?
858
- AttributeList.rekey(attributes, [nil].concat(pos_attrs))
893
+ if block_extensions && (extension = extensions.registered_for_block? block_context, cloaked_context)
894
+ unless (content_model = (ext_config = extension.config)[:content_model]) == :skip
895
+ unless (positional_attrs = ext_config[:positional_attrs] || ext_config[:pos_attrs]).nil_or_empty?
896
+ AttributeList.rekey(attributes, [nil] + positional_attrs)
859
897
  end
860
- if (default_attrs = extension.config[:default_attrs])
898
+ if (default_attrs = ext_config[:default_attrs])
861
899
  default_attrs.each {|k, v| attributes[k] ||= v }
862
900
  end
863
901
  # QUESTION should we clone the extension for each cloaked context and set in config?
864
902
  attributes['cloaked-context'] = cloaked_context
865
903
  end
866
- block = build_block block_context, content_model, terminator, parent, reader, attributes, :extension => extension
867
- unless block
904
+ unless (block = build_block block_context, content_model, terminator, parent, reader, attributes, extension: extension)
868
905
  attributes.clear
869
906
  return
870
907
  end
@@ -877,19 +914,24 @@ class Parser
877
914
 
878
915
  # FIXME we've got to clean this up, it's horrible!
879
916
  block.source_location = reader.cursor_at_mark if document.sourcemap
880
- # FIXME title should be assigned when block is constructed
881
- block.title = attributes.delete 'title' if attributes.key? 'title'
917
+ # FIXME title and caption should be assigned when block is constructed (though we need to handle all cases)
918
+ if attributes['title']
919
+ block.title = block_title = attributes.delete 'title'
920
+ block.assign_caption attributes.delete 'caption' if CAPTION_ATTRIBUTE_NAMES[block.context]
921
+ end
882
922
  # TODO eventually remove the style attribute from the attributes hash
883
923
  #block.style = attributes.delete 'style'
884
924
  block.style = attributes['style']
885
- if (block_id = (block.id ||= attributes['id']))
886
- unless document.register :refs, [block_id, block, attributes['reftext'] || (block.title? ? block.title : nil)]
887
- logger.warn message_with_context %(id assigned to block already in use: #{block_id}), :source_location => reader.cursor_at_mark
925
+ if (block_id = block.id || (block.id = attributes['id']))
926
+ # convert title to resolve attributes while in scope
927
+ block.title if block_title ? (block_title.include? ATTR_REF_HEAD) : block.title?
928
+ unless document.register :refs, [block_id, block]
929
+ logger.warn message_with_context %(id assigned to block already in use: #{block_id}), source_location: reader.cursor_at_mark
888
930
  end
889
931
  end
890
932
  # FIXME remove the need for this update!
891
- block.attributes.update(attributes) unless attributes.empty?
892
- block.lock_in_subs
933
+ block.update_attributes attributes unless attributes.empty?
934
+ block.commit_subs
893
935
 
894
936
  #if doc_attrs.key? :pending_attribute_entries
895
937
  # doc_attrs.delete(:pending_attribute_entries).each do |entry|
@@ -915,74 +957,44 @@ class Parser
915
957
  reader.read_lines_until opts, &break_condition
916
958
  end
917
959
 
918
- # Public: Determines whether this line is the start of any of the delimited blocks
960
+ # Public: Determines whether this line is the start of a known delimited block.
919
961
  #
920
- # returns the match data if this line is the first line of a delimited block or nil if not
921
- def self.is_delimited_block? line, return_match_data = false
962
+ # Returns the BlockMatchData (if return_match_data is true) or true (if return_match_data is false) if this line is
963
+ # the start of a delimited block, otherwise nothing.
964
+ def self.is_delimited_block? line, return_match_data = nil
922
965
  # highly optimized for best performance
923
- return unless (line_len = line.length) > 1 && DELIMITED_BLOCK_HEADS.include?(line.slice 0, 2)
924
- # catches open block
966
+ return unless (line_len = line.length) > 1 && DELIMITED_BLOCK_HEADS[line.slice 0, 2]
967
+ # open block
925
968
  if line_len == 2
926
969
  tip = line
927
- tl = 2
970
+ tip_len = 2
928
971
  else
929
- # catches all other delimited blocks, including fenced code
930
- if line_len <= 4
972
+ # all other delimited blocks, including fenced code
973
+ if line_len < 5
931
974
  tip = line
932
- tl = line_len
975
+ tip_len = line_len
933
976
  else
934
- tip = line.slice 0, 4
935
- tl = 4
977
+ tip = line.slice 0, (tip_len = 4)
936
978
  end
937
-
938
979
  # special case for fenced code blocks
939
- # REVIEW review this logic
940
- fenced_code = false
941
- if Compliance.markdown_syntax
942
- tip_3 = (tl == 4 ? tip.chop : tip)
943
- if tip_3 == '```'
944
- if tl == 4 && tip.end_with?('`')
980
+ if Compliance.markdown_syntax && (tip.start_with? '`')
981
+ if tip_len == 4
982
+ if tip == '````' || (tip = tip.chop) != '```'
945
983
  return
946
984
  end
947
- tip = tip_3
948
- tl = 3
949
- fenced_code = true
985
+ line = tip
986
+ line_len = tip_len = 3
987
+ elsif tip != '```'
988
+ return
950
989
  end
990
+ elsif tip_len == 3
991
+ return
951
992
  end
952
-
953
- # short circuit if not a fenced code block
954
- return if tl == 3 && !fenced_code
955
993
  end
956
-
957
- if DELIMITED_BLOCKS.key? tip
958
- # tip is the full line when delimiter is minimum length
959
- if tl < 4 || tl == line_len
960
- if return_match_data
961
- context, masq = DELIMITED_BLOCKS[tip]
962
- BlockMatchData.new(context, masq, tip, tip)
963
- else
964
- true
965
- end
966
- elsif %(#{tip}#{tip.slice(-1, 1) * (line_len - tl)}) == line
967
- if return_match_data
968
- context, masq = DELIMITED_BLOCKS[tip]
969
- BlockMatchData.new(context, masq, tip, line)
970
- else
971
- true
972
- end
973
- # only enable if/when we decide to support non-congruent block delimiters
974
- #elsif (match = BlockDelimiterRx.match(line))
975
- # if return_match_data
976
- # context, masq = DELIMITED_BLOCKS[tip]
977
- # BlockMatchData.new(context, masq, tip, match[0])
978
- # else
979
- # true
980
- # end
981
- else
982
- nil
983
- end
984
- else
985
- nil
994
+ # NOTE line matches the tip when delimiter is minimum length or fenced code
995
+ context, masq = DELIMITED_BLOCKS[tip]
996
+ if context && (line_len == tip_len || (uniform? (line.slice 1, line_len), DELIMITED_BLOCK_TAILS[tip], (line_len - 1)))
997
+ return_match_data ? (BlockMatchData.new context, masq, tip, line) : true
986
998
  end
987
999
  end
988
1000
 
@@ -990,9 +1002,10 @@ class Parser
990
1002
  # if terminator is false, that means the all the lines in the reader should be parsed
991
1003
  # NOTE could invoke filter in here, before and after parsing
992
1004
  def self.build_block(block_context, content_model, terminator, parent, reader, attributes, options = {})
993
- if content_model == :skip
1005
+ case content_model
1006
+ when :skip
994
1007
  skip_processing, parse_as_content_model = true, :simple
995
- elsif content_model == :raw
1008
+ when :raw
996
1009
  skip_processing, parse_as_content_model = false, :simple
997
1010
  else
998
1011
  skip_processing, parse_as_content_model = false, content_model
@@ -1000,16 +1013,16 @@ class Parser
1000
1013
 
1001
1014
  if terminator.nil?
1002
1015
  if parse_as_content_model == :verbatim
1003
- lines = reader.read_lines_until :break_on_blank_lines => true, :break_on_list_continuation => true
1016
+ lines = reader.read_lines_until break_on_blank_lines: true, break_on_list_continuation: true
1004
1017
  else
1005
1018
  content_model = :simple if content_model == :compound
1006
1019
  # TODO we could also skip processing if we're able to detect reader is a BlockReader
1007
- lines = read_paragraph_lines reader, false, :skip_line_comments => true, :skip_processing => skip_processing
1020
+ lines = read_paragraph_lines reader, false, skip_line_comments: true, skip_processing: skip_processing
1008
1021
  # QUESTION check for empty lines after grabbing lines for simple content model?
1009
1022
  end
1010
1023
  block_reader = nil
1011
1024
  elsif parse_as_content_model != :compound
1012
- lines = reader.read_lines_until :terminator => terminator, :skip_processing => skip_processing, :context => block_context, :cursor => :at_mark
1025
+ lines = reader.read_lines_until terminator: terminator, skip_processing: skip_processing, context: block_context, cursor: :at_mark
1013
1026
  block_reader = nil
1014
1027
  # terminator is false when reader has already been prepared
1015
1028
  elsif terminator == false
@@ -1018,16 +1031,18 @@ class Parser
1018
1031
  else
1019
1032
  lines = nil
1020
1033
  block_cursor = reader.cursor
1021
- block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing, :context => block_context, :cursor => :at_mark), block_cursor
1034
+ block_reader = Reader.new reader.read_lines_until(terminator: terminator, skip_processing: skip_processing, context: block_context, cursor: :at_mark), block_cursor
1022
1035
  end
1023
1036
 
1024
- if content_model == :verbatim
1037
+ case content_model
1038
+ when :verbatim
1039
+ tab_size = (attributes['tabsize'] || parent.document.attributes['tabsize']).to_i
1025
1040
  if (indent = attributes['indent'])
1026
- adjust_indentation! lines, indent, (attributes['tabsize'] || parent.document.attributes['tabsize'])
1027
- elsif (tab_size = (attributes['tabsize'] || parent.document.attributes['tabsize']).to_i) > 0
1028
- adjust_indentation! lines, nil, tab_size
1041
+ adjust_indentation! lines, indent.to_i, tab_size
1042
+ elsif tab_size > 0
1043
+ adjust_indentation! lines, -1, tab_size
1029
1044
  end
1030
- elsif content_model == :skip
1045
+ when :skip
1031
1046
  # QUESTION should we still invoke process method if extension is specified?
1032
1047
  return
1033
1048
  end
@@ -1035,12 +1050,12 @@ class Parser
1035
1050
  if (extension = options[:extension])
1036
1051
  # QUESTION do we want to delete the style?
1037
1052
  attributes.delete('style')
1038
- if (block = extension.process_method[parent, block_reader || (Reader.new lines), attributes.dup])
1053
+ if (block = extension.process_method[parent, block_reader || (Reader.new lines), attributes.merge]) && block != parent
1039
1054
  attributes.replace block.attributes
1040
1055
  # FIXME if the content model is set to compound, but we only have simple in this context, then
1041
1056
  # forcefully set the content_model to simple to prevent parsing blocks from children
1042
1057
  # TODO document this behavior!!
1043
- if block.content_model == :compound && !(lines = block.lines).nil_or_empty?
1058
+ if block.content_model == :compound && Block === block && !(lines = block.lines).empty?
1044
1059
  content_model = :compound
1045
1060
  block_reader = Reader.new lines
1046
1061
  end
@@ -1048,14 +1063,7 @@ class Parser
1048
1063
  return
1049
1064
  end
1050
1065
  else
1051
- block = Block.new(parent, block_context, :content_model => content_model, :source => lines, :attributes => attributes)
1052
- end
1053
-
1054
- # QUESTION should we have an explicit map or can we rely on check for *-caption attribute?
1055
- if (attributes.key? 'title') && block.context != :admonition &&
1056
- (parent.document.attributes.key? %(#{block.context}-caption))
1057
- block.title = attributes.delete 'title'
1058
- block.assign_caption(attributes.delete 'caption')
1066
+ block = Block.new(parent, block_context, content_model: content_model, source: lines, attributes: attributes)
1059
1067
  end
1060
1068
 
1061
1069
  # reader is confined within boundaries of a delimited block, so look for
@@ -1075,9 +1083,13 @@ class Parser
1075
1083
  # parent - The parent Block to which to attach the parsed blocks
1076
1084
  #
1077
1085
  # Returns nothing.
1078
- def self.parse_blocks(reader, parent)
1079
- while ((block = next_block reader, parent) && parent.blocks << block) || reader.has_more_lines?
1086
+ def self.parse_blocks(reader, parent, attributes = nil)
1087
+ if attributes
1088
+ while ((block = next_block reader, parent, attributes.merge) && parent.blocks << block) || reader.has_more_lines?; end
1089
+ else
1090
+ while ((block = next_block reader, parent) && parent.blocks << block) || reader.has_more_lines?; end
1080
1091
  end
1092
+ nil
1081
1093
  end
1082
1094
 
1083
1095
  # Internal: Parse and construct an ordered or unordered list at the current position of the Reader
@@ -1088,10 +1100,11 @@ class Parser
1088
1100
  # style - The block style assigned to this list (optional, default: nil)
1089
1101
  #
1090
1102
  # Returns the Block encapsulating the parsed unordered or ordered list
1091
- def self.parse_list(reader, list_type, parent, style)
1092
- list_block = List.new(parent, list_type)
1103
+ def self.parse_list reader, list_type, parent, style
1104
+ list_block = List.new parent, list_type
1105
+ list_rx = ListRxMap[list_type]
1093
1106
 
1094
- while reader.has_more_lines? && (list_rx ||= ListRxMap[list_type]) =~ reader.peek_line
1107
+ while reader.has_more_lines? && list_rx =~ reader.peek_line
1095
1108
  # NOTE parse_list_item will stop at sibling item or end of list; never sees ancestor items
1096
1109
  if (list_item = parse_list_item reader, list_block, $~, $1, style)
1097
1110
  list_block.items << list_item
@@ -1112,13 +1125,11 @@ class Parser
1112
1125
  def self.catalog_callouts(text, document)
1113
1126
  found = false
1114
1127
  autonum = 0
1115
- text.scan(CalloutScanRx) {
1116
- # lead with assignments for Ruby 1.8.7 compat
1117
- captured, num = $&, $2
1118
- document.callouts.register num == '.' ? (autonum += 1).to_s : num unless captured.start_with? '\\'
1128
+ text.scan CalloutScanRx do
1129
+ document.callouts.register $2 == '.' ? (autonum += 1).to_s : $2 unless $&.start_with? '\\'
1119
1130
  # we have to mark as found even if it's escaped so it can be unescaped
1120
1131
  found = true
1121
- } if text.include? '<'
1132
+ end if text.include? '<'
1122
1133
  found
1123
1134
  end
1124
1135
 
@@ -1131,12 +1142,11 @@ class Parser
1131
1142
  # doc - The document to which the node belongs; computed from node if not specified
1132
1143
  #
1133
1144
  # Returns nothing
1134
- def self.catalog_inline_anchor id, reftext, node, location, doc = nil
1135
- doc ||= node.document
1145
+ def self.catalog_inline_anchor id, reftext, node, location, doc = node.document
1136
1146
  reftext = doc.sub_attributes reftext if reftext && (reftext.include? ATTR_REF_HEAD)
1137
- unless doc.register :refs, [id, (Inline.new node, :anchor, reftext, :type => :ref, :id => id), reftext]
1147
+ unless doc.register :refs, [id, (Inline.new node, :anchor, reftext, type: :ref, id: id)]
1138
1148
  location = location.cursor if Reader === location
1139
- logger.warn message_with_context %(id assigned to anchor already in use: #{id}), :source_location => location
1149
+ logger.warn message_with_context %(id assigned to anchor already in use: #{id}), source_location: location
1140
1150
  end
1141
1151
  nil
1142
1152
  end
@@ -1149,26 +1159,26 @@ class Parser
1149
1159
  #
1150
1160
  # Returns nothing
1151
1161
  def self.catalog_inline_anchors text, block, document, reader
1152
- text.scan(InlineAnchorScanRx) do
1153
- # alias match for Ruby 1.8.7 compat
1154
- m = $~
1162
+ text.scan InlineAnchorScanRx do
1155
1163
  if (id = $1)
1156
- if (reftext = $2)
1157
- next if (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1158
- end
1164
+ next if (reftext = $2) && (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1159
1165
  else
1160
1166
  id = $3
1161
1167
  if (reftext = $4)
1162
- reftext = reftext.gsub '\]', ']' if reftext.include? ']'
1163
- next if (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1168
+ if reftext.include? ']'
1169
+ reftext = reftext.gsub '\]', ']'
1170
+ reftext = document.sub_attributes reftext if reftext.include? ATTR_REF_HEAD
1171
+ elsif (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
1172
+ next
1173
+ end
1164
1174
  end
1165
1175
  end
1166
- unless document.register :refs, [id, (Inline.new block, :anchor, reftext, :type => :ref, :id => id), reftext]
1176
+ unless document.register :refs, [id, (Inline.new block, :anchor, reftext, type: :ref, id: id)]
1167
1177
  location = reader.cursor_at_mark
1168
- if (offset = (m.pre_match.count LF) + ((m[0].start_with? LF) ? 1 : 0)) > 0
1178
+ if (offset = ($`.count LF) + (($&.start_with? LF) ? 1 : 0)) > 0
1169
1179
  (location = location.dup).advance offset
1170
1180
  end
1171
- logger.warn message_with_context %(id assigned to anchor already in use: #{id}), :source_location => location
1181
+ logger.warn message_with_context %(id assigned to anchor already in use: #{id}), source_location: location
1172
1182
  end
1173
1183
  end if (text.include? '[[') || (text.include? 'or:')
1174
1184
  nil
@@ -1184,8 +1194,8 @@ class Parser
1184
1194
  # Returns nothing
1185
1195
  def self.catalog_inline_biblio_anchor id, reftext, node, reader
1186
1196
  # QUESTION should we sub attributes in reftext (like with regular anchors)?
1187
- unless node.document.register :refs, [id, (Inline.new node, :anchor, (styled_reftext = %([#{reftext || id}])), :type => :bibref, :id => id), styled_reftext]
1188
- logger.warn message_with_context %(id assigned to bibliography anchor already in use: #{id}), :source_location => reader.cursor
1197
+ unless node.document.register :refs, [id, (Inline.new node, :anchor, reftext && %([#{reftext}]), type: :bibref, id: id)]
1198
+ logger.warn message_with_context %(id assigned to bibliography anchor already in use: #{id}), source_location: reader.cursor
1189
1199
  end
1190
1200
  nil
1191
1201
  end
@@ -1197,23 +1207,20 @@ class Parser
1197
1207
  # parent - The parent Block to which this description list belongs
1198
1208
  #
1199
1209
  # Returns the Block encapsulating the parsed description list
1200
- def self.parse_description_list(reader, match, parent)
1201
- list_block = List.new(parent, :dlist)
1202
- previous_pair = nil
1203
- # allows us to capture until we find a description item
1204
- # that uses the same delimiter (::, :::, :::: or ;;)
1210
+ def self.parse_description_list reader, match, parent
1211
+ list_block = List.new parent, :dlist
1212
+ # detects a description list item that uses the same delimiter (::, :::, :::: or ;;)
1205
1213
  sibling_pattern = DescriptionListSiblingRx[match[2]]
1214
+ list_block.items << (current_pair = parse_list_item reader, list_block, match, sibling_pattern)
1206
1215
 
1207
- # NOTE skip the match on the first time through as we've already done it (emulates begin...while)
1208
- while match || (reader.has_more_lines? && (match = sibling_pattern.match(reader.peek_line)))
1209
- term, item = parse_list_item(reader, list_block, match, sibling_pattern)
1210
- if previous_pair && !previous_pair[1]
1211
- previous_pair[0] << term
1212
- previous_pair[1] = item
1216
+ while reader.has_more_lines? && sibling_pattern =~ reader.peek_line
1217
+ next_pair = parse_list_item reader, list_block, $~, sibling_pattern
1218
+ if current_pair[1]
1219
+ list_block.items << (current_pair = next_pair)
1213
1220
  else
1214
- list_block.items << (previous_pair = [[term], item])
1221
+ current_pair[0] << next_pair[0][0]
1222
+ current_pair[1] = next_pair[1]
1215
1223
  end
1216
- match = nil
1217
1224
  end
1218
1225
 
1219
1226
  list_block
@@ -1239,12 +1246,12 @@ class Parser
1239
1246
  end
1240
1247
  # might want to move this check to a validate method
1241
1248
  unless num == next_index.to_s
1242
- logger.warn message_with_context %(callout list item index: expected #{next_index}, got #{num}), :source_location => reader.cursor_at_mark
1249
+ logger.warn message_with_context %(callout list item index: expected #{next_index}, got #{num}), source_location: reader.cursor_at_mark
1243
1250
  end
1244
1251
  if (list_item = parse_list_item reader, list_block, match, '<1>')
1245
1252
  list_block.items << list_item
1246
1253
  if (coids = callouts.callout_ids list_block.items.size).empty?
1247
- logger.warn message_with_context %(no callout found for <#{list_block.items.size}>), :source_location => reader.cursor_at_mark
1254
+ logger.warn message_with_context %(no callout found for <#{list_block.items.size}>), source_location: reader.cursor_at_mark
1248
1255
  else
1249
1256
  list_item.attributes['coids'] = coids
1250
1257
  end
@@ -1274,7 +1281,7 @@ class Parser
1274
1281
  # marker pattern.
1275
1282
  # style - The block style assigned to this list (optional, default: nil)
1276
1283
  #
1277
- # Returns the next ListItem or ListItem pair (description list) for the parent list Block.
1284
+ # Returns the next ListItem or [[ListItem], ListItem] pair (description list) for the parent list Block.
1278
1285
  def self.parse_list_item(reader, list_block, match, sibling_trait, style = nil)
1279
1286
  if (list_type = list_block.context) == :dlist
1280
1287
  dlist = true
@@ -1296,7 +1303,8 @@ class Parser
1296
1303
  has_text = true
1297
1304
  list_item = ListItem.new(list_block, (item_text = match[2]))
1298
1305
  list_item.source_location = reader.cursor if list_block.document.sourcemap
1299
- if list_type == :ulist
1306
+ case list_type
1307
+ when :ulist
1300
1308
  list_item.marker = sibling_trait
1301
1309
  if item_text.start_with?('[')
1302
1310
  if style && style == 'bibliography'
@@ -1308,27 +1316,28 @@ class Parser
1308
1316
  catalog_inline_anchor $1, $2, list_item, reader
1309
1317
  end
1310
1318
  elsif item_text.start_with?('[ ] ', '[x] ', '[*] ')
1311
- # FIXME next_block wipes out update to options attribute
1312
- #list_block.set_option 'checklist' unless list_block.attributes['checklist-option']
1313
- list_block.attributes['checklist-option'] = ''
1319
+ list_block.set_option 'checklist'
1314
1320
  list_item.attributes['checkbox'] = ''
1315
1321
  list_item.attributes['checked'] = '' unless item_text.start_with? '[ '
1316
1322
  list_item.text = item_text.slice(4, item_text.length)
1317
1323
  end
1318
1324
  end
1319
- elsif list_type == :olist
1325
+ when :olist
1320
1326
  sibling_trait, implicit_style = resolve_ordered_list_marker(sibling_trait, (ordinal = list_block.items.size), true, reader)
1321
1327
  list_item.marker = sibling_trait
1322
1328
  if ordinal == 0 && !style
1323
1329
  # using list level makes more sense, but we don't track it
1324
- # basing style on marker level is compliant with AsciiDoc Python
1325
- list_block.style = implicit_style || ((ORDERED_LIST_STYLES[sibling_trait.length - 1] || 'arabic').to_s)
1330
+ # basing style on marker level is compliant with AsciiDoc.py
1331
+ list_block.style = implicit_style || (ORDERED_LIST_STYLES[sibling_trait.length - 1] || 'arabic').to_s
1326
1332
  end
1327
1333
  if item_text.start_with?('[[') && LeadingInlineAnchorRx =~ item_text
1328
1334
  catalog_inline_anchor $1, $2, list_item, reader
1329
1335
  end
1330
1336
  else # :colist
1331
1337
  list_item.marker = sibling_trait
1338
+ if item_text.start_with?('[[') && LeadingInlineAnchorRx =~ item_text
1339
+ catalog_inline_anchor $1, $2, list_item, reader
1340
+ end
1332
1341
  end
1333
1342
  end
1334
1343
 
@@ -1342,38 +1351,28 @@ class Parser
1342
1351
  comment_lines = list_item_reader.skip_line_comments
1343
1352
  if (subsequent_line = list_item_reader.peek_line)
1344
1353
  list_item_reader.unshift_lines comment_lines unless comment_lines.empty?
1345
- if (continuation_connects_first_block = subsequent_line.empty?)
1346
- content_adjacent = false
1347
- else
1354
+ unless subsequent_line.empty?
1348
1355
  content_adjacent = true
1349
1356
  # treat lines as paragraph text if continuation does not connect first block (i.e., has_text = nil)
1350
1357
  has_text = nil unless dlist
1351
1358
  end
1352
- else
1353
- # NOTE we have no use for any trailing comment lines we might have found
1354
- continuation_connects_first_block = false
1355
- content_adjacent = false
1356
1359
  end
1357
1360
 
1358
1361
  # reader is confined to boundaries of list, which means only blocks will be found (no sections)
1359
- if (block = next_block(list_item_reader, list_item, {}, :text => !has_text))
1362
+ if (block = next_block(list_item_reader, list_item, {}, text_only: has_text ? nil : true, list_type: list_type))
1360
1363
  list_item.blocks << block
1361
1364
  end
1362
1365
 
1363
1366
  while list_item_reader.has_more_lines?
1364
- if (block = next_block(list_item_reader, list_item))
1367
+ if (block = next_block(list_item_reader, list_item, {}, list_type: list_type))
1365
1368
  list_item.blocks << block
1366
1369
  end
1367
1370
  end
1368
1371
 
1369
- list_item.fold_first(continuation_connects_first_block, content_adjacent)
1372
+ list_item.fold_first if content_adjacent && (first_block = list_item.blocks[0]) && first_block.context == :paragraph
1370
1373
  end
1371
1374
 
1372
- if dlist
1373
- list_item.text? || list_item.blocks? ? [list_term, list_item] : [list_term]
1374
- else
1375
- list_item
1376
- end
1375
+ dlist ? [[list_term], (list_item.text? || list_item.blocks? ? list_item : nil)] : list_item
1377
1376
  end
1378
1377
 
1379
1378
  # Internal: Collect the lines belonging to the current list item, navigating
@@ -1407,6 +1406,8 @@ class Parser
1407
1406
  # it gets associated with the outermost block
1408
1407
  detached_continuation = nil
1409
1408
 
1409
+ dlist = list_type == :dlist
1410
+
1410
1411
  while reader.has_more_lines?
1411
1412
  this_line = reader.read_line
1412
1413
 
@@ -1443,7 +1444,7 @@ class Parser
1443
1444
  buffer << this_line
1444
1445
  # grab all the lines in the block, leaving the delimiters in place
1445
1446
  # we're being more strict here about the terminator, but I think that's a good thing
1446
- buffer.concat reader.read_lines_until(:terminator => match.terminator, :read_last_line => true, :context => nil)
1447
+ buffer.concat reader.read_lines_until(terminator: match.terminator, read_last_line: true, context: nil)
1447
1448
  continuation = :inactive
1448
1449
  else
1449
1450
  break
@@ -1451,118 +1452,110 @@ class Parser
1451
1452
  # technically BlockAttributeLineRx only breaks if ensuing line is not a list item
1452
1453
  # which really means BlockAttributeLineRx only breaks if it's acting as a block delimiter
1453
1454
  # FIXME to be AsciiDoc compliant, we shouldn't break if style in attribute line is "literal" (i.e., [literal])
1454
- elsif list_type == :dlist && continuation != :active && (BlockAttributeLineRx.match? this_line)
1455
+ elsif dlist && continuation != :active && (BlockAttributeLineRx.match? this_line)
1455
1456
  break
1456
- else
1457
- if continuation == :active && !this_line.empty?
1458
- # literal paragraphs have special considerations (and this is one of
1459
- # two entry points into one)
1460
- # if we don't process it as a whole, then a line in it that looks like a
1461
- # list item will throw off the exit from it
1462
- if LiteralParagraphRx.match? this_line
1463
- reader.unshift_line this_line
1464
- buffer.concat reader.read_lines_until(
1465
- :preserve_last_line => true,
1466
- :break_on_blank_lines => true,
1467
- :break_on_list_continuation => true) {|line|
1468
- # we may be in an indented list disguised as a literal paragraph
1469
- # so we need to make sure we don't slurp up a legitimate sibling
1470
- list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
1471
- }
1472
- continuation = :inactive
1473
- # let block metadata play out until we find the block
1474
- elsif (BlockTitleRx.match? this_line) || (BlockAttributeLineRx.match? this_line) || (AttributeEntryRx.match? this_line)
1475
- buffer << this_line
1457
+ elsif continuation == :active && !this_line.empty?
1458
+ # literal paragraphs have special considerations (and this is one of
1459
+ # two entry points into one)
1460
+ # if we don't process it as a whole, then a line in it that looks like a
1461
+ # list item will throw off the exit from it
1462
+ if LiteralParagraphRx.match? this_line
1463
+ reader.unshift_line this_line
1464
+ if dlist
1465
+ # we may be in an indented list disguised as a literal paragraph
1466
+ # so we need to make sure we don't slurp up a legitimate sibling
1467
+ buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true) {|line| is_sibling_list_item? line, list_type, sibling_trait }
1476
1468
  else
1477
- if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx].match? this_line }
1478
- within_nested_list = true
1479
- if nested_list_type == :dlist && $3.nil_or_empty?
1480
- # get greedy again
1481
- has_text = false
1482
- end
1483
- end
1484
- buffer << this_line
1485
- continuation = :inactive
1469
+ buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
1486
1470
  end
1487
- elsif prev_line && prev_line.empty?
1488
- # advance to the next line of content
1489
- if this_line.empty?
1490
- # stop reading if we reach eof
1491
- break unless (this_line = reader.skip_blank_lines && reader.read_line)
1492
- # stop reading if we hit a sibling list item
1493
- break if is_sibling_list_item? this_line, list_type, sibling_trait
1471
+ continuation = :inactive
1472
+ # let block metadata play out until we find the block
1473
+ elsif (BlockTitleRx.match? this_line) || (BlockAttributeLineRx.match? this_line) || (AttributeEntryRx.match? this_line)
1474
+ buffer << this_line
1475
+ else
1476
+ if (nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx].match? this_line })
1477
+ within_nested_list = true
1478
+ if nested_list_type == :dlist && $3.nil_or_empty?
1479
+ # get greedy again
1480
+ has_text = false
1481
+ end
1494
1482
  end
1483
+ buffer << this_line
1484
+ continuation = :inactive
1485
+ end
1486
+ elsif prev_line && prev_line.empty?
1487
+ # advance to the next line of content
1488
+ if this_line.empty?
1489
+ # stop reading if we reach eof
1490
+ break unless (this_line = reader.skip_blank_lines && reader.read_line)
1491
+ # stop reading if we hit a sibling list item
1492
+ break if is_sibling_list_item? this_line, list_type, sibling_trait
1493
+ end
1495
1494
 
1496
- if this_line == LIST_CONTINUATION
1497
- detached_continuation = buffer.size
1495
+ if this_line == LIST_CONTINUATION
1496
+ detached_continuation = buffer.size
1497
+ buffer << this_line
1498
+ elsif has_text # has_text only relevant for dlist, which is more greedy until it has text for an item; has_text is always true for all other lists
1499
+ # in this block, we have to see whether we stay in the list
1500
+ # TODO any way to combine this with the check after skipping blank lines?
1501
+ if is_sibling_list_item?(this_line, list_type, sibling_trait)
1502
+ break
1503
+ elsif (nested_list_type = NESTABLE_LIST_CONTEXTS.find {|ctx| ListRxMap[ctx] =~ this_line })
1498
1504
  buffer << this_line
1499
- else
1500
- # has_text is only relevant for dlist, which is more greedy until it has text for an item
1501
- # for all other lists, has_text is always true
1502
- # in this block, we have to see whether we stay in the list
1503
- if has_text
1504
- # TODO any way to combine this with the check after skipping blank lines?
1505
- if is_sibling_list_item?(this_line, list_type, sibling_trait)
1506
- break
1507
- elsif nested_list_type = NESTABLE_LIST_CONTEXTS.find {|ctx| ListRxMap[ctx] =~ this_line }
1508
- buffer << this_line
1509
- within_nested_list = true
1510
- if nested_list_type == :dlist && $3.nil_or_empty?
1511
- # get greedy again
1512
- has_text = false
1513
- end
1514
- # slurp up any literal paragraph offset by blank lines
1515
- # NOTE we have to check for indented list items first
1516
- elsif LiteralParagraphRx.match? this_line
1517
- reader.unshift_line this_line
1518
- buffer.concat reader.read_lines_until(
1519
- :preserve_last_line => true,
1520
- :break_on_blank_lines => true,
1521
- :break_on_list_continuation => true) {|line|
1522
- # we may be in an indented list disguised as a literal paragraph
1523
- # so we need to make sure we don't slurp up a legitimate sibling
1524
- list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
1525
- }
1526
- else
1527
- break
1528
- end
1529
- else # only dlist in need of item text, so slurp it up!
1530
- # pop the blank line so it's not interpretted as a list continuation
1531
- buffer.pop unless within_nested_list
1532
- buffer << this_line
1533
- has_text = true
1534
- end
1535
- end
1536
- else
1537
- has_text = true if !this_line.empty?
1538
- if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx] =~ this_line }
1539
1505
  within_nested_list = true
1540
1506
  if nested_list_type == :dlist && $3.nil_or_empty?
1541
1507
  # get greedy again
1542
1508
  has_text = false
1543
1509
  end
1510
+ # slurp up any literal paragraph offset by blank lines
1511
+ # NOTE we have to check for indented list items first
1512
+ elsif LiteralParagraphRx.match? this_line
1513
+ reader.unshift_line this_line
1514
+ if dlist
1515
+ # we may be in an indented list disguised as a literal paragraph
1516
+ # so we need to make sure we don't slurp up a legitimate sibling
1517
+ buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true) {|line| is_sibling_list_item? line, list_type, sibling_trait }
1518
+ else
1519
+ buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
1520
+ end
1521
+ else
1522
+ break
1544
1523
  end
1524
+ else # only dlist in need of item text, so slurp it up!
1525
+ # pop the blank line so it's not interpreted as a list continuation
1526
+ buffer.pop unless within_nested_list
1545
1527
  buffer << this_line
1528
+ has_text = true
1546
1529
  end
1530
+ else
1531
+ has_text = true unless this_line.empty?
1532
+ if (nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx] =~ this_line })
1533
+ within_nested_list = true
1534
+ if nested_list_type == :dlist && $3.nil_or_empty?
1535
+ # get greedy again
1536
+ has_text = false
1537
+ end
1538
+ end
1539
+ buffer << this_line
1547
1540
  end
1548
1541
  this_line = nil
1549
1542
  end
1550
1543
 
1551
1544
  reader.unshift_line this_line if this_line
1552
1545
 
1553
- if detached_continuation
1554
- buffer.delete_at detached_continuation
1555
- end
1556
-
1557
- # strip trailing blank lines to prevent empty blocks
1558
- buffer.pop while !buffer.empty? && buffer[-1].empty?
1546
+ buffer[detached_continuation] = '' if detached_continuation
1559
1547
 
1560
- # We do need to replace the optional trailing continuation
1561
- # a blank line would have served the same purpose in the document
1562
- buffer.pop if !buffer.empty? && buffer[-1] == LIST_CONTINUATION
1563
-
1564
- #warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.join LF}<BUFFER"
1565
- #warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.inspect}<BUFFER"
1548
+ until buffer.empty?
1549
+ # strip trailing blank lines to prevent empty blocks
1550
+ if (last_line = buffer[-1]).empty?
1551
+ buffer.pop
1552
+ else
1553
+ # drop optional trailing continuation
1554
+ # (a blank line would have served the same purpose in the document)
1555
+ buffer.pop if last_line == LIST_CONTINUATION
1556
+ break
1557
+ end
1558
+ end
1566
1559
 
1567
1560
  buffer
1568
1561
  end
@@ -1584,19 +1577,15 @@ class Parser
1584
1577
  sect_style = attributes[1]
1585
1578
  sect_id, sect_reftext, sect_title, sect_level, sect_atx = parse_section_title reader, document, attributes['id']
1586
1579
 
1587
- if sect_reftext
1588
- attributes['reftext'] = sect_reftext
1589
- else
1590
- sect_reftext = attributes['reftext']
1591
- end
1592
-
1593
1580
  if sect_style
1594
1581
  if book && sect_style == 'abstract'
1595
1582
  sect_name, sect_level = 'chapter', 1
1583
+ elsif (sect_style.start_with? 'sect') && (SectionLevelStyleRx.match? sect_style)
1584
+ sect_name = 'section'
1596
1585
  else
1597
1586
  sect_name, sect_special = sect_style, true
1598
1587
  sect_level = 1 if sect_level == 0
1599
- sect_numbered = sect_style == 'appendix'
1588
+ sect_numbered = sect_name == 'appendix'
1600
1589
  end
1601
1590
  elsif book
1602
1591
  sect_name = sect_level == 0 ? 'part' : (sect_level > 1 ? 'section' : 'chapter')
@@ -1606,6 +1595,7 @@ class Parser
1606
1595
  sect_name = 'section'
1607
1596
  end
1608
1597
 
1598
+ attributes['reftext'] = sect_reftext if sect_reftext
1609
1599
  section = Section.new parent, sect_level
1610
1600
  section.id, section.title, section.sectname, section.source_location = sect_id, sect_title, sect_name, source_location
1611
1601
  if sect_special
@@ -1613,7 +1603,7 @@ class Parser
1613
1603
  if sect_numbered
1614
1604
  section.numbered = true
1615
1605
  elsif document.attributes['sectnums'] == 'all'
1616
- section.numbered = book && sect_level == 1 ? :chapter : true
1606
+ section.numbered = (book && sect_level == 1 ? :chapter : true)
1617
1607
  end
1618
1608
  elsif document.attributes['sectnums'] && sect_level > 0
1619
1609
  # NOTE a special section here is guaranteed to be nested in another section
@@ -1623,9 +1613,11 @@ class Parser
1623
1613
  end
1624
1614
 
1625
1615
  # generate an ID if one was not embedded or specified as anchor above section title
1626
- if (id = section.id ||= ((document.attributes.key? 'sectids') ? (Section.generate_id section.title, document) : nil))
1627
- unless document.register :refs, [id, section, sect_reftext || section.title]
1628
- logger.warn message_with_context %(id assigned to section already in use: #{id}), :source_location => (reader.cursor_at_line reader.lineno - (sect_atx ? 1 : 2))
1616
+ if (id = section.id || (section.id = (document.attributes.key? 'sectids') ? (generated_id = Section.generate_id section.title, document) : nil))
1617
+ # convert title to resolve attributes while in scope
1618
+ section.title unless generated_id || !(sect_title.include? ATTR_REF_HEAD)
1619
+ unless document.register :refs, [id, section]
1620
+ logger.warn message_with_context %(id assigned to section already in use: #{id}), source_location: (reader.cursor_at_line reader.lineno - (sect_atx ? 1 : 2))
1629
1621
  end
1630
1622
  end
1631
1623
 
@@ -1642,9 +1634,8 @@ class Parser
1642
1634
  #
1643
1635
  # Returns the Integer section level if the Reader is positioned at a section title or nil otherwise
1644
1636
  def self.is_next_line_section?(reader, attributes)
1645
- if (style = attributes[1]) && (style == 'discrete' || style == 'float')
1646
- return
1647
- elsif Compliance.underline_style_section_titles
1637
+ return if (style = attributes[1]) && (style == 'discrete' || style == 'float')
1638
+ if Compliance.underline_style_section_titles
1648
1639
  next_lines = reader.peek_lines 2, style && style == 'comment'
1649
1640
  is_section_title?(next_lines[0] || '', next_lines[1])
1650
1641
  else
@@ -1698,9 +1689,8 @@ class Parser
1698
1689
  #
1699
1690
  # Returns the [Integer] section level if these lines are an setext section title, otherwise nothing.
1700
1691
  def self.setext_section_title? line1, line2
1701
- if (level = SETEXT_SECTION_LEVELS[line2_ch1 = line2.chr]) &&
1702
- line2_ch1 * (line2_len = line2.length) == line2 && SetextSectionTitleRx.match?(line1) &&
1703
- (line_length(line1) - line2_len).abs < 2
1692
+ if (level = SETEXT_SECTION_LEVELS[line2_ch0 = line2.chr]) && (uniform? line2, line2_ch0, (line2_len = line2.length)) &&
1693
+ (SetextSectionTitleRx.match? line1) && (line1.length - line2_len).abs < 2
1704
1694
  level
1705
1695
  end
1706
1696
  end
@@ -1760,9 +1750,8 @@ class Parser
1760
1750
  sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
1761
1751
  end unless sect_id
1762
1752
  elsif Compliance.underline_style_section_titles && (line2 = reader.peek_line(true)) &&
1763
- (sect_level = SETEXT_SECTION_LEVELS[line2_ch1 = line2.chr]) &&
1764
- line2_ch1 * (line2_len = line2.length) == line2 && (sect_title = SetextSectionTitleRx =~ line1 && $1) &&
1765
- (line_length(line1) - line2_len).abs < 2
1753
+ (sect_level = SETEXT_SECTION_LEVELS[line2_ch0 = line2.chr]) && (uniform? line2, line2_ch0, (line2_len = line2.length)) &&
1754
+ (sect_title = SetextSectionTitleRx =~ line1 && $1) && (line1.length - line2_len).abs < 2
1766
1755
  atx = false
1767
1756
  if sect_title.end_with?(']]') && InlineSectionAnchorRx =~ sect_title && !$1 # escaped
1768
1757
  sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
@@ -1771,23 +1760,11 @@ class Parser
1771
1760
  else
1772
1761
  raise %(Unrecognized section at #{reader.cursor_at_prev_line})
1773
1762
  end
1774
- sect_level += document.attr('leveloffset').to_i if document.attr?('leveloffset')
1775
- [sect_id, sect_reftext, sect_title, sect_level, atx]
1776
- end
1777
-
1778
- # Public: Calculate the number of unicode characters in the line, excluding the endline
1779
- #
1780
- # line - the String to calculate
1781
- #
1782
- # returns the number of unicode characters in the line
1783
- if FORCE_UNICODE_LINE_LENGTH
1784
- def self.line_length(line)
1785
- line.scan(UnicodeCharScanRx).size
1786
- end
1787
- else
1788
- def self.line_length(line)
1789
- line.length
1763
+ if document.attr? 'leveloffset'
1764
+ sect_level += (document.attr 'leveloffset').to_i
1765
+ sect_level = 0 if sect_level < 0
1790
1766
  end
1767
+ [sect_id, sect_reftext, sect_title, sect_level, atx]
1791
1768
  end
1792
1769
 
1793
1770
  # Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info).
@@ -1801,42 +1778,34 @@ class Parser
1801
1778
  # Examples
1802
1779
  #
1803
1780
  # data = ["Author Name <author@example.org>\n", "v1.0, 2012-12-21: Coincide w/ end of world.\n"]
1804
- # parse_header_metadata(Reader.new data, nil, :normalize => true)
1805
- # # => {'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org',
1806
- # # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.'}
1807
- def self.parse_header_metadata(reader, document = nil)
1781
+ # parse_header_metadata(Reader.new data, nil, normalize: true)
1782
+ # # => { 'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org',
1783
+ # # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.' }
1784
+ def self.parse_header_metadata reader, document = nil, retrieve = true
1808
1785
  doc_attrs = document && document.attributes
1809
1786
  # NOTE this will discard any comment lines, but not skip blank lines
1810
1787
  process_attribute_entries reader, document
1811
1788
 
1812
- metadata, implicit_author, implicit_authorinitials = implicit_authors = {}, nil, nil
1813
-
1814
1789
  if reader.has_more_lines? && !reader.next_line_empty?
1815
- unless (author_metadata = process_authors reader.read_line).empty?
1816
- if document
1817
- # apply header subs and assign to document
1818
- author_metadata.each do |key, val|
1819
- unless doc_attrs.key? key
1820
- doc_attrs[key] = ::String === val ? (document.apply_header_subs val) : val
1821
- end
1822
- end
1823
-
1824
- implicit_author = doc_attrs['author']
1825
- implicit_authorinitials = doc_attrs['authorinitials']
1826
- implicit_authors = doc_attrs['authors']
1790
+ authorcount = (implicit_author_metadata = process_authors reader.read_line).delete 'authorcount'
1791
+ if document && (doc_attrs['authorcount'] = authorcount) > 0
1792
+ implicit_author_metadata.each do |key, val|
1793
+ # apply header subs and assign to document; attributes substitution only relevant for email
1794
+ doc_attrs[key] = document.apply_header_subs val unless doc_attrs.key? key
1827
1795
  end
1828
-
1829
- metadata = author_metadata
1796
+ implicit_author = doc_attrs['author']
1797
+ implicit_authorinitials = doc_attrs['authorinitials']
1798
+ implicit_authors = doc_attrs['authors']
1830
1799
  end
1800
+ implicit_author_metadata['authorcount'] = authorcount
1831
1801
 
1832
1802
  # NOTE this will discard any comment lines, but not skip blank lines
1833
1803
  process_attribute_entries reader, document
1834
1804
 
1835
- rev_metadata = {}
1836
-
1837
1805
  if reader.has_more_lines? && !reader.next_line_empty?
1838
1806
  rev_line = reader.read_line
1839
- if (match = RevisionInfoLineRx.match(rev_line))
1807
+ if (match = RevisionInfoLineRx.match rev_line)
1808
+ rev_metadata = {}
1840
1809
  rev_metadata['revnumber'] = match[1].rstrip if match[1]
1841
1810
  unless (component = match[2].strip).empty?
1842
1811
  # version must begin with 'v' if date is absent
@@ -1847,31 +1816,24 @@ class Parser
1847
1816
  end
1848
1817
  end
1849
1818
  rev_metadata['revremark'] = match[3].rstrip if match[3]
1819
+ if document && !rev_metadata.empty?
1820
+ # apply header subs and assign to document
1821
+ rev_metadata.each do |key, val|
1822
+ doc_attrs[key] = document.apply_header_subs val unless doc_attrs.key? key
1823
+ end
1824
+ end
1850
1825
  else
1851
1826
  # throw it back
1852
1827
  reader.unshift_line rev_line
1853
1828
  end
1854
1829
  end
1855
1830
 
1856
- unless rev_metadata.empty?
1857
- if document
1858
- # apply header subs and assign to document
1859
- rev_metadata.each do |key, val|
1860
- unless doc_attrs.key? key
1861
- doc_attrs[key] = document.apply_header_subs val
1862
- end
1863
- end
1864
- end
1865
-
1866
- metadata.update rev_metadata
1867
- end
1868
-
1869
1831
  # NOTE this will discard any comment lines, but not skip blank lines
1870
1832
  process_attribute_entries reader, document
1871
1833
 
1872
1834
  reader.skip_blank_lines
1873
1835
  else
1874
- author_metadata = {}
1836
+ implicit_author_metadata = {}
1875
1837
  end
1876
1838
 
1877
1839
  # process author attribute entries that override (or stand in for) the implicit author line
@@ -1888,7 +1850,7 @@ class Parser
1888
1850
  while doc_attrs.key? author_key
1889
1851
  # only use indexed author attribute if value is different
1890
1852
  # leaves corner case if line matches with underscores converted to spaces; use double space to force
1891
- if (author_override = doc_attrs[author_key]) == author_metadata[author_key]
1853
+ if (author_override = doc_attrs[author_key]) == implicit_author_metadata[author_key]
1892
1854
  authors << nil
1893
1855
  sparse = true
1894
1856
  else
@@ -1900,23 +1862,26 @@ class Parser
1900
1862
  if explicit
1901
1863
  # rebuild implicit author names to reparse
1902
1864
  authors.each_with_index do |author, idx|
1903
- unless author
1904
- authors[idx] = [
1905
- author_metadata[%(firstname_#{name_idx = idx + 1})],
1906
- author_metadata[%(middlename_#{name_idx})],
1907
- author_metadata[%(lastname_#{name_idx})]
1908
- ].compact.map {|it| it.tr ' ', '_' }.join ' '
1909
- end
1865
+ next if author
1866
+ authors[idx] = [
1867
+ implicit_author_metadata[%(firstname_#{name_idx = idx + 1})],
1868
+ implicit_author_metadata[%(middlename_#{name_idx})],
1869
+ implicit_author_metadata[%(lastname_#{name_idx})]
1870
+ ].compact.map {|it| it.tr ' ', '_' }.join ' '
1910
1871
  end if sparse
1911
1872
  # process as names only
1912
1873
  author_metadata = process_authors authors, true, false
1913
1874
  else
1914
- author_metadata = {}
1875
+ author_metadata = { 'authorcount' => 0 }
1915
1876
  end
1916
1877
  end
1917
1878
 
1918
- if author_metadata.empty?
1919
- metadata['authorcount'] ||= (doc_attrs['authorcount'] = 0)
1879
+ if author_metadata['authorcount'] == 0
1880
+ if authorcount
1881
+ author_metadata = nil
1882
+ else
1883
+ doc_attrs['authorcount'] = 0
1884
+ end
1920
1885
  else
1921
1886
  doc_attrs.update author_metadata
1922
1887
 
@@ -1927,7 +1892,7 @@ class Parser
1927
1892
  end
1928
1893
  end
1929
1894
 
1930
- metadata
1895
+ implicit_author_metadata.merge rev_metadata.to_h, author_metadata.to_h if retrieve
1931
1896
  end
1932
1897
 
1933
1898
  # Internal: Parse the author line into a Hash of author metadata
@@ -1942,20 +1907,13 @@ class Parser
1942
1907
  def self.process_authors author_line, names_only = false, multiple = true
1943
1908
  author_metadata = {}
1944
1909
  author_idx = 0
1945
- keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
1946
- author_entries = multiple ? (author_line.split ';').map {|it| it.strip } : Array(author_line)
1947
- author_entries.each do |author_entry|
1910
+ (multiple && (author_line.include? ';') ? (author_line.split AuthorDelimiterRx) : [*author_line]).each do |author_entry|
1948
1911
  next if author_entry.empty?
1949
- author_idx += 1
1950
1912
  key_map = {}
1951
- if author_idx == 1
1952
- keys.each do |key|
1953
- key_map[key.to_sym] = key
1954
- end
1913
+ if (author_idx += 1) == 1
1914
+ AuthorKeys.each {|key| key_map[key.to_sym] = key }
1955
1915
  else
1956
- keys.each do |key|
1957
- key_map[key.to_sym] = %(#{key}_#{author_idx})
1958
- end
1916
+ AuthorKeys.each {|key| key_map[key.to_sym] = %(#{key}_#{author_idx}) }
1959
1917
  end
1960
1918
 
1961
1919
  if names_only # when parsing an attribute value
@@ -1999,9 +1957,7 @@ class Parser
1999
1957
  else
2000
1958
  # only assign the _1 attributes once we see the second author
2001
1959
  if author_idx == 2
2002
- keys.each do |key|
2003
- author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key
2004
- end
1960
+ AuthorKeys.each {|key| author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key }
2005
1961
  end
2006
1962
  author_metadata['authors'] = %(#{author_metadata['authors']}, #{author_metadata[key_map[:author]]})
2007
1963
  end
@@ -2020,7 +1976,7 @@ class Parser
2020
1976
  # document - the current Document
2021
1977
  # attributes - a Hash of attributes in which any metadata found will be stored (default: {})
2022
1978
  # options - a Hash of options to control processing: (default: {})
2023
- # * :text indicates that parser is only looking for text content
1979
+ # * :text_only indicates that parser is only looking for text content
2024
1980
  # and thus the block title should not be captured
2025
1981
  #
2026
1982
  # returns the Hash of attributes including any metadata found
@@ -2049,13 +2005,13 @@ class Parser
2049
2005
  # document - the current Document
2050
2006
  # attributes - a Hash of attributes in which any metadata found will be stored
2051
2007
  # options - a Hash of options to control processing: (default: {})
2052
- # * :text indicates the parser is only looking for text content,
2008
+ # * :text_only indicates the parser is only looking for text content,
2053
2009
  # thus neither a block title or attribute entry should be captured
2054
2010
  #
2055
- # returns true if the line contains metadata, otherwise false
2011
+ # returns true if the line contains metadata, otherwise falsy
2056
2012
  def self.parse_block_metadata_line reader, document, attributes, options = {}
2057
2013
  if (next_line = reader.peek_line) &&
2058
- (options[:text] ? (next_line.start_with? '[', '/') : (normal = next_line.start_with? '[', '.', '/', ':'))
2014
+ (options[:text_only] ? (next_line.start_with? '[', '/') : (normal = next_line.start_with? '[', '.', '/', ':'))
2059
2015
  if next_line.start_with? '['
2060
2016
  if next_line.start_with? '[['
2061
2017
  if (next_line.end_with? ']]') && BlockAnchorRx =~ next_line
@@ -2069,7 +2025,7 @@ class Parser
2069
2025
  elsif (next_line.end_with? ']') && BlockAttributeListRx =~ next_line
2070
2026
  current_style = attributes[1]
2071
2027
  # extract id, role, and options from first positional attribute and remove, if present
2072
- if (document.parse_attributes $1, [], :sub_input => true, :sub_result => true, :into => attributes)[1]
2028
+ if (document.parse_attributes $1, [], sub_input: true, sub_result: true, into: attributes)[1]
2073
2029
  attributes[1] = (parse_style_attribute attributes, reader) || current_style
2074
2030
  end
2075
2031
  return true
@@ -2084,9 +2040,9 @@ class Parser
2084
2040
  elsif !normal || (next_line.start_with? '/')
2085
2041
  if next_line == '//'
2086
2042
  return true
2087
- elsif normal && '/' * (ll = next_line.length) == next_line
2043
+ elsif normal && (uniform? next_line, '/', (ll = next_line.length))
2088
2044
  unless ll == 3
2089
- reader.read_lines_until :terminator => next_line, :skip_first_line => true, :preserve_last_line => true, :skip_processing => true, :context => :comment
2045
+ reader.read_lines_until terminator: next_line, skip_first_line: true, preserve_last_line: true, skip_processing: true, context: :comment
2090
2046
  return true
2091
2047
  end
2092
2048
  else
@@ -2098,6 +2054,7 @@ class Parser
2098
2054
  return true
2099
2055
  end
2100
2056
  end
2057
+ nil
2101
2058
  end
2102
2059
 
2103
2060
  # Process consecutive attribute entry lines, ignoring adjacent line comments and comment blocks.
@@ -2113,11 +2070,11 @@ class Parser
2113
2070
  end
2114
2071
 
2115
2072
  def self.process_attribute_entry reader, document, attributes = nil, match = nil
2116
- if (match ||= (reader.has_more_lines? ? (AttributeEntryRx.match reader.peek_line) : nil))
2073
+ if match || (match = reader.has_more_lines? ? (AttributeEntryRx.match reader.peek_line) : nil)
2117
2074
  if (value = match[2]).nil_or_empty?
2118
2075
  value = ''
2119
2076
  elsif value.end_with? LINE_CONTINUATION, LINE_CONTINUATION_LEGACY
2120
- con, value = value.slice(-2, 2), (value.slice 0, value.length - 2).rstrip
2077
+ con, value = (value.slice value.length - 2, 2), (value.slice 0, value.length - 2).rstrip
2121
2078
  while reader.advance && !(next_line = reader.peek_line || '').empty?
2122
2079
  next_line = next_line.lstrip
2123
2080
  next_line = (next_line.slice 0, next_line.length - 2).rstrip if (keep_open = next_line.end_with? con)
@@ -2144,15 +2101,21 @@ class Parser
2144
2101
  # TODO move processing of attribute value to utility method
2145
2102
  if name.end_with? '!'
2146
2103
  # a nil value signals the attribute should be deleted (unset)
2147
- name, value = name.chop, nil
2104
+ name = name.chop
2105
+ value = nil
2148
2106
  elsif name.start_with? '!'
2149
2107
  # a nil value signals the attribute should be deleted (unset)
2150
- name, value = (name.slice 1, name.length), nil
2108
+ name = (name.slice 1, name.length)
2109
+ value = nil
2151
2110
  end
2152
2111
 
2153
- name = sanitize_attribute_name name
2154
- # alias numbered attribute to sectnums
2155
- name = 'sectnums' if name == 'numbered'
2112
+ if (name = sanitize_attribute_name name) == 'numbered'
2113
+ name = 'sectnums'
2114
+ elsif name == 'hardbreaks'
2115
+ name = 'hardbreaks-option'
2116
+ elsif name == 'showtitle'
2117
+ store_attribute 'notitle', (value ? nil : ''), doc, attrs
2118
+ end
2156
2119
 
2157
2120
  if doc
2158
2121
  if value
@@ -2196,9 +2159,10 @@ class Parser
2196
2159
  #
2197
2160
  # Returns the String 0-index marker for this list item
2198
2161
  def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil)
2199
- if list_type == :ulist
2162
+ case list_type
2163
+ when :ulist
2200
2164
  marker
2201
- elsif list_type == :olist
2165
+ when :olist
2202
2166
  resolve_ordered_list_marker(marker, ordinal, validate, reader)[0]
2203
2167
  else # :colist
2204
2168
  '<1>'
@@ -2268,7 +2232,7 @@ class Parser
2268
2232
  end
2269
2233
 
2270
2234
  if validate && expected != actual
2271
- logger.warn message_with_context %(list item index: expected #{expected}, got #{actual}), :source_location => reader.cursor
2235
+ logger.warn message_with_context %(list item index: expected #{expected}, got #{actual}), source_location: reader.cursor
2272
2236
  end
2273
2237
 
2274
2238
  [marker, style]
@@ -2281,20 +2245,12 @@ class Parser
2281
2245
  # list_type - The context of the list (:olist, :ulist, :colist, :dlist)
2282
2246
  # sibling_trait - The String marker for the list or the Regexp to match a sibling
2283
2247
  #
2284
- # Returns a Boolean indicating whether this line is a sibling list item given
2285
- # the criteria provided
2286
- def self.is_sibling_list_item?(line, list_type, sibling_trait)
2248
+ # Returns a Boolean indicating whether this line is a sibling list item given the criteria provided
2249
+ def self.is_sibling_list_item? line, list_type, sibling_trait
2287
2250
  if ::Regexp === sibling_trait
2288
- matcher = sibling_trait
2289
- else
2290
- matcher = ListRxMap[list_type]
2291
- expected_marker = sibling_trait
2292
- end
2293
-
2294
- if matcher =~ line
2295
- expected_marker ? expected_marker == resolve_list_marker(list_type, $1) : true
2251
+ sibling_trait.match? line
2296
2252
  else
2297
- false
2253
+ ListRxMap[list_type] =~ line && sibling_trait == (resolve_list_marker list_type, $1)
2298
2254
  end
2299
2255
  end
2300
2256
 
@@ -2307,10 +2263,6 @@ class Parser
2307
2263
  # returns an instance of Asciidoctor::Table parsed from the provided reader
2308
2264
  def self.parse_table(table_reader, parent, attributes)
2309
2265
  table = Table.new(parent, attributes)
2310
- if attributes.key? 'title'
2311
- table.title = attributes.delete 'title'
2312
- table.assign_caption(attributes.delete 'caption')
2313
- end
2314
2266
 
2315
2267
  if (attributes.key? 'cols') && !(colspecs = parse_colspecs attributes['cols']).empty?
2316
2268
  table.create_columns colspecs
@@ -2318,9 +2270,15 @@ class Parser
2318
2270
  end
2319
2271
 
2320
2272
  skipped = table_reader.skip_blank_lines || 0
2273
+ if attributes['header-option']
2274
+ table.has_header_option = true
2275
+ elsif skipped == 0 && !attributes['noheader-option']
2276
+ # NOTE: assume table has header until we know otherwise; if it doesn't (nil), cells in first row get reprocessed
2277
+ table.has_header_option = :implicit
2278
+ implicit_header = true
2279
+ end
2321
2280
  parser_ctx = Table::ParserContext.new table_reader, table, attributes
2322
2281
  format, loop_idx, implicit_header_boundary = parser_ctx.format, -1, nil
2323
- implicit_header = true unless skipped > 0 || (attributes.key? 'header-option') || (attributes.key? 'noheader-option')
2324
2282
 
2325
2283
  while (line = table_reader.read_line)
2326
2284
  if (beyond_first = (loop_idx += 1) > 0) && line.empty?
@@ -2340,7 +2298,7 @@ class Parser
2340
2298
  implicit_header_boundary = nil if implicit_header_boundary
2341
2299
  # otherwise, the cell continues from previous line
2342
2300
  elsif implicit_header_boundary && implicit_header_boundary == loop_idx
2343
- implicit_header, implicit_header_boundary = false, nil
2301
+ table.has_header_option = implicit_header = implicit_header_boundary = nil
2344
2302
  end
2345
2303
  end
2346
2304
  end
@@ -2352,7 +2310,7 @@ class Parser
2352
2310
  if table_reader.has_more_lines? && table_reader.peek_line.empty?
2353
2311
  implicit_header_boundary = 1
2354
2312
  else
2355
- implicit_header = false
2313
+ table.has_header_option = implicit_header = nil
2356
2314
  end
2357
2315
  end
2358
2316
  end
@@ -2403,7 +2361,7 @@ class Parser
2403
2361
  case format
2404
2362
  when 'csv'
2405
2363
  if parser_ctx.buffer_has_unclosed_quotes?
2406
- implicit_header, implicit_header_boundary = false, nil if implicit_header_boundary && loop_idx == 0
2364
+ table.has_header_option = implicit_header = implicit_header_boundary = nil if implicit_header_boundary && loop_idx == 0
2407
2365
  parser_ctx.keep_cell_open
2408
2366
  else
2409
2367
  parser_ctx.close_cell true
@@ -2425,16 +2383,8 @@ class Parser
2425
2383
  end
2426
2384
  end
2427
2385
 
2428
- unless (table.attributes['colcount'] ||= table.columns.size) == 0 || explicit_colspecs
2429
- table.assign_column_widths
2430
- end
2431
-
2432
- if implicit_header
2433
- table.has_header_option = true
2434
- attributes['header-option'] = ''
2435
- attributes['options'] = (attributes.key? 'options') ? %(#{attributes['options']},header) : 'header'
2436
- end
2437
-
2386
+ table.assign_column_widths unless (table.attributes['colcount'] ||= table.columns.size) == 0 || explicit_colspecs
2387
+ table.has_header_option = true if implicit_header
2438
2388
  table.partition_header_footer attributes
2439
2389
 
2440
2390
  table
@@ -2490,9 +2440,7 @@ class Parser
2490
2440
  end
2491
2441
 
2492
2442
  if m[1]
2493
- 1.upto(m[1].to_i) {
2494
- specs << spec.dup
2495
- }
2443
+ 1.upto(m[1].to_i) { specs << spec.merge }
2496
2444
  else
2497
2445
  specs << spec
2498
2446
  end
@@ -2517,7 +2465,7 @@ class Parser
2517
2465
 
2518
2466
  if pos == :start
2519
2467
  if line.include? delimiter
2520
- spec_part, rest = line.split delimiter, 2
2468
+ spec_part, _, rest = line.partition delimiter
2521
2469
  if (m = CellSpecStartRx.match spec_part)
2522
2470
  return [{}, rest] if m[0].empty?
2523
2471
  else
@@ -2526,14 +2474,12 @@ class Parser
2526
2474
  else
2527
2475
  return [nil, line]
2528
2476
  end
2529
- else # pos == :end
2530
- if (m = CellSpecEndRx.match line)
2531
- # NOTE return the line stripped of trailing whitespace if no cellspec is found in this case
2532
- return [{}, line.rstrip] if m[0].lstrip.empty?
2533
- rest = m.pre_match
2534
- else
2535
- return [{}, line]
2536
- end
2477
+ elsif (m = CellSpecEndRx.match line) # when pos == :end
2478
+ # NOTE return the line stripped of trailing whitespace if no cellspec is found in this case
2479
+ return [{}, line.rstrip] if m[0].lstrip.empty?
2480
+ rest = m.pre_match
2481
+ else
2482
+ return [{}, line]
2537
2483
  end
2538
2484
 
2539
2485
  spec = {}
@@ -2541,10 +2487,11 @@ class Parser
2541
2487
  colspec, rowspec = m[1].split '.'
2542
2488
  colspec = colspec.nil_or_empty? ? 1 : colspec.to_i
2543
2489
  rowspec = rowspec.nil_or_empty? ? 1 : rowspec.to_i
2544
- if m[2] == '+'
2490
+ case m[2]
2491
+ when '+'
2545
2492
  spec['colspan'] = colspec unless colspec == 1
2546
2493
  spec['rowspan'] = rowspec unless rowspec == 1
2547
- elsif m[2] == '*'
2494
+ when '*'
2548
2495
  spec['repeatcol'] = colspec unless colspec == 1
2549
2496
  end
2550
2497
  end
@@ -2569,7 +2516,7 @@ class Parser
2569
2516
  # Public: Parse the first positional attribute and assign named attributes
2570
2517
  #
2571
2518
  # Parse the first positional attribute to extract the style, role and id
2572
- # parts, assign the values to their cooresponding attribute keys and return
2519
+ # parts, assign the values to their corresponding attribute keys and return
2573
2520
  # the parsed style from the first positional attribute.
2574
2521
  #
2575
2522
  # attributes - The Hash of attributes to process and update
@@ -2587,93 +2534,94 @@ class Parser
2587
2534
  # "role" => "lead", "options" => "fragment", "fragment-option" => '' }
2588
2535
  #
2589
2536
  # Returns the String style parsed from the first positional attribute
2590
- def self.parse_style_attribute(attributes, reader = nil)
2537
+ def self.parse_style_attribute attributes, reader = nil
2591
2538
  # NOTE spaces are not allowed in shorthand, so if we detect one, this ain't no shorthand
2592
2539
  if (raw_style = attributes[1]) && !raw_style.include?(' ') && Compliance.shorthand_property_syntax
2593
- type, collector, parsed = :style, [], {}
2594
- # QUESTION should this be a private method? (though, it's never called if shorthand isn't used)
2595
- save_current = lambda {
2596
- if collector.empty?
2597
- unless type == :style
2598
- if reader
2599
- logger.warn message_with_context %(invalid empty #{type} detected in style attribute), :source_location => reader.cursor_at_prev_line
2600
- else
2601
- logger.warn %(invalid empty #{type} detected in style attribute)
2602
- end
2603
- end
2604
- else
2605
- case type
2606
- when :role, :option
2607
- (parsed[type] ||= []) << collector.join
2608
- when :id
2609
- if parsed.key? :id
2610
- if reader
2611
- logger.warn message_with_context 'multiple ids detected in style attribute', :source_location => reader.cursor_at_prev_line
2612
- else
2613
- logger.warn 'multiple ids detected in style attribute'
2614
- end
2615
- end
2616
- parsed[type] = collector.join
2617
- else
2618
- parsed[type] = collector.join
2619
- end
2620
- collector = []
2621
- end
2622
- }
2540
+ name = nil
2541
+ accum = ''
2542
+ parsed_attrs = {}
2623
2543
 
2624
2544
  raw_style.each_char do |c|
2625
- if c == '.' || c == '#' || c == '%'
2626
- save_current.call
2627
- case c
2628
- when '.'
2629
- type = :role
2630
- when '#'
2631
- type = :id
2632
- when '%'
2633
- type = :option
2634
- end
2545
+ case c
2546
+ when '.'
2547
+ yield_buffered_attribute parsed_attrs, name, accum, reader
2548
+ accum = ''
2549
+ name = :role
2550
+ when '#'
2551
+ yield_buffered_attribute parsed_attrs, name, accum, reader
2552
+ accum = ''
2553
+ name = :id
2554
+ when '%'
2555
+ yield_buffered_attribute parsed_attrs, name, accum, reader
2556
+ accum = ''
2557
+ name = :option
2635
2558
  else
2636
- collector << c
2559
+ accum += c
2637
2560
  end
2638
2561
  end
2639
2562
 
2640
2563
  # small optimization if no shorthand is found
2641
- if type == :style
2642
- attributes['style'] = raw_style
2643
- else
2644
- save_current.call
2564
+ if name
2565
+ yield_buffered_attribute parsed_attrs, name, accum, reader
2645
2566
 
2646
- parsed_style = attributes['style'] = parsed[:style] if parsed.key? :style
2567
+ if (parsed_style = parsed_attrs[:style])
2568
+ attributes['style'] = parsed_style
2569
+ end
2647
2570
 
2648
- attributes['id'] = parsed[:id] if parsed.key? :id
2571
+ attributes['id'] = parsed_attrs[:id] if parsed_attrs.key? :id
2649
2572
 
2650
- if parsed.key? :role
2651
- attributes['role'] = (existing_role = attributes['role']).nil_or_empty? ? (parsed[:role].join ' ') : %(#{existing_role} #{parsed[:role].join ' '})
2573
+ if parsed_attrs.key? :role
2574
+ attributes['role'] = (existing_role = attributes['role']).nil_or_empty? ? (parsed_attrs[:role].join ' ') : %(#{existing_role} #{parsed_attrs[:role].join ' '})
2652
2575
  end
2653
2576
 
2654
- if parsed.key? :option
2655
- (opts = parsed[:option]).each {|opt| attributes[%(#{opt}-option)] = '' }
2656
- attributes['options'] = (existing_opts = attributes['options']).nil_or_empty? ? (opts.join ',') : %(#{existing_opts},#{opts.join ','})
2657
- end
2577
+ parsed_attrs[:option].each {|opt| attributes[%(#{opt}-option)] = '' } if parsed_attrs.key? :option
2658
2578
 
2659
2579
  parsed_style
2580
+ else
2581
+ attributes['style'] = raw_style
2660
2582
  end
2661
2583
  else
2662
2584
  attributes['style'] = raw_style
2663
2585
  end
2664
2586
  end
2665
2587
 
2666
- # Remove the block indentation (the leading whitespace equal to the amount of
2667
- # leading whitespace of the least indented line), then replace tabs with
2668
- # spaces (using proper tab expansion logic) and, finally, indent the lines by
2669
- # the amount specified.
2588
+ # Internal: Save the collected attribute (:id, :option, :role, or nil for :style) in the attribute Hash.
2589
+ def self.yield_buffered_attribute attrs, name, value, reader
2590
+ if name
2591
+ if value.empty?
2592
+ if reader
2593
+ logger.warn message_with_context %(invalid empty #{name} detected in style attribute), source_location: reader.cursor_at_prev_line
2594
+ else
2595
+ logger.warn %(invalid empty #{name} detected in style attribute)
2596
+ end
2597
+ elsif name == :id
2598
+ if attrs.key? :id
2599
+ if reader
2600
+ logger.warn message_with_context 'multiple ids detected in style attribute', source_location: reader.cursor_at_prev_line
2601
+ else
2602
+ logger.warn 'multiple ids detected in style attribute'
2603
+ end
2604
+ end
2605
+ attrs[name] = value
2606
+ else
2607
+ (attrs[name] ||= []) << value
2608
+ end
2609
+ else
2610
+ attrs[:style] = value unless value.empty?
2611
+ end
2612
+ nil
2613
+ end
2614
+
2615
+ # Remove the block indentation (the amount of whitespace of the least indented line), replace tabs with spaces (using
2616
+ # proper tab expansion logic) and, finally, indent the lines by the margin width. Modifies the input Array directly.
2670
2617
  #
2671
- # This method preserves the relative indentation of the lines.
2618
+ # This method preserves the significant indentation (that exceeding the block indent) on each line.
2672
2619
  #
2673
- # lines - the Array of String lines to process (no trailing endlines)
2674
- # indent - the integer number of spaces to add to the beginning
2675
- # of each line; if this value is nil, the existing
2676
- # space is preserved (optional, default: 0)
2620
+ # lines - The Array of String lines to process (no trailing newlines)
2621
+ # indent_size - The Integer number of spaces to readd to the start of non-empty lines after removing the indentation.
2622
+ # If this value is < 0, the existing indentation is preserved (optional, default: 0)
2623
+ # tab_size - the Integer number of spaces to use in place of a tab. A value of <= 0 disables the replacement
2624
+ # (optional, default: 0)
2677
2625
  #
2678
2626
  # Examples
2679
2627
  #
@@ -2683,89 +2631,95 @@ class Parser
2683
2631
  # end
2684
2632
  # EOS
2685
2633
  #
2686
- # source.split "\n"
2634
+ # source.split ?\n
2687
2635
  # # => [" def names", " @names.split", " end"]
2688
2636
  #
2689
- # puts Parser.adjust_indentation!(source.split "\n").join "\n"
2637
+ # puts (Parser.adjust_indentation! source.split ?\n).join ?\n
2690
2638
  # # => def names
2691
2639
  # # => @names.split
2692
2640
  # # => end
2693
2641
  #
2694
2642
  # returns Nothing
2695
- #--
2696
- # QUESTION should indent be called margin?
2697
- def self.adjust_indentation! lines, indent = 0, tab_size = 0
2643
+ def self.adjust_indentation! lines, indent_size = 0, tab_size = 0
2698
2644
  return if lines.empty?
2699
2645
 
2700
- # expand tabs if a tab is detected unless tab_size is nil
2701
- if (tab_size = tab_size.to_i) > 0 && (lines.join.include? TAB)
2702
- #if (tab_size = tab_size.to_i) > 0 && (lines.index {|line| line.include? TAB })
2646
+ # expand tabs if a tab character is detected and tab_size > 0
2647
+ if tab_size > 0 && lines.any? {|line| line.include? TAB }
2703
2648
  full_tab_space = ' ' * tab_size
2704
2649
  lines.map! do |line|
2705
- next line if line.empty?
2706
-
2707
- # NOTE Opal has to patch this use of sub!
2708
- line.sub!(TabIndentRx) { full_tab_space * $&.length } if line.start_with? TAB
2709
-
2710
- if line.include? TAB
2650
+ if line.empty? || (tab_idx = line.index TAB).nil?
2651
+ line
2652
+ else
2653
+ if tab_idx == 0
2654
+ leading_tabs = 0
2655
+ line.each_byte do |b|
2656
+ break unless b == 9
2657
+ leading_tabs += 1
2658
+ end
2659
+ line = %(#{full_tab_space * leading_tabs}#{line.slice leading_tabs, line.length})
2660
+ next line unless line.include? TAB
2661
+ end
2711
2662
  # keeps track of how many spaces were added to adjust offset in match data
2712
2663
  spaces_added = 0
2713
- # NOTE Opal has to patch this use of gsub!
2714
- line.gsub!(TabRx) {
2715
- # calculate how many spaces this tab represents, then replace tab with spaces
2716
- if (offset = ($~.begin 0) + spaces_added) % tab_size == 0
2717
- spaces_added += (tab_size - 1)
2718
- full_tab_space
2719
- else
2720
- unless (spaces = tab_size - offset % tab_size) == 1
2721
- spaces_added += (spaces - 1)
2664
+ idx = 0
2665
+ result = ''
2666
+ line.each_char do |c|
2667
+ if c == TAB
2668
+ # calculate how many spaces this tab represents, then replace tab with spaces
2669
+ if (offset = idx + spaces_added) % tab_size == 0
2670
+ spaces_added += tab_size - 1
2671
+ result += full_tab_space
2672
+ else
2673
+ unless (spaces = tab_size - offset % tab_size) == 1
2674
+ spaces_added += spaces - 1
2675
+ end
2676
+ result += ' ' * spaces
2722
2677
  end
2723
- ' ' * spaces
2678
+ else
2679
+ result += c
2724
2680
  end
2725
- }
2726
- else
2727
- line
2681
+ idx += 1
2682
+ end
2683
+ result
2728
2684
  end
2729
2685
  end
2730
2686
  end
2731
2687
 
2732
- # skip adjustment of gutter if indent is -1
2733
- return unless indent && (indent = indent.to_i) > -1
2688
+ # skip block indent adjustment if indent_size is < 0
2689
+ return if indent_size < 0
2734
2690
 
2735
- # determine width of gutter
2736
- gutter_width = nil
2691
+ # determine block indent (assumes no whitespace-only lines are present)
2692
+ block_indent = nil
2737
2693
  lines.each do |line|
2738
2694
  next if line.empty?
2739
- # NOTE this logic assumes no whitespace-only lines
2740
2695
  if (line_indent = line.length - line.lstrip.length) == 0
2741
- gutter_width = nil
2696
+ block_indent = nil
2742
2697
  break
2743
- else
2744
- unless gutter_width && line_indent > gutter_width
2745
- gutter_width = line_indent
2746
- end
2747
2698
  end
2699
+ block_indent = line_indent unless block_indent && block_indent < line_indent
2748
2700
  end
2749
2701
 
2750
- # remove gutter then apply new indent if specified
2751
- # NOTE gutter_width is > 0 if not nil
2752
- if indent == 0
2753
- if gutter_width
2754
- lines.map! {|line| line.empty? ? line : (line.slice gutter_width, line.length) }
2755
- end
2702
+ # remove block indent then apply indent_size if specified
2703
+ # NOTE block_indent is > 0 if not nil
2704
+ if indent_size == 0
2705
+ lines.map! {|line| line.empty? ? line : (line.slice block_indent, line.length) } if block_indent
2756
2706
  else
2757
- padding = ' ' * indent
2758
- if gutter_width
2759
- lines.map! {|line| line.empty? ? line : padding + (line.slice gutter_width, line.length) }
2707
+ new_block_indent = ' ' * indent_size
2708
+ if block_indent
2709
+ lines.map! {|line| line.empty? ? line : new_block_indent + (line.slice block_indent, line.length) }
2760
2710
  else
2761
- lines.map! {|line| line.empty? ? line : padding + line }
2711
+ lines.map! {|line| line.empty? ? line : new_block_indent + line }
2762
2712
  end
2763
2713
  end
2764
2714
 
2765
2715
  nil
2766
2716
  end
2767
2717
 
2768
- # Public: Convert a string to a legal attribute name.
2718
+ def self.uniform? str, chr, len
2719
+ (str.count chr) == len
2720
+ end
2721
+
2722
+ # Internal: Convert a string to a legal attribute name.
2769
2723
  #
2770
2724
  # name - the String name of the attribute
2771
2725
  #