asciidoctor 0.1.0 → 0.1.1

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 (40) hide show
  1. data/README.asciidoc +11 -2
  2. data/asciidoctor.gemspec +3 -2
  3. data/lib/asciidoctor.rb +95 -62
  4. data/lib/asciidoctor/abstract_block.rb +7 -5
  5. data/lib/asciidoctor/abstract_node.rb +63 -15
  6. data/lib/asciidoctor/attribute_list.rb +3 -1
  7. data/lib/asciidoctor/backends/base_template.rb +17 -7
  8. data/lib/asciidoctor/backends/docbook45.rb +182 -150
  9. data/lib/asciidoctor/backends/html5.rb +138 -110
  10. data/lib/asciidoctor/block.rb +21 -18
  11. data/lib/asciidoctor/callouts.rb +3 -1
  12. data/lib/asciidoctor/cli/invoker.rb +3 -3
  13. data/lib/asciidoctor/cli/options.rb +6 -6
  14. data/lib/asciidoctor/debug.rb +7 -6
  15. data/lib/asciidoctor/document.rb +197 -25
  16. data/lib/asciidoctor/errors.rb +1 -1
  17. data/lib/asciidoctor/helpers.rb +29 -0
  18. data/lib/asciidoctor/inline.rb +11 -4
  19. data/lib/asciidoctor/lexer.rb +338 -182
  20. data/lib/asciidoctor/list_item.rb +14 -12
  21. data/lib/asciidoctor/reader.rb +423 -206
  22. data/lib/asciidoctor/renderer.rb +59 -15
  23. data/lib/asciidoctor/section.rb +7 -4
  24. data/lib/asciidoctor/substituters.rb +536 -511
  25. data/lib/asciidoctor/table.rb +473 -472
  26. data/lib/asciidoctor/version.rb +1 -1
  27. data/man/asciidoctor.1 +23 -14
  28. data/man/asciidoctor.ad +13 -7
  29. data/test/attributes_test.rb +42 -8
  30. data/test/blocks_test.rb +161 -1
  31. data/test/document_test.rb +134 -16
  32. data/test/invoker_test.rb +14 -6
  33. data/test/lexer_test.rb +45 -18
  34. data/test/lists_test.rb +79 -0
  35. data/test/paragraphs_test.rb +9 -1
  36. data/test/reader_test.rb +456 -19
  37. data/test/sections_test.rb +19 -0
  38. data/test/substitutions_test.rb +14 -12
  39. data/test/tables_test.rb +10 -10
  40. metadata +3 -5
@@ -1,5 +1,5 @@
1
1
  # Base project exception
2
2
  module Asciidoctor
3
- class ProjectError < StandardError; end
3
+ class ProjectError < StandardError; end
4
4
  end
5
5
 
@@ -0,0 +1,29 @@
1
+ module Asciidoctor
2
+ module Helpers
3
+ # Internal: Prior to invoking Kernel#require, issues a warning urging a
4
+ # manual require if running in a threaded environment.
5
+ #
6
+ # name - the String name of the library to require.
7
+ #
8
+ # returns false if the library is detected on the load path or the return
9
+ # value of delegating to Kernel#require
10
+ def self.require_library(name)
11
+ if Thread.list.size > 1
12
+ main_script = "#{name}.rb"
13
+ main_script_path_segment = "/#{name}.rb"
14
+ if !$LOADED_FEATURES.detect {|p| p == main_script || p.end_with?(main_script_path_segment) }.nil?
15
+ return false
16
+ else
17
+ warn "WARN: asciidoctor is autoloading '#{name}' in threaded environment. " +
18
+ "The use of an explicit require '#{name}' statement is recommended."
19
+ end
20
+ end
21
+ require name
22
+ end
23
+
24
+ # Public: A generic capture output routine to be used in templates
25
+ #def self.capture_output(*args, &block)
26
+ # Proc.new { block.call(*args) }
27
+ #end
28
+ end
29
+ end
@@ -1,5 +1,6 @@
1
+ module Asciidoctor
1
2
  # Public: Methods for managing inline elements in AsciiDoc block
2
- class Asciidoctor::Inline < Asciidoctor::AbstractNode
3
+ class Inline < AbstractNode
3
4
  # Public: Get the text of this inline element
4
5
  attr_reader :text
5
6
 
@@ -13,9 +14,14 @@ class Asciidoctor::Inline < Asciidoctor::AbstractNode
13
14
  super(parent, context)
14
15
 
15
16
  @text = text
16
- @id = opts[:id] if opts.has_key?(:id)
17
- @type = opts[:type] if opts.has_key?(:type)
18
- @target = opts[:target] if opts.has_key?(:target)
17
+
18
+ #@id = opts[:id] if opts.has_key?(:id)
19
+ #@type = opts[:type] if opts.has_key?(:type)
20
+ #@target = opts[:target] if opts.has_key?(:target)
21
+
22
+ @id = opts[:id]
23
+ @type = opts[:type]
24
+ @target = opts[:target]
19
25
 
20
26
  if opts.has_key?(:attributes) && (attributes = opts[:attributes]).is_a?(Hash)
21
27
  update_attributes(opts[:attributes]) unless attributes.empty?
@@ -27,3 +33,4 @@ class Asciidoctor::Inline < Asciidoctor::AbstractNode
27
33
  end
28
34
 
29
35
  end
36
+ end
@@ -1,3 +1,4 @@
1
+ module Asciidoctor
1
2
  # Public: Methods to parse lines of AsciiDoc into an object hierarchy
2
3
  # representing the structure of the document. All methods are class methods and
3
4
  # should be invoked from the Lexer class. The main entry point is ::next_block.
@@ -20,9 +21,9 @@
20
21
  # block = Lexer.next_block(reader, doc)
21
22
  # block.class
22
23
  # # => Asciidoctor::Block
23
- class Asciidoctor::Lexer
24
+ class Lexer
24
25
 
25
- include Asciidoctor
26
+ BlockMatchData = Struct.new(:name, :tip, :terminator)
26
27
 
27
28
  # Public: Make sure the Lexer object doesn't get initialized.
28
29
  #
@@ -40,28 +41,60 @@ class Asciidoctor::Lexer
40
41
  #
41
42
  # reader - the Reader holding the source lines of the document
42
43
  # document - the empty Document into which the lines will be parsed
44
+ # options - a Hash of options to control processing
43
45
  #
44
46
  # returns the Document object
45
- def self.parse(reader, document)
46
- # process and plow away any attribute lines that proceed the first block so
47
- # we can get at the document title, if present, then begin parsing blocks
48
- reader.skip_blank_lines
49
- attributes = parse_block_metadata_lines(reader, document)
47
+ def self.parse(reader, document, options = {})
48
+ block_attributes = parse_document_header(reader, document)
50
49
 
