asciidoctor 1.5.6.2 → 1.5.7

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

Potentially problematic release.


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

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