51
- # by processing the header here, we enforce its position at head of the document
52
- next_level = is_next_line_section? reader, attributes
53
- if next_level == 0
54
- title_info = parse_section_title(reader)
55
- document.title = title_info[1]
56
- parse_header_metadata(reader, document)
50
+ unless options[:header_only]
51
+ while reader.has_more_lines?
52
+ new_section, block_attributes = next_section(reader, document, block_attributes)
53
+ document << new_section unless new_section.nil?
54
+ end
57
55
  end
58
56
 
59
- while reader.has_lines?
60
- new_section, attributes = next_section(reader, document, attributes)
61
- document << new_section unless new_section.nil?
57
+ document
58
+ end
59
+
60
+ # Public: Parses the document header of the AsciiDoc source read from the Reader
61
+ #
62
+ # Reads the AsciiDoc source from the Reader until the end of the document
63
+ # header is reached. The Document object is populated with information from
64
+ # the header (document title, document attributes, etc). The document
65
+ # attributes are then saved to establish a save point to which to rollback
66
+ # after parsing is complete.
67
+ #
68
+ # This method assumes that there are no blank lines at the start of the document,
69
+ # which are automatically removed by the reader.
70
+ #
71
+ # returns the Hash of orphan block attributes captured above the header
72
+ def self.parse_document_header(reader, document)
73
+ # capture any lines of block-level metadata and plow away any comment lines
74
+ # that precede first block
75
+ block_attributes = parse_block_metadata_lines(reader, document)
76
+
77
+ # check if the first line is the document title
78
+ # if so, add a header to the document and parse the header metadata
79
+ if is_next_line_document_title?(reader, block_attributes)
80
+ document.id, document.title, _, _ = parse_section_title(reader)
81
+ # QUESTION: should this be encapsulated in document?
82
+ if document.id.nil? && block_attributes.has_key?('id')
83
+ document.id = block_attributes.delete('id')
84
+ end
85
+ parse_header_metadata(reader, document)
62
86
  end
63
87
 
64
- document
88
+ if document.attributes.has_key? 'doctitle'
89
+ document.title = document.attributes['doctitle']
90
+ end
91
+
92
+ document.clear_playback_attributes block_attributes
93
+ document.save_attributes
94
+
95
+ # NOTE these are the block-level attributes (not document attributes) that
96
+ # precede the first line of content (document title, first section or first block)
97
+ block_attributes
65
98
  end
66
99
 
67
100
  # Public: Return the next section from the Reader.
@@ -145,18 +178,18 @@ class Asciidoctor::Lexer
145
178
  #
146
179
  # We have to parse all the metadata lines before continuing with the loop,
147
180
  # otherwise subsequent metadata lines get interpreted as block content
148
- while reader.has_lines?
181
+ while reader.has_more_lines?
149
182
  parse_block_metadata_lines(reader, section, attributes)
150
183
 
151
184
  next_level = is_next_line_section? reader, attributes
152
185
  if next_level
153
186
  doctype = parent.document.doctype
154
187
  if next_level == 0 && doctype != 'book'
155
- puts "asciidoctor: ERROR: only book doctypes can contain level 0 sections"
188
+ puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
156
189
  end
157
190
  if next_level > current_level || (section.is_a?(Document) && next_level == 0)
158
191
  unless expected_next_levels.nil? || expected_next_levels.include?(next_level)
159
- puts "asciidoctor: WARNING: section title out of sequence: " +
192
+ puts "asciidoctor: WARNING: line #{reader.lineno + 1}: section title out of sequence: " +
160
193
  "expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, " +
161
194
  "got level #{next_level}"
162
195
  end
@@ -210,23 +243,20 @@ class Asciidoctor::Lexer
210
243
  # Returns a Section or Block object holding the parsed content of the processed lines
211
244
  def self.next_block(reader, parent, attributes = {}, options = {})
212
245
  # Skip ahead to the block content
213
- skipped = reader.skip_blank
246
+ skipped = reader.skip_blank_lines
214
247
 
215
248
  # bail if we've reached the end of the section content
216
- return nil unless reader.has_lines?
249
+ return nil unless reader.has_more_lines?
217
250
 
218
251
  if options[:text] && skipped > 0
219
252
  options.delete(:text)
220
253
  end
221
254
 
222
- Asciidoctor.debug {
255
+ Debug.debug {
223
256
  msg = []
224
257
  msg << '/' * 64
225
258
  msg << 'next_block() - First two lines are:'
226
- msg << reader.peek_line
227
- tmp_line = reader.get_line
228
- msg << reader.peek_line
229
- reader.unshift tmp_line
259
+ msg.concat reader.peek_lines(2)
230
260
  msg << '/' * 64
231
261
  msg * "\n"
232
262
  }
@@ -238,9 +268,9 @@ class Asciidoctor::Lexer
238
268
  context = parent.is_a?(Block) ? parent.context : nil
239
269
  block = nil
240
270
 
241
- while reader.has_lines? && block.nil?
271
+ while reader.has_more_lines? && block.nil?
242
272
  if parse_metadata && parse_block_metadata_line(reader, document, attributes, options)
243
- reader.next_line
273
+ reader.advance
244
274
  next
245
275
  elsif parse_sections && context.nil? && is_next_line_section?(reader, attributes)
246
276
  block, attributes = next_section(reader, parent, attributes)
@@ -249,24 +279,19 @@ class Asciidoctor::Lexer
249
279
 
250
280
  this_line = reader.get_line
251
281
 
252
- delimited_blk = delimited_block? this_line
253
-
254
- # NOTE I've haven't decided whether I want this check here or in
255
- # parse_block_metadata (where it is currently)
256
- #if this_line.match(REGEXP[:comment_blk])
257
- # reader.grab_lines_until {|line| line.match( REGEXP[:comment_blk] ) }
258
- # reader.skip_blank
259
- # # NOTE we should break here because we have found a block, it
260
- # # just happens to be nil...if we keep going we potentially overrun
261
- # # a section heading which is not processed in this anymore
262
- # break
282
+ block_context = nil
283
+ terminator = nil
284
+ if delimited_blk_match = is_delimited_block?(this_line, true)
285
+ block_context = delimited_blk_match.name
286
+ terminator = delimited_blk_match.terminator
287
+ end
263
288
 
264
- # NOTE we're letting ruler have attributes
265
- if !options[:text] && this_line.match(REGEXP[:ruler])
266
- block = Block.new(parent, :ruler)
267
- reader.skip_blank
289
+ # NOTE we're letting break lines (ruler, page_break, etc) have attributes
290
+ if !options[:text] && block_context.nil? && (match = this_line.match(REGEXP[:break_line]))
291
+ block = Block.new(parent, BREAK_LINES[match[0][0..2]])
292
+ reader.skip_blank_lines
268
293
 
269
- elsif !options[:text] && (match = this_line.match(REGEXP[:image_blk]))
294
+ elsif !options[:text] && block_context.nil? && (match = this_line.match(REGEXP[:image_blk]))
270
295
  block = Block.new(parent, :image)
271
296
  AttributeList.new(document.sub_attributes(match[2])).parse_into(attributes, ['alt', 'width', 'height'])
272
297
  target = block.sub_attributes(match[1])
@@ -274,57 +299,56 @@ class Asciidoctor::Lexer
274
299
  attributes['target'] = target
275
300
  document.register(:images, target)
276
301
  attributes['alt'] ||= File.basename(target, File.extname(target))
277
- # hmmm, this assignment seems like a one-off
278
302
  block.title = attributes['title']
279
- if block.title? && attributes['caption'].nil?
280
- attributes['caption'] = "Figure #{document.counter('figure-number')}. "
303
+ if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
304
+ number = document.counter('figure-number')
305
+ attributes['caption'] = "#{document.attributes['figure-caption']} #{number}. "
306
+ Document::AttributeEntry.new('figure-number', number).save_to(attributes)
281
307
  end
282
308
  else
283
309
  # drop the line if target resolves to nothing
284
310
  block = nil
285
311
  end
286
- reader.skip_blank
312
+ reader.skip_blank_lines
287
313
 
288
- elsif delimited_blk && (match = this_line.match(REGEXP[:open_blk]))
314
+ elsif block_context == :open
289
315
  # an open block is surrounded by '--' lines and has zero or more blocks inside
290
- terminator = match[0]
291
316
  buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
292
317
 
293
318
  # Strip lines off end of block - not implemented yet
294
- # while buffer.has_lines? && buffer.last.strip.empty?
319
+ # while buffer.has_more_lines? && buffer.last.strip.empty?
295
320
  # buffer.pop
296
321
  # end
297
322
 
298
- block = Block.new(parent, :open)
299
- while buffer.has_lines?
323
+ block = Block.new(parent, block_context)
324
+ while buffer.has_more_lines?
300
325
  new_block = next_block(buffer, block)
301
326
  block.blocks << new_block unless new_block.nil?
302
327
  end
303
328
 
304
329
  # needs to come before list detection
305
- elsif delimited_blk && (match = this_line.match(REGEXP[:sidebar_blk]))
330
+ elsif block_context == :sidebar
306
331
  # sidebar is surrounded by '****' (4 or more '*' chars) lines
307
- terminator = match[0]
308
332
  # FIXME violates DRY because it's a duplication of quote parsing
309
- block = Block.new(parent, :sidebar)
333
+ block = Block.new(parent, block_context)
310
334
  buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
311
335
 
312
- while buffer.has_lines?
336
+ while buffer.has_more_lines?
313
337
  new_block = next_block(buffer, block)
314
338
  block.blocks << new_block unless new_block.nil?
315
339
  end
316
340
 
317
- elsif match = this_line.match(REGEXP[:colist])
341
+ elsif block_context.nil? && (match = this_line.match(REGEXP[:colist]))
318
342
  block = Block.new(parent, :colist)
319
343
  attributes['style'] = 'arabic'
320
344
  items = []
321
345
  block.buffer = items
322
- reader.unshift this_line
346
+ reader.unshift_line this_line
323
347
  expected_index = 1
324
348
  begin
325
349
  # might want to move this check to a validate method
326
350
  if match[1].to_i != expected_index
327
- puts "asciidoctor: WARNING: callout list item index: expected #{expected_index} got #{match[1]}"
351
+ puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}"
328
352
  end
329
353
  list_item = next_list_item(reader, block, match)
330
354
  expected_index += 1
@@ -334,21 +358,21 @@ class Asciidoctor::Lexer
334
358
  if !coids.empty?
335
359
  list_item.attributes['coids'] = coids
336
360
  else
337
- puts 'asciidoctor: WARNING: no callouts refer to list item ' + items.size.to_s
361
+ puts "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{items.size}"
338
362
  end
339
363
  end
340
- end while reader.has_lines? && match = reader.peek_line.match(REGEXP[:colist])
364
+ end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist])
341
365
 
342
366
  document.callouts.next_list
343
367
 
344
- elsif match = this_line.match(REGEXP[:ulist])
368
+ elsif block_context.nil? && (match = this_line.match(REGEXP[:ulist]))
345
369
  AttributeList.rekey(attributes, ['style'])
346
- reader.unshift(this_line)
370
+ reader.unshift_line this_line
347
371
  block = next_outline_list(reader, :ulist, parent)
348
372
 
349
- elsif match = this_line.match(REGEXP[:olist])
373
+ elsif block_context.nil? && (match = this_line.match(REGEXP[:olist]))
350
374
  AttributeList.rekey(attributes, ['style'])
351
- reader.unshift(this_line)
375
+ reader.unshift_line this_line
352
376
  block = next_outline_list(reader, :olist, parent)
353
377
  # QUESTION move this logic to next_outline_list?
354
378
  if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style')
@@ -363,68 +387,80 @@ class Asciidoctor::Lexer
363
387
  end
364
388
  end
365
389
 
366
- elsif match = this_line.match(REGEXP[:dlist])
367
- reader.unshift this_line
390
+ elsif block_context.nil? && (match = this_line.match(REGEXP[:dlist]))
391
+ reader.unshift_line this_line
368
392
  block = next_labeled_list(reader, match, parent)
369
393
  AttributeList.rekey(attributes, ['style'])
370
394
 
371
- elsif delimited_blk && (match = this_line.match(document.nested? ? REGEXP[:table_nested] : REGEXP[:table]))
395
+ elsif block_context == :table
372
396
  # table is surrounded by lines starting with a | followed by 3 or more '=' chars
373
- terminator = match[0]
374
397
  AttributeList.rekey(attributes, ['style'])
375
398
  table_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
376
399
  block = next_table(table_reader, parent, attributes)
377
- # hmmm, this assignment seems like a one-off
378
400
  block.title = attributes['title']
379
- if block.title? && attributes['caption'].nil?
380
- attributes['caption'] = "Table #{document.counter('table-number')}. "
401
+ if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
402
+ number = document.counter('table-number')
403
+ attributes['caption'] = "#{document.attributes['table-caption']} #{number}. "
404
+ Document::AttributeEntry.new('table-number', number).save_to(attributes)
381
405
  end
382
406
 
383
407
  # FIXME violates DRY because it's a duplication of other block parsing
384
- elsif delimited_blk && (match = this_line.match(REGEXP[:example]))
408
+ elsif block_context == :example
385
409
  # example is surrounded by lines with 4 or more '=' chars
386
- terminator = match[0]
387
410
  AttributeList.rekey(attributes, ['style'])
388
411
  if admonition_style = ADMONITION_STYLES.detect {|s| attributes['style'] == s}
389
412
  block = Block.new(parent, :admonition)
390
- attributes['name'] = admonition_style.downcase
391
- attributes['caption'] ||= admonition_style.capitalize
413
+ attributes['name'] = admonition_name = admonition_style.downcase
414
+ attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
392
415
  else
393
- block = Block.new(parent, :example)
394
- # hmmm, this assignment seems like a one-off
416
+ block = Block.new(parent, block_context)
395
417
  block.title = attributes['title']
396
- if block.title? && attributes['caption'].nil?
397
- attributes['caption'] = "Example #{document.counter('example-number')}. "
418
+ if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
419
+ number = document.counter('example-number')
420
+ attributes['caption'] = "#{document.attributes['example-caption']} #{number}. "
421
+ Document::AttributeEntry.new('example-number', number).save_to(attributes)
398
422
  end
399
423
  end
400
424
  buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
401
425
 
402
- while buffer.has_lines?
426
+ while buffer.has_more_lines?
403
427
  new_block = next_block(buffer, block)
404
428
  block.blocks << new_block unless new_block.nil?
405
429
  end
406
430
 
407
431
  # FIXME violates DRY w/ non-delimited block listing
408
- elsif delimited_blk && (match = this_line.match(REGEXP[:listing]))
409
- terminator = match[0]
410
- AttributeList.rekey(attributes, ['style', 'language', 'linenums'])
432
+ elsif block_context == :listing || block_context == :fenced_code
433
+ if block_context == :fenced_code
434
+ attributes['style'] = 'source'
435
+ lang = this_line[3..-1].strip
436
+ attributes['language'] = lang unless lang.empty?
437
+ terminator = terminator[0..2] if terminator.length > 3
438
+ else
439
+ AttributeList.rekey(attributes, ['style', 'language', 'linenums'])
440
+ end
411
441
  buffer = reader.grab_lines_until(:terminator => terminator)
412
442
  buffer.last.chomp! unless buffer.empty?
413
443
  block = Block.new(parent, :listing, buffer)
444
+ block.title = attributes['title']
445
+ if document.attributes.has_key?('listing-caption') &&
446
+ block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
447
+ number = document.counter('listing-number')
448
+ attributes['caption'] = "#{document.attributes['listing-caption']} #{number}. "
449
+ Document::AttributeEntry.new('listing-number', number).save_to(attributes)
450
+ end
414
451
 
415
- elsif delimited_blk && (match = this_line.match(REGEXP[:quote]))
452
+ elsif block_context == :quote
416
453
  # multi-line verse or quote is surrounded by a block delimiter
417
- terminator = match[0]
418
454
  AttributeList.rekey(attributes, ['style', 'attribution', 'citetitle'])
419
455
  quote_context = (attributes['style'] == 'verse' ? :verse : :quote)
420
456
  block_reader = Reader.new reader.grab_lines_until(:terminator => terminator)
421
457
 
422
- # only quote can have other section elements (as as section block)
458
+ # only quote can have other section elements (as section block)
423
459
  section_body = (quote_context == :quote)
424
460
 
425
461
  if section_body
426
462
  block = Block.new(parent, quote_context)
427
- while block_reader.has_lines?
463
+ while block_reader.has_more_lines?
428
464
  new_block = next_block(block_reader, block)
429
465
  block.blocks << new_block unless new_block.nil?
430
466
  end
@@ -433,29 +469,28 @@ class Asciidoctor::Lexer
433
469
  block = Block.new(parent, quote_context, block_reader.lines)
434
470
  end
435
471
 
436
- elsif delimited_blk && (blk_ctx = [:literal, :pass].detect{|t| this_line.match(REGEXP[t])})
472
+ elsif block_context == :literal || block_context == :pass
437
473
  # literal is surrounded by '....' (4 or more '.' chars) lines
438
474
  # pass is surrounded by '++++' (4 or more '+' chars) lines
439
- terminator = $~[0]
440
475
  buffer = reader.grab_lines_until(:terminator => terminator)
441
476
  buffer.last.chomp! unless buffer.empty?
442
477
  # a literal can masquerade as a listing
443
478
  if attributes[1] == 'listing'
444
- blk_ctx = :listing
479
+ block_context = :listing
445
480
  end
446
- block = Block.new(parent, blk_ctx, buffer)
481
+ block = Block.new(parent, block_context, buffer)
447
482
 
448
483
  elsif this_line.match(REGEXP[:lit_par])
449
484
  # literal paragraph is contiguous lines starting with
450
485
  # one or more space or tab characters
451
486
 
452
487
  # So we need to actually include this one in the grab_lines group
453
- reader.unshift this_line
488
+ reader.unshift_line this_line
454
489
  buffer = reader.grab_lines_until(:preserve_last_line => true, :break_on_blank_lines => true) {|line|
455
490
  # labeled list terms can be indented, but a preceding blank indicates
456
491
  # we are in a list continuation and therefore literals should be strictly literal
457
492
  (context == :dlist && skipped == 0 && line.match(REGEXP[:dlist])) ||
458
- delimited_block?(line)
493
+ is_delimited_block?(line)
459
494
  }
460
495
 
461
496
  # trim off the indentation equivalent to the size of the least indented line
@@ -477,27 +512,35 @@ class Asciidoctor::Lexer
477
512
 
478
513
  ## these switches based on style need to come immediately before the else ##
479
514
 
480
- elsif attributes[1] == 'source'
481
- AttributeList.rekey(attributes, ['style', 'language', 'linenums'])
482
- reader.unshift(this_line)
515
+ elsif attributes[1] == 'source' || attributes[1] == 'listing'
516
+ if attributes[1] == 'source'
517
+ AttributeList.rekey(attributes, ['style', 'language', 'linenums'])
518
+ end
519
+ reader.unshift_line this_line
483
520
  buffer = reader.grab_lines_until(:break_on_blank_lines => true)
484
521
  buffer.last.chomp! unless buffer.empty?
485
522
  block = Block.new(parent, :listing, buffer)
486
523
 
524
+ elsif attributes[1] == 'literal'
525
+ reader.unshift_line this_line
526
+ buffer = reader.grab_lines_until(:break_on_blank_lines => true)
527
+ buffer.last.chomp! unless buffer.empty?
528
+ block = Block.new(parent, :literal, buffer)
529
+
487
530
  elsif admonition_style = ADMONITION_STYLES.detect{|s| attributes[1] == s}
488
531
  # an admonition preceded by [<TYPE>] and lasts until a blank line
489
- reader.unshift(this_line)
532
+ reader.unshift_line this_line
490
533
  buffer = reader.grab_lines_until(:break_on_blank_lines => true)
491
534
  buffer.last.chomp! unless buffer.empty?
492
535
  block = Block.new(parent, :admonition, buffer)
493
536
  attributes['style'] = admonition_style
494
- attributes['name'] = admonition_style.downcase
495
- attributes['caption'] ||= admonition_style.capitalize
537
+ attributes['name'] = admonition_name = admonition_style.downcase
538
+ attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
496
539
 
497
540
  elsif quote_context = [:quote, :verse].detect{|s| attributes[1] == s.to_s}
498
541
  # single-paragraph verse or quote is preceded by [verse] or [quote], respectively, and lasts until a blank line
499
542
  AttributeList.rekey(attributes, ['style', 'attribution', 'citetitle'])
500
- reader.unshift(this_line)
543
+ reader.unshift_line this_line
501
544
  buffer = reader.grab_lines_until(:break_on_blank_lines => true)
502
545
  buffer.last.chomp! unless buffer.empty?
503
546
  block = Block.new(parent, quote_context, buffer)
@@ -505,7 +548,7 @@ class Asciidoctor::Lexer
505
548
  # a floating (i.e., discrete) title
506
549
  elsif ['float', 'discrete'].include?(attributes[1]) && is_section_title?(this_line, reader.peek_line)
507
550
  attributes['style'] = attributes[1]
508
- reader.unshift this_line
551
+ reader.unshift_line this_line
509
552
  float_id, float_title, float_level, _ = parse_section_title reader
510
553
  block = Block.new(parent, :floating_title)
511
554
  if float_id.nil? || float_id.empty?
@@ -522,9 +565,9 @@ class Asciidoctor::Lexer
522
565
 
523
566
  # a paragraph - contiguous nonblank/noncontinuation lines
524
567
  else
525
- reader.unshift this_line
568
+ reader.unshift_line this_line
526
569
  buffer = reader.grab_lines_until(:break_on_blank_lines => true, :preserve_last_line => true, :skip_line_comments => true) {|line|
527
- delimited_block?(line) || line.match(REGEXP[:attr_line]) ||
570
+ is_delimited_block?(line) || line.match(REGEXP[:attr_line]) ||
528
571
  # next list item can be directly adjacent to paragraph of previous list item
529
572
  context == :dlist && line.match(REGEXP[:dlist])
530
573
  # not sure if there are any cases when we need this check for other list types
@@ -544,8 +587,8 @@ class Asciidoctor::Lexer
544
587
  buffer[0] = admonition.post_match
545
588
  block = Block.new(parent, :admonition, buffer)
546
589
  attributes['style'] = admonition[1]
547
- attributes['name'] = admonition[1].downcase
548
- attributes['caption'] ||= admonition[1].capitalize
590
+ attributes['name'] = admonition_name = admonition[1].downcase
591
+ attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
549
592
  else
550
593
  buffer.last.chomp!
551
594
  block = Block.new(parent, :paragraph, buffer)
@@ -578,17 +621,37 @@ class Asciidoctor::Lexer
578
621
  # Public: Determines whether this line is the start of any of the delimited blocks
579
622
  #
580
623
  # returns the match data if this line is the first line of a delimited block or nil if not
581
- #--
582
- # TODO could use the match value as a lookup for the block type so we don't have
583
- # to do any subsequent regexp
584
- def self.delimited_block?(line)
585
- # naive match
586
- #line.match(REGEXP[:any_blk])
587
-
588
- # attempt at better performance
589
- if line.length > 0
590
- # NOTE accessing the first element before calling ord is first Ruby 1.8.7 compat
591
- REGEXP[:any_blk_ord].include?(line[0..0][0].ord) ? line.match(REGEXP[:any_blk]) : nil
624
+ def self.is_delimited_block?(line, return_match_data = false)
625
+ line_len = line.length
626
+ # optimized for best performance
627
+ if line_len > 2
628
+ if line_len == 3
629
+ tip = line.chop
630
+ tl = 2
631
+ else
632
+ tip = line[0..3]
633
+ tl = 4
634
+
635
+ # special case for fenced code blocks
636
+ tip_alt = tip.chop
637
+ if tip_alt == '```' || tip_alt == '~~~'
638
+ tip = tip_alt
639
+ tl = 3
640
+ end
641
+ end
642
+
643
+ if DELIMITED_BLOCKS.has_key? tip
644
+ # if tip is the full line
645
+ if tl == line_len - 1
646
+ return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, tip) : true
647
+ elsif match = line.match(REGEXP[:any_blk])
648
+ return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, match[0]) : true
649
+ else
650
+ nil
651
+ end
652
+ else
653
+ nil
654
+ end
592
655
  else
593
656
  nil
594
657
  end
@@ -610,9 +673,9 @@ class Asciidoctor::Lexer
610
673
  else
611
674
  list_block.level = 1
612
675
  end
613
- Asciidoctor.debug { "Created #{list_type} block: #{list_block}" }
676
+ Debug.debug { "Created #{list_type} block: #{list_block}" }
614
677
 
615
- while reader.has_lines? && (match = reader.peek_line.match(REGEXP[list_type]))
678
+ while reader.has_more_lines? && (match = reader.peek_line.match(REGEXP[list_type]))
616
679
 
617
680
  marker = resolve_list_marker(list_type, match[1])
618
681
 
@@ -648,7 +711,7 @@ class Asciidoctor::Lexer
648
711
  items << list_item unless list_item.nil?
649
712
  list_item = nil
650
713
 
651
- reader.skip_blank
714
+ reader.skip_blank_lines
652
715
  end
653
716
 
654
717
  list_block
@@ -707,7 +770,7 @@ class Asciidoctor::Lexer
707
770
 
708
771
  begin
709
772
  pairs << next_list_item(reader, block, match, sibling_pattern)
710
- end while reader.has_lines? && match = reader.peek_line.match(sibling_pattern)
773
+ end while reader.has_more_lines? && match = reader.peek_line.match(sibling_pattern)
711
774
 
712
775
  block
713
776
  end
@@ -750,14 +813,19 @@ class Asciidoctor::Lexer
750
813
  # first skip the line with the marker / term
751
814
  reader.get_line
752
815
  list_item_reader = Reader.new grab_lines_for_list_item(reader, list_type, sibling_trait, has_text)
753
- if list_item_reader.has_lines?
816
+ if list_item_reader.has_more_lines?
754
817
  comment_lines = list_item_reader.consume_line_comments
755
818
  subsequent_line = list_item_reader.peek_line
756
819
  list_item_reader.unshift(*comment_lines) unless comment_lines.empty?
757
820
 
758
821
  if !subsequent_line.nil?
759
822
  continuation_connects_first_block = (subsequent_line == "\n")
760
- content_adjacent = !subsequent_line.strip.empty?
823
+ # if there's no continuation connecting the first block, then
824
+ # treat the lines as paragraph text (activated when has_text = false)
825
+ if !continuation_connects_first_block && list_type != :dlist
826
+ has_text = false
827
+ end
828
+ content_adjacent = !subsequent_line.chomp.empty?
761
829
  else
762
830
  continuation_connects_first_block = false
763
831
  content_adjacent = false
@@ -766,7 +834,7 @@ class Asciidoctor::Lexer
766
834
  # only relevant for :dlist
767
835
  options = {:text => !has_text}
768
836
 
769
- while list_item_reader.has_lines?
837
+ while list_item_reader.has_more_lines?
770
838
  new_block = next_block(list_item_reader, list_block, {}, options)
771
839
  list_item.blocks << new_block unless new_block.nil?
772
840
  end
@@ -815,7 +883,7 @@ class Asciidoctor::Lexer
815
883
  # it gets associated with the outermost block
816
884
  detached_continuation = nil
817
885
 
818
- while reader.has_lines?
886
+ while reader.has_more_lines?
819
887
  this_line = reader.get_line
820
888
 
821
889
  # if we've arrived at a sibling item in this list, we've captured
@@ -846,13 +914,12 @@ class Asciidoctor::Lexer
846
914
 
847
915
  # a delimited block immediately breaks the list unless preceded
848
916
  # by a list continuation (they are harsh like that ;0)
849
- if match = delimited_block?(this_line)
917
+ if match = is_delimited_block?(this_line, true)
850
918
  if continuation == :active
851
919
  buffer << this_line
852
920
  # grab all the lines in the block, leaving the delimiters in place
853
921
  # we're being more strict here about the terminator, but I think that's a good thing
854
- terminator = match[0]
855
- buffer.concat reader.grab_lines_until(:terminator => terminator, :grab_last_line => true)
922
+ buffer.concat reader.grab_lines_until(:terminator => match.terminator, :grab_last_line => true)
856
923
  continuation = :inactive
857
924
  else
858
925
  break
@@ -868,7 +935,7 @@ class Asciidoctor::Lexer
868
935
  # if we don't process it as a whole, then a line in it that looks like a
869
936
  # list item will throw off the exit from it
870
937
  if this_line.match(REGEXP[:lit_par])
871
- reader.unshift this_line
938
+ reader.unshift_line this_line
872
939
  buffer.concat reader.grab_lines_until(
873
940
  :preserve_last_line => true,
874
941
  :break_on_blank_lines => true,
@@ -879,7 +946,7 @@ class Asciidoctor::Lexer
879
946
  }
880
947
  continuation = :inactive
881
948
  # let block metadata play out until we find the block
882
- elsif this_line.match(REGEXP[:blk_title]) || this_line.match(REGEXP[:attr_line])
949
+ elsif this_line.match(REGEXP[:blk_title]) || this_line.match(REGEXP[:attr_line]) || this_line.match(REGEXP[:attr_entry])
883
950
  buffer << this_line
884
951
  else
885
952
  if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).detect {|ctx| this_line.match(REGEXP[ctx]) }
@@ -911,7 +978,7 @@ class Asciidoctor::Lexer
911
978
  if has_text
912
979
  # slurp up any literal paragraph offset by blank lines
913
980
  if this_line.match(REGEXP[:lit_par])
914
- reader.unshift this_line
981
+ reader.unshift_line this_line
915
982
  buffer.concat reader.grab_lines_until(
916
983
  :preserve_last_line => true,
917
984
  :break_on_blank_lines => true,
@@ -955,20 +1022,21 @@ class Asciidoctor::Lexer
955
1022
  this_line = nil
956
1023
  end
957
1024
 
958
- reader.unshift this_line if !this_line.nil?
1025
+ reader.unshift_line this_line if !this_line.nil?
959
1026
 
960
1027
  if detached_continuation
961
1028
  buffer.delete_at detached_continuation
962
1029
  end
963
1030
 
964
1031
  # strip trailing blank lines to prevent empty blocks
965
- buffer.pop while !buffer.empty? && buffer.last.strip.empty?
1032
+ buffer.pop while !buffer.empty? && buffer.last.chomp.empty?
966
1033
 
967
1034
  # We do need to replace the optional trailing continuation
968
1035
  # a blank line would have served the same purpose in the document
969
1036
  if !buffer.empty? && buffer.last.chomp == LIST_CONTINUATION
970
1037
  buffer.pop
971
1038
  end
1039
+
972
1040
  #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.join}<BUFFER"
973
1041
  #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer}<BUFFER"
974
1042
 
@@ -997,14 +1065,19 @@ class Asciidoctor::Lexer
997
1065
  if attributes[1]
998
1066
  section.sectname = attributes[1]
999
1067
  section.special = true
1000
- if section.sectname == 'appendix'
1001
- attributes['caption'] ||= "Appendix #{parent.document.counter('appendix-number', 'A')}: "
1068
+ document = parent.document
1069
+ if section.sectname == 'appendix' &&
1070
+ !attributes.has_key?('caption') &&
1071
+ !document.attributes.has_key?('caption')
1072
+ number = document.counter('appendix-number', 'A')
1073
+ attributes['caption'] = "#{document.attributes['appendix-caption']} #{number}: "
1074
+ Document::AttributeEntry.new('appendix-number', number).save_to(attributes)
1002
1075
  end
1003
1076
  else
1004
1077
  section.sectname = "sect#{section.level}"
1005
1078
  end
1006
1079
  section.update_attributes(attributes)
1007
- reader.skip_blank
1080
+ reader.skip_blank_lines
1008
1081
 
1009
1082
  section
1010
1083
  end
@@ -1014,7 +1087,7 @@ class Asciidoctor::Lexer
1014
1087
  #
1015
1088
  # line - the String line from under the section title.
1016
1089
  def self.section_level(line)
1017
- char = line.strip.chars.to_a.uniq
1090
+ char = line.chomp.chars.to_a.uniq
1018
1091
  case char
1019
1092
  when ['=']; 0
1020
1093
  when ['-']; 1
@@ -1032,21 +1105,25 @@ class Asciidoctor::Lexer
1032
1105
 
1033
1106
  # Internal: Checks if the next line on the Reader is a section title
1034
1107
  #
1035
- # reader - the source Reader
1108
+ # reader - the source Reader
1109
+ # attributes - a Hash of attributes collected above the current line
1036
1110
  #
1037
1111
  # returns the section level if the Reader is positioned at a section title,
1038
1112
  # false otherwise
1039
1113
  def self.is_next_line_section?(reader, attributes)
1040
1114
  return false if !attributes[1].nil? && ['float', 'discrete'].include?(attributes[1])
1041
- if reader.has_lines?
1042
- line1 = reader.get_line
1043
- line2 = reader.peek_line
1044
- reader.unshift line1
1045
- else
1046
- return false
1047
- end
1115
+ return false if !reader.has_more_lines?
1116
+ is_section_title?(*reader.peek_lines(2))
1117
+ end
1048
1118
 
1049
- is_section_title?(line1, line2)
1119
+ # Internal: Convenience API for checking if the next line on the Reader is the document title
1120
+ #
1121
+ # reader - the source Reader
1122
+ # attributes - a Hash of attributes collected above the current line
1123
+ #
1124
+ # returns true if the Reader is positioned at the document title, false otherwise
1125
+ def self.is_next_line_document_title?(reader, attributes)
1126
+ is_next_line_section?(reader, attributes) == 0
1050
1127
  end
1051
1128
 
1052
1129
  # Public: Checks if these lines are a section title
@@ -1172,15 +1249,14 @@ class Asciidoctor::Lexer
1172
1249
  # # => {'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org',
1173
1250
  # # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.'}
1174
1251
  def self.parse_header_metadata(reader, document = nil)
1175
- # capture consecutive comment lines so we can reinsert them after the header
1176
- comment_lines = reader.consume_comments
1252
+ # NOTE this will discard away any comment lines, but not skip blank lines
1253
+ process_attribute_entries(reader, document)
1254
+
1255
+ metadata = {}
1177
1256
 
1178
- metadata = !document.nil? ? document.attributes : {}
1179
- author_initials = metadata['authorinitials']
1180
- if reader.has_lines? && !reader.peek_line.strip.empty?
1257
+ if reader.has_more_lines? && !reader.peek_line.chomp.empty?
1181
1258
  author_line = reader.get_line
1182
- match = author_line.match(REGEXP[:author_info])
1183
- if match
1259
+ if match = author_line.match(REGEXP[:author_info])
1184
1260
  metadata['firstname'] = fname = match[1].tr('_', ' ')
1185
1261
  metadata['author'] = fname
1186
1262
  metadata['authorinitials'] = fname[0, 1]
@@ -1200,28 +1276,36 @@ class Asciidoctor::Lexer
1200
1276
  metadata['authorinitials'] = metadata['firstname'][0, 1]
1201
1277
  end
1202
1278
 
1203
- # hack because of incorrect order of attribute processing
1204
- metadata['authorinitials'] = author_initials unless author_initials.nil?
1279
+ # NOTE this will discard away any comment lines, but not skip blank lines
1280
+ process_attribute_entries(reader, document)
1205
1281
 
1206
- # capture consecutive comment lines so we can reinsert them after the header
1207
- comment_lines += reader.consume_comments
1208
-
1209
- if reader.has_lines? && !reader.peek_line.strip.empty?
1282
+ if reader.has_more_lines? && !reader.peek_line.chomp.empty?
1210
1283
  rev_line = reader.get_line
1211
- match = rev_line.match(REGEXP[:revision_info])
1212
- if match
1213
- metadata['revdate'] = match[2]
1214
- metadata['revnumber'] = match[1] unless match[1].nil?
1215
- metadata['revremark'] = match[3] unless match[3].nil?
1284
+ if match = rev_line.match(REGEXP[:revision_info])
1285
+ metadata['revdate'] = match[2].strip
1286
+ metadata['revnumber'] = match[1].rstrip unless match[1].nil?
1287
+ metadata['revremark'] = match[3].rstrip unless match[3].nil?
1216
1288
  else
1217
- metadata['revdate'] = rev_line.strip
1289
+ # throw it back
1290
+ reader.unshift_line rev_line
1218
1291
  end
1219
1292
  end
1220
1293
 
1221
- reader.skip_blank
1294
+ # NOTE this will discard away any comment lines, but not skip blank lines
1295
+ process_attribute_entries(reader, document)
1296
+
1297
+ reader.skip_blank_lines
1298
+
1299
+ # apply header subs and assign to document
1300
+ if !document.nil?
1301
+ metadata.map do |key, val|
1302
+ val = document.apply_header_subs(val)
1303
+ document.attributes[key] = val if !document.attributes.has_key?(key)
1304
+ val
1305
+ end
1306
+ end
1222
1307
  end
1223
1308
 
1224
- reader.unshift(*comment_lines)
1225
1309
  metadata
1226
1310
  end
1227
1311
 
@@ -1241,7 +1325,7 @@ class Asciidoctor::Lexer
1241
1325
  def self.parse_block_metadata_lines(reader, parent, attributes = {}, options = {})
1242
1326
  while parse_block_metadata_line(reader, parent, attributes, options)
1243
1327
  # discard the line just processed
1244
- reader.next_line
1328
+ reader.advance
1245
1329
  reader.skip_blank_lines
1246
1330
  end
1247
1331
  attributes
@@ -1268,15 +1352,15 @@ class Asciidoctor::Lexer
1268
1352
  #
1269
1353
  # returns true if the line contains metadata, otherwise false
1270
1354
  def self.parse_block_metadata_line(reader, parent, attributes, options = {})
1271
- return false if !reader.has_lines?
1355
+ return false if !reader.has_more_lines?
1272
1356
  next_line = reader.peek_line
1273
- if next_line.match(REGEXP[:comment])
1274
- # do nothing, we'll skip it
1275
- # QUESTION should we parse block comments here instead of next_block?
1276
- # disable until we can agree what the current line is coming in
1277
- elsif match = next_line.match(REGEXP[:comment_blk])
1357
+ if (commentish = next_line.start_with?('//')) && (match = next_line.match(REGEXP[:comment_blk]))
1278
1358
  terminator = match[0]
1279
- reader.grab_lines_until(:skip_first_line => true, :preserve_last_line => true, :terminator => terminator)
1359
+ reader.grab_lines_until(:skip_first_line => true, :preserve_last_line => true, :terminator => terminator, :preprocess => false)
1360
+ elsif commentish && next_line.match(REGEXP[:comment])
1361
+ # do nothing, we'll skip it
1362
+ elsif !options[:text] && (match = next_line.match(REGEXP[:attr_entry]))
1363
+ process_attribute_entry(reader, parent, attributes, match)
1280
1364
  elsif match = next_line.match(REGEXP[:anchor])
1281
1365
  id, reftext = match[1].split(',')
1282
1366
  attributes['id'] = id
@@ -1290,7 +1374,6 @@ class Asciidoctor::Lexer
1290
1374
  elsif match = next_line.match(REGEXP[:blk_attr_list])
1291
1375
  AttributeList.new(parent.document.sub_attributes(match[1]), parent.document).parse_into(attributes)
1292
1376
  # NOTE title doesn't apply to section, but we need to stash it for the first block
1293
- # TODO need test for this getting passed on to first block after section if found above section
1294
1377
  # TODO should issue an error if this is found above the document title
1295
1378
  elsif !options[:text] && (match = next_line.match(REGEXP[:blk_title]))
1296
1379
  attributes['title'] = match[1]
@@ -1301,6 +1384,57 @@ class Asciidoctor::Lexer
1301
1384
  true
1302
1385
  end
1303
1386
 
1387
+ def self.process_attribute_entries(reader, parent, attributes = nil)
1388
+ reader.skip_comment_lines
1389
+ while process_attribute_entry(reader, parent, attributes)
1390
+ # discard line just processed
1391
+ reader.advance
1392
+ reader.skip_comment_lines
1393
+ end
1394
+ end
1395
+
1396
+ def self.process_attribute_entry(reader, parent, attributes = nil, match = nil)
1397
+ match ||= reader.has_more_lines? ? reader.peek_line.match(REGEXP[:attr_entry]) : nil
1398
+ if match
1399
+ name = match[1]
1400
+ value = match[2].nil? ? '' : match[2]
1401
+ if value.end_with? LINE_BREAK
1402
+ value.chop!.rstrip!
1403
+ while reader.advance
1404
+ next_line = reader.peek_line.strip
1405
+ break if next_line.empty?
1406
+ if next_line.end_with? LINE_BREAK
1407
+ value = "#{value} #{next_line.chop.rstrip}"
1408
+ else
1409
+ value = "#{value} #{next_line}"
1410
+ break
1411
+ end
1412
+ end
1413
+ end
1414
+
1415
+ if name.end_with?('!')
1416
+ # a nil value signals the attribute should be deleted (undefined)
1417
+ value = nil
1418
+ name = name.chop
1419
+ end
1420
+
1421
+ name = sanitize_attribute_name(name)
1422
+ accessible = true
1423
+ if !parent.nil?
1424
+ accessible = value.nil? ?
1425
+ parent.document.delete_attribute(name) :
1426
+ parent.document.set_attribute(name, value)
1427
+ end
1428
+
1429
+ if !attributes.nil?
1430
+ Document::AttributeEntry.new(name, value).save_to(attributes) if accessible
1431
+ end
1432
+ true
1433
+ else
1434
+ false
1435
+ end
1436
+ end
1437
+
1304
1438
  # Internal: Resolve the 0-index marker for this list item
1305
1439
  #
1306
1440
  # For ordered lists, match the marker used for this list item against the
@@ -1387,6 +1521,7 @@ class Asciidoctor::Lexer
1387
1521
  end
1388
1522
 
1389
1523
  if validate && expected != actual
1524
+ # FIXME I need a reader reference or line number to report line number
1390
1525
  puts "asciidoctor: WARNING: list item index: expected #{expected}, got #{actual}"
1391
1526
  end
1392
1527
 
@@ -1441,8 +1576,8 @@ class Asciidoctor::Lexer
1441
1576
 
1442
1577
  table_reader.skip_blank_lines
1443
1578
 
1444
- parser_ctx = Asciidoctor::Table::ParserContext.new(table, attributes)
1445
- while table_reader.has_lines?
1579
+ parser_ctx = Table::ParserContext.new(table, attributes)
1580
+ while table_reader.has_more_lines?
1446
1581
  line = table_reader.get_line
1447
1582
 
1448
1583
  if parser_ctx.format == 'psv'
@@ -1507,7 +1642,7 @@ class Asciidoctor::Lexer
1507
1642
 
1508
1643
  table_reader.skip_blank_lines unless parser_ctx.cell_open?
1509
1644
 
1510
- if !table_reader.has_lines?
1645
+ if !table_reader.has_more_lines?
1511
1646
  parser_ctx.close_cell true
1512
1647
  end
1513
1648
  end
@@ -1597,7 +1732,7 @@ class Asciidoctor::Lexer
1597
1732
 
1598
1733
  if m = line.match(REGEXP[:table_cellspec][pos])
1599
1734
  spec = {}
1600
- return [spec, line] if m[0].strip.empty?
1735
+ return [spec, line] if m[0].chomp.empty?
1601
1736
  rest = (pos == :start ? m.post_match : m.pre_match)
1602
1737
  if m[1]
1603
1738
  colspec, rowspec = m[1].split '.'
@@ -1629,6 +1764,26 @@ class Asciidoctor::Lexer
1629
1764
  [spec, rest]
1630
1765
  end
1631
1766
 
1767
+ # Public: Convert a string to a legal attribute name.
1768
+ #
1769
+ # name - the String name of the attribute
1770
+ #
1771
+ # Returns a String with the legal AsciiDoc attribute name.
1772
+ #
1773
+ # Examples
1774
+ #
1775
+ # sanitize_attribute_name('Foo Bar')
1776
+ # => 'foobar'
1777
+ #
1778
+ # sanitize_attribute_name('foo')
1779
+ # => 'foo'
1780
+ #
1781
+ # sanitize_attribute_name('Foo 3 #-Billy')
1782
+ # => 'foo3-billy'
1783
+ def self.sanitize_attribute_name(name)
1784
+ name.gsub(REGEXP[:illegal_attr_name_chars], '').downcase
1785
+ end
1786
+
1632
1787
  # Internal: Converts a Roman numeral to an integer value.
1633
1788
  #
1634
1789
  # value - The String Roman numeral to convert
@@ -1651,3 +1806,4 @@ class Asciidoctor::Lexer
1651
1806
  result
1652
1807
  end
1653
1808
  end
1809
+ end