asciidoctor 0.1.3 → 0.1.4

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +387 -0
  3. data/README.adoc +358 -348
  4. data/asciidoctor.gemspec +30 -9
  5. data/bin/asciidoctor +3 -0
  6. data/bin/asciidoctor-safe +3 -0
  7. data/compat/asciidoc.conf +76 -4
  8. data/lib/asciidoctor.rb +174 -79
  9. data/lib/asciidoctor/abstract_block.rb +131 -101
  10. data/lib/asciidoctor/abstract_node.rb +108 -26
  11. data/lib/asciidoctor/attribute_list.rb +1 -1
  12. data/lib/asciidoctor/backends/_stylesheets.rb +204 -62
  13. data/lib/asciidoctor/backends/base_template.rb +11 -22
  14. data/lib/asciidoctor/backends/docbook45.rb +158 -163
  15. data/lib/asciidoctor/backends/docbook5.rb +103 -0
  16. data/lib/asciidoctor/backends/html5.rb +662 -445
  17. data/lib/asciidoctor/block.rb +54 -44
  18. data/lib/asciidoctor/cli/invoker.rb +41 -20
  19. data/lib/asciidoctor/cli/options.rb +66 -20
  20. data/lib/asciidoctor/debug.rb +1 -1
  21. data/lib/asciidoctor/document.rb +265 -100
  22. data/lib/asciidoctor/extensions.rb +443 -0
  23. data/lib/asciidoctor/helpers.rb +38 -6
  24. data/lib/asciidoctor/inline.rb +5 -5
  25. data/lib/asciidoctor/lexer.rb +532 -250
  26. data/lib/asciidoctor/{list_item.rb → list.rb} +33 -13
  27. data/lib/asciidoctor/path_resolver.rb +28 -2
  28. data/lib/asciidoctor/reader.rb +814 -455
  29. data/lib/asciidoctor/renderer.rb +128 -42
  30. data/lib/asciidoctor/section.rb +55 -41
  31. data/lib/asciidoctor/substituters.rb +380 -107
  32. data/lib/asciidoctor/table.rb +40 -30
  33. data/lib/asciidoctor/version.rb +1 -1
  34. data/man/asciidoctor.1 +32 -96
  35. data/man/{asciidoctor.ad → asciidoctor.adoc} +57 -48
  36. data/test/attributes_test.rb +200 -27
  37. data/test/blocks_test.rb +361 -22
  38. data/test/document_test.rb +496 -81
  39. data/test/extensions_test.rb +448 -0
  40. data/test/fixtures/basic-docinfo-footer.html +6 -0
  41. data/test/fixtures/basic-docinfo-footer.xml +8 -0
  42. data/test/fixtures/basic-docinfo.xml +3 -3
  43. data/test/fixtures/basic.asciidoc +1 -0
  44. data/test/fixtures/child-include.adoc +5 -0
  45. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
  46. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
  47. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
  48. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
  49. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
  50. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
  51. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
  52. data/test/fixtures/docinfo-footer.html +1 -0
  53. data/test/fixtures/docinfo-footer.xml +9 -0
  54. data/test/fixtures/docinfo.xml +1 -0
  55. data/test/fixtures/grandchild-include.adoc +3 -0
  56. data/test/fixtures/parent-include-restricted.adoc +5 -0
  57. data/test/fixtures/parent-include.adoc +5 -0
  58. data/test/invoker_test.rb +82 -8
  59. data/test/lexer_test.rb +21 -3
  60. data/test/links_test.rb +34 -2
  61. data/test/lists_test.rb +304 -7
  62. data/test/options_test.rb +19 -3
  63. data/test/paragraphs_test.rb +13 -0
  64. data/test/paths_test.rb +22 -0
  65. data/test/preamble_test.rb +20 -0
  66. data/test/reader_test.rb +1096 -644
  67. data/test/renderer_test.rb +152 -12
  68. data/test/sections_test.rb +417 -76
  69. data/test/substitutions_test.rb +339 -138
  70. data/test/tables_test.rb +109 -4
  71. data/test/test_helper.rb +79 -13
  72. data/test/text_test.rb +111 -11
  73. metadata +54 -18
@@ -1,6 +1,9 @@
1
1
  module Asciidoctor
2
2
  # Public: Methods for managing inline elements in AsciiDoc block
3
3
  class Inline < AbstractNode
4
+ # Public: Get/Set the String name of the render template
5
+ attr_accessor :template_name
6
+
4
7
  # Public: Get the text of this inline element
5
8
  attr_reader :text
6
9
 
@@ -12,13 +15,10 @@ class Inline < AbstractNode
12
15
 
13
16
  def initialize(parent, context, text = nil, opts = {})
14
17
  super(parent, context)
18
+ @template_name = "inline_#{context}"
15
19
 
16
20
  @text = text
17
21
 
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
22
  @id = opts[:id]
23
23
  @type = opts[:type]
24
24
  @target = opts[:target]
@@ -29,7 +29,7 @@ class Inline < AbstractNode
29
29
  end
30
30
 
31
31
  def render
32
- renderer.render("inline_#{@context}", self).chomp
32
+ renderer.render(@template_name, self).chomp
33
33
  end
34
34
 
35
35
  end
@@ -77,10 +77,7 @@ class Lexer
77
77
  # special case, block title is not allowed above document title,
78
78
  # carry attributes over to the document body
79
79
  if block_attributes.has_key?('title')
80
- document.clear_playback_attributes block_attributes
81
- document.save_attributes
82
- block_attributes['invalid-header'] = true
83
- return block_attributes
80
+ return document.finalize_header block_attributes, false
84
81
  end
85
82
 
86
83
  # yep, document title logic in AsciiDoc is just insanity
@@ -101,7 +98,7 @@ class Lexer
101
98
  assigned_doctitle = doctitle
102
99
  end
103
100
  document.attributes['doctitle'] = section_title = doctitle
104
- # QUESTION: should this be encapsulated in document?
101
+ # QUESTION: should the id assignment on Document be encapsulated in the Document class?
105
102
  if document.id.nil? && block_attributes.has_key?('id')
106
103
  document.id = block_attributes.delete('id')
107
104
  end
@@ -118,13 +115,50 @@ class Lexer
118
115
  if assigned_doctitle
119
116
  document.attributes['doctitle'] = assigned_doctitle
120
117
  end
118
+
119
+ # parse title and consume name section of manpage document
120
+ parse_manpage_header(reader, document) if document.doctype == 'manpage'
121
121
 
122
- document.clear_playback_attributes block_attributes
123
- document.save_attributes
124
-
125
- # NOTE these are the block-level attributes (not document attributes) that
122
+ # NOTE block_attributes are the block-level attributes (not document attributes) that
126
123
  # precede the first line of content (document title, first section or first block)
127
- block_attributes
124
+ document.finalize_header block_attributes
125
+ end
126
+
127
+ # Public: Parses the manpage header of the AsciiDoc source read from the Reader
128
+ #
129
+ # returns Nothing
130
+ def self.parse_manpage_header(reader, document)
131
+ if (m = document.attributes['doctitle'].match(REGEXP[:mantitle_manvolnum]))
132
+ document.attributes['mantitle'] = document.sub_attributes(m[1].rstrip.downcase)
133
+ document.attributes['manvolnum'] = m[2].strip
134
+ else
135
+ warn "asciidoctor: ERROR: #{reader.prev_line_info}: malformed manpage title"
136
+ end
137
+
138
+ reader.skip_blank_lines
139
+
140
+ if is_next_line_section?(reader, {})
141
+ name_section = initialize_section(reader, document, {})
142
+ if name_section.level == 1
143
+ name_section_buffer = reader.read_lines_until(:break_on_blank_lines => true).join.tr_s("\n ", ' ')
144
+ if (m = name_section_buffer.match(REGEXP[:manname_manpurpose]))
145
+ document.attributes['manname'] = m[1]
146
+ document.attributes['manpurpose'] = m[2]
147
+ # TODO parse multiple man names
148
+
149
+ if document.backend == 'manpage'
150
+ document.attributes['docname'] = document.attributes['manname']
151
+ document.attributes['outfilesuffix'] = ".#{document.attributes['manvolnum']}"
152
+ end
153
+ else
154
+ warn "asciidoctor: ERROR: #{reader.prev_line_info}: malformed name section body"
155
+ end
156
+ else
157
+ warn "asciidoctor: ERROR: #{reader.prev_line_info}: name section title must be at level 1"
158
+ end
159
+ else
160
+ warn "asciidoctor: ERROR: #{reader.prev_line_info}: name section expected"
161
+ end
128
162
  end
129
163
 
130
164
  # Public: Return the next section from the Reader.
@@ -176,7 +210,7 @@ class Lexer
176
210
  (parent.has_header? || attributes.delete('invalid-header') || !is_next_line_section?(reader, attributes))
177
211
 
178
212
  if parent.has_header?
179
- preamble = Block.new(parent, :preamble)
213
+ preamble = Block.new(parent, :preamble, :content_model => :compound)
180
214
  parent << preamble
181
215
  end
182
216
  section = parent
@@ -225,9 +259,9 @@ class Lexer
225
259
  doctype = parent.document.doctype
226
260
  if next_level > current_level || (section.is_a?(Document) && next_level == 0)
227
261
  if next_level == 0 && doctype != 'book'
228
- puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
262
+ warn "asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections"
229
263
  elsif !expected_next_levels.nil? && !expected_next_levels.include?(next_level)
230
- puts "asciidoctor: WARNING: line #{reader.lineno + 1}: section title out of sequence: " +
264
+ warn "asciidoctor: WARNING: #{reader.line_info}: section title out of sequence: " +
231
265
  "expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, " +
232
266
  "got level #{next_level}"
233
267
  end
@@ -236,7 +270,7 @@ class Lexer
236
270
  section << new_section
237
271
  else
238
272
  if next_level == 0 && doctype != 'book'
239
- puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
273
+ warn "asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections"
240
274
  end
241
275
  # close this section (and break out of the nesting) to begin a new one
242
276
  break
@@ -258,7 +292,7 @@ class Lexer
258
292
 
259
293
  if preamble && !preamble.blocks?
260
294
  # drop the preamble if it has no content
261
- section.delete_at(0)
295
+ section.blocks.delete_at(0)
262
296
  end
263
297
 
264
298
  # The attributes returned here are orphaned attributes that fall at the end
@@ -305,7 +339,14 @@ class Lexer
305
339
  #parse_sections = options.fetch(:parse_sections, false)
306
340
 
307
341
  document = parent.document
308
- parent_context = parent.is_a?(Block) ? parent.context : nil
342
+ if (extensions = document.extensions)
343
+ block_extensions = extensions.blocks?
344
+ macro_extensions = extensions.block_macros?
345
+ else
346
+ block_extensions = macro_extensions = false
347
+ end
348
+ #parent_context = parent.is_a?(Block) ? parent.context : nil
349
+ in_list = parent.is_a?(List)
309
350
  block = nil
310
351
  style = nil
311
352
  explicit_style = nil
@@ -320,19 +361,20 @@ class Lexer
320
361
  # break
321
362
  end
322
363
 
323
- # QUESTION introduce parsing context object?
324
- this_line = reader.get_line
364
+ # QUESTION should we introduce a parsing context object?
365
+ this_line = reader.read_line
325
366
  delimited_block = false
326
367
  block_context = nil
368
+ cloaked_context = nil
327
369
  terminator = nil
328
370
  # QUESTION put this inside call to rekey attributes?
329
371
  if attributes[1]
330
- style, explicit_style = parse_style_attribute(attributes)
372
+ style, explicit_style = parse_style_attribute(attributes, reader)
331
373
  end
332
374
 
333
375
  if delimited_blk_match = is_delimited_block?(this_line, true)
334
376
  delimited_block = true
335
- block_context = delimited_blk_match.context
377
+ block_context = cloaked_context = delimited_blk_match.context
336
378
  terminator = delimited_blk_match.terminator
337
379
  if !style
338
380
  style = attributes['style'] = block_context.to_s
@@ -341,8 +383,10 @@ class Lexer
341
383
  block_context = style.to_sym
342
384
  elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
343
385
  block_context = :admonition
386
+ elsif block_extensions && extensions.processor_registered_for_block?(style, block_context)
387
+ block_context = style.to_sym
344
388
  else
345
- puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for #{block_context} block: #{style}"
389
+ warn "asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for #{block_context} block: #{style}"
346
390
  style = block_context.to_s
347
391
  end
348
392
  end
@@ -365,16 +409,17 @@ class Lexer
365
409
  end
366
410
 
367
411
  # process lines normally
368
- if !text_only
412
+ unless text_only
413
+ first_char = Compliance.markdown_syntax ? this_line.lstrip[0..0] : this_line[0..0]
369
414
  # NOTE we're letting break lines (ruler, page_break, etc) have attributes
370
- if (match = this_line.match(REGEXP[:break_line]))
371
- block = Block.new(parent, BREAK_LINES[match[0][0..2]])
415
+ if BREAK_LINES.has_key?(first_char) && this_line.length > 3 &&
416
+ (match = this_line.match(Compliance.markdown_syntax ? REGEXP[:break_line_plus] : REGEXP[:break_line]))
417
+ block = Block.new(parent, BREAK_LINES[first_char], :content_model => :empty)
372
418
  break
373
419
 
374
- # TODO make this a media_blk and handle image, video & audio
375
420
  elsif (match = this_line.match(REGEXP[:media_blk_macro]))
376
421
  blk_ctx = match[1].to_sym
377
- block = Block.new(parent, blk_ctx)
422
+ block = Block.new(parent, blk_ctx, :content_model => :empty)
378
423
  if blk_ctx == :image
379
424
  posattrs = ['alt', 'width', 'height']
380
425
  elsif blk_ctx == :video
@@ -394,53 +439,74 @@ class Lexer
394
439
  :sub_input => true,
395
440
  :sub_result => false,
396
441
  :into => attributes)
397
- target = block.sub_attributes(match[2])
442
+ target = block.sub_attributes(match[2], :attribute_missing => 'drop-line')
398
443
  if target.empty?
399
- # drop the line if target resolves to nothing
400
- return nil
444
+ if document.attributes.fetch('attribute-missing', COMPLIANCE[:attribute_missing]) == 'skip'
445
+ # retain as unparsed
446
+ return Block.new(parent, :paragraph, :source => [this_line.chomp])
447
+ else
448
+ # drop the line if target resolves to nothing
449
+ return nil
450
+ end
401
451
  end
402
452
 
403
453
  attributes['target'] = target
404
454
  block.title = attributes.delete('title') if attributes.has_key?('title')
405
455
  if blk_ctx == :image
406
456
  document.register(:images, target)
407
- attributes['alt'] ||= File.basename(target, File.extname(target))
457
+ attributes['alt'] ||= File.basename(target, File.extname(target)).tr('_-', ' ')
408
458
  # QUESTION should video or audio have an auto-numbered caption?
409
459
  block.assign_caption attributes.delete('caption'), 'figure'
410
460
  end
411
461
  break
412
462
 
413
463
  # NOTE we're letting the toc macro have attributes
414
- elsif (match = this_line.match(REGEXP[:toc]))
415
- block = Block.new(parent, :toc)
464
+ elsif first_char == 't' && (match = this_line.match(REGEXP[:toc]))
465
+ block = Block.new(parent, :toc, :content_model => :empty)
416
466
  block.parse_attributes(match[1], [], :sub_result => false, :into => attributes)
417
467
  break
418
468
 
469
+ elsif macro_extensions && (match = this_line.match(REGEXP[:generic_blk_macro])) &&
470
+ extensions.processor_registered_for_block_macro?(match[1])
471
+ name = match[1]
472
+ target = match[2]
473
+ raw_attributes = match[3]
474
+ processor = extensions.load_block_macro_processor name, document
475
+ unless raw_attributes.empty?
476
+ document.parse_attributes(raw_attributes, processor.options.fetch(:pos_attrs, []),
477
+ :sub_input => true, :sub_result => false, :into => attributes)
478
+ end
479
+ if !(default_attrs = processor.options.fetch(:default_attrs, {})).empty?
480
+ default_attrs.each {|k, v| attributes[k] ||= v }
481
+ end
482
+ block = processor.process parent, target, attributes
483
+ return nil if block.nil?
484
+ break
419
485
  end
420
486
  end
421
487
 
422
488
  # haven't found anything yet, continue
423
489
  if (match = this_line.match(REGEXP[:colist]))
424
- block = Block.new(parent, :colist)
490
+ block = List.new(parent, :colist)
425
491
  attributes['style'] = 'arabic'
426
- items = []
427
- block.buffer = items
428
492
  reader.unshift_line this_line
429
493
  expected_index = 1
430
494
  begin
431
495
  # might want to move this check to a validate method
432
496
  if match[1].to_i != expected_index
433
- puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}"
497
+ # FIXME this lineno - 2 hack means we need a proper look-behind cursor
498
+ warn "asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: callout list item index: expected #{expected_index} got #{match[1]}"
434
499
  end
435
500
  list_item = next_list_item(reader, block, match)
436
501
  expected_index += 1
437
502
  if !list_item.nil?
438
- items << list_item
439
- coids = document.callouts.callout_ids(items.size)
503
+ block << list_item
504
+ coids = document.callouts.callout_ids(block.items.size)
440
505
  if !coids.empty?
441
506
  list_item.attributes['coids'] = coids
442
507
  else
443
- puts "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{items.size}"
508
+ # FIXME this lineno - 2 hack means we need a proper look-behind cursor
509
+ warn "asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: no callouts refer to list item #{block.items.size}"
444
510
  end
445
511
  end
446
512
  end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist])
@@ -457,10 +523,10 @@ class Lexer
457
523
  reader.unshift_line this_line
458
524
  block = next_outline_list(reader, :olist, parent)
459
525
  # QUESTION move this logic to next_outline_list?
460
- if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style')
461
- marker = block.buffer.first.marker
526
+ if !attributes['style'] && !block.attributes['style']
527
+ marker = block.items.first.marker
462
528
  if marker.start_with? '.'
463
- # first one makes more sense, but second on is AsciiDoc-compliant
529
+ # first one makes more sense, but second one is AsciiDoc-compliant
464
530
  #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s
465
531
  attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s
466
532
  else
@@ -475,11 +541,12 @@ class Lexer
475
541
  block = next_labeled_list(reader, match, parent)
476
542
  break
477
543
 
478
- elsif (style == 'float' || style == 'discrete') && is_section_title?(this_line, reader.peek_line)
544
+ elsif (style == 'float' || style == 'discrete') &&
545
+ is_section_title?(this_line, (Compliance.underline_style_section_titles ? reader.peek_line(true) : nil))
479
546
  reader.unshift_line this_line
480
547
  float_id, float_title, float_level, _ = parse_section_title(reader, document)
481
548
  float_id ||= attributes['id'] if attributes.has_key?('id')
482
- block = Block.new(parent, :floating_title)
549
+ block = Block.new(parent, :floating_title, :content_model => :empty)
483
550
  if float_id.nil? || float_id.empty?
484
551
  # FIXME remove hack of creating throwaway Section to get at the generate_id method
485
552
  tmp_sect = Section.new(parent)
@@ -494,31 +561,40 @@ class Lexer
494
561
  break
495
562
 
496
563
  # FIXME create another set for "passthrough" styles
564
+ # FIXME make this more DRY!
497
565
  elsif !style.nil? && style != 'normal'
498
566
  if PARAGRAPH_STYLES.include?(style)
499
567
  block_context = style.to_sym
568
+ cloaked_context = :paragraph
500
569
  reader.unshift_line this_line
501
570
  # advance to block parsing =>
502
571
  break
503
572
  elsif ADMONITION_STYLES.include?(style)
504
573
  block_context = :admonition
574
+ cloaked_context = :paragraph
575
+ reader.unshift_line this_line
576
+ # advance to block parsing =>
577
+ break
578
+ elsif block_extensions && extensions.processor_registered_for_block?(style, :paragraph)
579
+ block_context = style.to_sym
580
+ cloaked_context = :paragraph
505
581
  reader.unshift_line this_line
506
582
  # advance to block parsing =>
507
583
  break
508
584
  else
509
- puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for paragraph: #{style}"
585
+ warn "asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for paragraph: #{style}"
510
586
  style = nil
511
587
  # continue to process paragraph
512
588
  end
513
589
  end
514
590
 
515
- break_at_list = (skipped == 0 && parent_context.to_s.end_with?('list'))
591
+ break_at_list = (skipped == 0 && in_list)
516
592
 
517
593
  # a literal paragraph is contiguous lines starting at least one space
518
594
  if style != 'normal' && this_line.match(REGEXP[:lit_par])
519
- # So we need to actually include this one in the grab_lines group
595
+ # So we need to actually include this one in the read_lines group
520
596
  reader.unshift_line this_line
521
- buffer = reader.grab_lines_until(
597
+ lines = reader.read_lines_until(
522
598
  :break_on_blank_lines => true,
523
599
  :break_on_list_continuation => true,
524
600
  :preserve_last_line => true) {|line|
@@ -530,20 +606,17 @@ class Lexer
530
606
  (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
531
607
  }
532
608
 
533
- reset_block_indent! buffer
609
+ reset_block_indent! lines
534
610
 
535
- block = Block.new(parent, :literal, buffer)
611
+ block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes)
536
612
  # a literal gets special meaning inside of a definition list
537
- if LIST_CONTEXTS.include?(parent_context)
538
- attributes['options'] ||= []
539
- # TODO this feels hacky, better way to distinguish from explicit literal block?
540
- attributes['options'] << 'listparagraph'
541
- end
613
+ # TODO this feels hacky, better way to distinguish from explicit literal block?
614
+ block.set_option('listparagraph') if in_list
542
615
 
543
616
  # a paragraph is contiguous nonblank/noncontinuation lines
544
617
  else
545
618
  reader.unshift_line this_line
546
- buffer = reader.grab_lines_until(
619
+ lines = reader.read_lines_until(
547
620
  :break_on_blank_lines => true,
548
621
  :break_on_list_continuation => true,
549
622
  :preserve_last_line => true,
@@ -559,23 +632,23 @@ class Lexer
559
632
  # NOTE we need this logic because we've asked the reader to skip
560
633
  # line comments, which may leave us w/ an empty buffer if those
561
634
  # were the only lines found
562
- if buffer.empty?
563
- # call get_line since the reader preserved the last line
564
- reader.get_line
635
+ if lines.empty?
636
+ # call advance since the reader preserved the last line
637
+ reader.advance
565
638
  return nil
566
639
  end
567
640
 
568
- catalog_inline_anchors(buffer.join, document)
641
+ catalog_inline_anchors(lines.join, document)
569
642
 
570
- first_line = buffer.first
643
+ first_line = lines.first
571
644
  if !text_only && (admonition_match = first_line.match(REGEXP[:admonition_inline]))
572
- buffer[0] = admonition_match.post_match.lstrip
573
- block = Block.new(parent, :admonition, buffer)
645
+ lines[0] = admonition_match.post_match.lstrip
574
646
  attributes['style'] = admonition_match[1]
575
647
  attributes['name'] = admonition_name = admonition_match[1].downcase
576
648
  attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
577
- elsif !text_only && COMPLIANCE[:markdown_syntax] && first_line.start_with?('> ')
578
- buffer.map! {|line|
649
+ block = Block.new(parent, :admonition, :source => lines, :attributes => attributes)
650
+ elsif !text_only && Compliance.markdown_syntax && first_line.start_with?('> ')
651
+ lines.map! {|line|
579
652
  if line.start_with?('> ')
580
653
  line[2..-1]
581
654
  elsif line.chomp == '>'
@@ -585,10 +658,10 @@ class Lexer
585
658
  end
586
659
  }
587
660
 
588
- if buffer.last.start_with?('-- ')
589
- attribution, citetitle = buffer.pop[3..-1].split(', ')
590
- buffer.pop while buffer.last.chomp.empty?
591
- buffer[-1] = buffer.last.chomp
661
+ if lines.last.start_with?('-- ')
662
+ attribution, citetitle = lines.pop[3..-1].split(', ', 2)
663
+ lines.pop while lines.last.chomp.empty?
664
+ lines[-1] = lines.last.chomp
592
665
  else
593
666
  attribution, citetitle = nil
594
667
  end
@@ -597,27 +670,34 @@ class Lexer
597
670
  attributes['citetitle'] = citetitle unless citetitle.nil?
598
671
  # NOTE will only detect headings that are floating titles (not section titles)
599
672
  # TODO could assume a floating title when inside a block context
600
- block = build_block(:quote, :complex, false, parent, Reader.new(buffer), attributes)
601
- elsif !text_only && buffer.size > 1 && first_line.start_with?('"') &&
602
- buffer.last.start_with?('-- ') && buffer[-2].chomp.end_with?('"')
603
- buffer[0] = first_line[1..-1]
604
- attribution, citetitle = buffer.pop[3..-1].split(', ')
605
- buffer.pop while buffer.last.chomp.empty?
606
- buffer[-1] = buffer.last.chomp.chop
673
+ # FIXME Reader needs to be created w/ line info
674
+ block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
675
+ elsif !text_only && lines.size > 1 && first_line.start_with?('"') &&
676
+ lines.last.start_with?('-- ') && lines[-2].chomp.end_with?('"')
677
+ lines[0] = first_line[1..-1]
678
+ attribution, citetitle = lines.pop[3..-1].split(', ', 2)
679
+ lines.pop while lines.last.chomp.empty?
680
+ lines[-1] = lines.last.chomp.chop
607
681
  attributes['style'] = 'quote'
608
682
  attributes['attribution'] = attribution unless attribution.nil?
609
683
  attributes['citetitle'] = citetitle unless citetitle.nil?
610
- block = Block.new(parent, :quote, buffer)
611
- #block = Block.new(parent, :quote)
612
- #block << Block.new(block, :paragraph, buffer)
684
+ block = Block.new(parent, :quote, :source => lines, :attributes => attributes)
685
+ #block = Block.new(parent, :quote, :content_model => :compound, :attributes => attributes)
686
+ #block << Block.new(block, :paragraph, :source => lines)
613
687
  else
614
- # QUESTION is this necessary?
615
- #if style == 'normal' && [' ', "\t"].include?(buffer.first[0..0])
616
- # # QUESTION should we only trim leading blanks?
617
- # buffer.map! &:lstrip
618
- #end
688
+ # if [normal] is used over an indented paragraph, unindent it
689
+ if style == 'normal' && ((first_char = lines.first[0..0]) == ' ' || first_char == "\t")
690
+ first_line = lines.first
691
+ first_line_shifted = first_line.lstrip
692
+ indent = line_length(first_line) - line_length(first_line_shifted)
693
+ lines[0] = first_line_shifted
694
+ # QUESTION should we fix the rest of the lines, since in XML output it's insignificant?
695
+ lines.size.times do |i|
696
+ lines[i] = lines[i][indent..-1] if i > 0
697
+ end
698
+ end
619
699
 
620
- block = Block.new(parent, :paragraph, buffer)
700
+ block = Block.new(parent, :paragraph, :source => lines, :attributes => attributes)
621
701
  end
622
702
  end
623
703
 
@@ -636,21 +716,24 @@ class Lexer
636
716
  when :admonition
637
717
  attributes['name'] = admonition_name = style.downcase
638
718
  attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
639
- block = build_block(block_context, :complex, terminator, parent, reader, attributes)
719
+ block = build_block(block_context, :compound, terminator, parent, reader, attributes)
640
720
 
641
721
  when :comment
642
- reader.grab_lines_until(:break_on_blank_lines => true, :chomp_last_line => false)
722
+ build_block(block_context, :skip, terminator, parent, reader, attributes)
643
723
  return nil
644
724
 
645
725
  when :example
646
- block = build_block(block_context, :complex, terminator, parent, reader, attributes, {:supports_caption => true})
726
+ block = build_block(block_context, :compound, terminator, parent, reader, attributes, {:supports_caption => true})
647
727
 
648
728
  when :listing, :fenced_code, :source
649
729
  if block_context == :fenced_code
650
730
  style = attributes['style'] = 'source'
651
- lang = this_line[3..-1].strip
652
- attributes['language'] = lang unless lang.empty?
653
- terminator = terminator[0..2] if terminator.length > 3
731
+ language, linenums = this_line[3...-1].split(',', 2)
732
+ if language && !(language = language.strip).empty?
733
+ attributes['language'] = language
734
+ attributes['linenums'] = '' if linenums && !linenums.strip.empty?
735
+ end
736
+ terminator = terminator[0..2]
654
737
  elsif block_context == :source
655
738
  AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
656
739
  end
@@ -660,13 +743,14 @@ class Lexer
660
743
  block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
661
744
 
662
745
  when :pass
663
- block = build_block(block_context, :simple, terminator, parent, reader, attributes)
746
+ block = build_block(block_context, :raw, terminator, parent, reader, attributes)
664
747
 
665
748
  when :open, :sidebar
666
- block = build_block(block_context, :complex, terminator, parent, reader, attributes)
749
+ block = build_block(block_context, :compound, terminator, parent, reader, attributes)
667
750
 
668
751
  when :table
669
- block_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
752
+ cursor = reader.cursor
753
+ block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true), cursor
670
754
  case terminator[0..0]
671
755
  when ','
672
756
  attributes['format'] = 'csv'
@@ -677,11 +761,26 @@ class Lexer
677
761
 
678
762
  when :quote, :verse
679
763
  AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
680
- block = build_block(block_context, (block_context == :verse ? :verbatim : :complex), terminator, parent, reader, attributes)
764
+ block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes)
681
765
 
682
766
  else
683
- # this should only happen if there is a misconfiguration
684
- raise "Unsupported block type #{block_context} at line #{reader.lineno}"
767
+ if block_extensions && extensions.processor_registered_for_block?(block_context, cloaked_context)
768
+ processor = extensions.load_block_processor block_context, document
769
+
770
+ if (content_model = processor.options[:content_model]) != :skip
771
+ if !(pos_attrs = processor.options.fetch(:pos_attrs, [])).empty?
772
+ AttributeList.rekey(attributes, [nil].concat(pos_attrs))
773
+ end
774
+ if !(default_attrs = processor.options.fetch(:default_attrs, {})).empty?
775
+ default_attrs.each {|k, v| attributes[k] ||= v }
776
+ end
777
+ end
778
+ block = build_block(block_context, content_model, terminator, parent, reader, attributes, :processor => processor)
779
+ return nil if block.nil?
780
+ else
781
+ # this should only happen if there's a misconfiguration
782
+ raise "Unsupported block type #{block_context} at #{reader.line_info}"
783
+ end
685
784
  end
686
785
  end
687
786
  end
@@ -689,21 +788,34 @@ class Lexer
689
788
  # when looking for nested content, one or more line comments, comment
690
789
  # blocks or trailing attribute lists could leave us without a block,
691
790
  # so handle accordingly
692
- # REVIEW we may no longer need this check
791
+ # REVIEW we may no longer need this nil check
693
792
  if !block.nil?
694
793
  # REVIEW seems like there is a better way to organize this wrap-up
695
794
  block.id ||= attributes['id'] if attributes.has_key?('id')
696
795
  block.title = attributes['title'] unless block.title?
697
796
  block.caption ||= attributes.delete('caption')
797
+ # TODO eventualy remove the style attribute from the attributes hash
798
+ #block.style = attributes.delete('style')
799
+ block.style = attributes['style']
698
800
  # AsciiDoc always use [id] as the reftext in HTML output,
699
801
  # but I'd like to do better in Asciidoctor
700
802
  if block.id && block.title? && !attributes.has_key?('reftext')
701
803
  document.register(:ids, [block.id, block.title])
702
804
  end
703
805
  block.update_attributes(attributes)
704
-
705
- if block.context == :listing || block.context == :literal
706
- catalog_callouts(block.buffer.join, document)
806
+ block.lock_in_subs
807
+
808
+ #if document.attributes.has_key? :pending_attribute_entries
809
+ # document.attributes.delete(:pending_attribute_entries).each do |entry|
810
+ # entry.save_to block.attributes
811
+ # end
812
+ #end
813
+
814
+ if block.sub? :callouts
815
+ if !(catalog_callouts block.source, document)
816
+ # No need to look for callouts if they aren't there
817
+ block.remove_sub :callouts
818
+ end
707
819
  end
708
820
  end
709
821
 
@@ -713,48 +825,68 @@ class Lexer
713
825
  # Public: Determines whether this line is the start of any of the delimited blocks
714
826
  #
715
827
  # returns the match data if this line is the first line of a delimited block or nil if not
716
- def self.is_delimited_block?(line, return_match_data = false)
717
- line_len = line.length
718
- # optimized for best performance
719
- if line_len > 2
720
- if line_len == 3
721
- tip = line.chop
722
- tl = 2
828
+ def self.is_delimited_block? line, return_match_data = false
829
+ # highly optimized for best performance
830
+ line_len = line.length - 1
831
+ return nil unless line_len > 1 && DELIMITED_BLOCK_LEADERS.include?(line[0..1])
832
+ line = line.chomp
833
+ # counts endline character in line length
834
+ if line_len == 2
835
+ tip = line
836
+ tl = 2
837
+ elsif line_len < 3
838
+ return nil
839
+ else
840
+ if line_len < 5
841
+ tip = line
842
+ tl = line_len
723
843
  else
724
844
  tip = line[0..3]
725
845
  tl = 4
846
+ end
726
847
 
727
- if COMPLIANCE[:markdown_syntax]
728
- # special case for fenced code blocks
729
- tip_alt = tip.chop
730
- if tip_alt == '```' || tip_alt == '~~~'
731
- tip = tip_alt
732
- tl = 3
848
+ # special case for fenced code blocks
849
+ if Compliance.markdown_syntax
850
+ tip_alt = tip.chop if tl == 4
851
+ if tip_alt == '```'
852
+ if tip.end_with? '`'
853
+ return nil
733
854
  end
855
+ tip = tip_alt
856
+ tl = 3
857
+ elsif tip_alt == '~~~'
858
+ if tip.end_with? '~'
859
+ return nil
860
+ end
861
+ tip = tip_alt
862
+ tl = 3
734
863
  end
735
864
  end
865
+ end
736
866
 
737
- if DELIMITED_BLOCKS.has_key? tip
738
- # if tip is the full line
739
- if tl == line_len - 1
740
- #return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, tip) : true
741
- if return_match_data
742
- context, masq = *DELIMITED_BLOCKS[tip]
743
- BlockMatchData.new(context, masq, tip, tip)
744
- else
745
- true
746
- end
747
- elsif match = line.match(REGEXP[:any_blk])
748
- #return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, match[0]) : true
749
- if return_match_data
750
- context, masq = *DELIMITED_BLOCKS[tip]
751
- BlockMatchData.new(context, masq, tip, match[0])
752
- else
753
- true
754
- end
867
+ if DELIMITED_BLOCKS.has_key? tip
868
+ # tip is the full line when delimiter is minimum length
869
+ if tl == 3 || tl == line_len
870
+ if return_match_data
871
+ context, masq = *DELIMITED_BLOCKS[tip]
872
+ BlockMatchData.new(context, masq, tip, tip)
873
+ else
874
+ true
875
+ end
876
+ elsif %(#{tip}#{tip[-1..-1] * (line_len - tl)}) == line
877
+ if return_match_data
878
+ context, masq = *DELIMITED_BLOCKS[tip]
879
+ BlockMatchData.new(context, masq, tip, line)
755
880
  else
756
- nil
881
+ true
757
882
  end
883
+ #elsif match = line.match(REGEXP[:any_blk])
884
+ # if return_match_data
885
+ # context, masq = *DELIMITED_BLOCKS[tip]
886
+ # BlockMatchData.new(context, masq, tip, match[0])
887
+ # else
888
+ # true
889
+ # end
758
890
  else
759
891
  nil
760
892
  end
@@ -766,53 +898,93 @@ class Lexer
766
898
  # whether a block supports complex content should be a config setting
767
899
  # if terminator is false, that means the all the lines in the reader should be parsed
768
900
  # NOTE could invoke filter in here, before and after parsing
769
- def self.build_block(block_context, content_type, terminator, parent, reader, attributes, options = {})
901
+ def self.build_block(block_context, content_model, terminator, parent, reader, attributes, options = {})
902
+ if content_model == :skip || content_model == :raw
903
+ skip_processing = content_model == :skip
904
+ parse_as_content_model = :simple
905
+ else
906
+ skip_processing = false
907
+ parse_as_content_model = content_model
908
+ end
909
+
770
910
  if terminator.nil?
771
- if content_type == :verbatim
772
- buffer = reader.grab_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true)
911
+ if parse_as_content_model == :verbatim
912
+ lines = reader.read_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true)
773
913
  else
774
- buffer = reader.grab_lines_until(
914
+ content_model = :simple if content_model == :compound
915
+ lines = reader.read_lines_until(
775
916
  :break_on_blank_lines => true,
776
917
  :break_on_list_continuation => true,
777
918
  :preserve_last_line => true,
778
- :skip_line_comments => true) {|line|
919
+ :skip_line_comments => true,
920
+ :skip_processing => skip_processing) {|line|
779
921
  COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line]))
780
922
  }
781
- # QUESTION check for empty buffer?
923
+ # QUESTION check for empty lines after grabbing lines for simple content model?
782
924
  end
783
- elsif content_type != :complex
784
- buffer = reader.grab_lines_until(:terminator => terminator, :chomp_last_line => true)
925
+ block_reader = nil
926
+ elsif parse_as_content_model != :compound
927
+ lines = reader.read_lines_until(:terminator => terminator, :chomp_last_line => true, :skip_processing => skip_processing)
928
+ block_reader = nil
929
+ # terminator is false when reader has already been prepared
785
930
  elsif terminator == false
786
- buffer = nil
931
+ lines = nil
787
932
  block_reader = reader
788
933
  else
789
- buffer = nil
790
- block_reader = Reader.new reader.grab_lines_until(:terminator => terminator)
934
+ lines = nil
935
+ cursor = reader.cursor
936
+ block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing), cursor
791
937
  end
792
938
 
793
- if content_type == :verbatim && attributes.has_key?('indent')
794
- reset_block_indent! buffer, attributes['indent'].to_i
939
+ if content_model == :skip
940
+ attributes.clear
941
+ return lines
942
+ end
943
+
944
+ if content_model == :verbatim && attributes.has_key?('indent')
945
+ reset_block_indent! lines, attributes['indent'].to_i
946
+ end
947
+
948
+ if (processor = options[:processor])
949
+ attributes.delete('style')
950
+ processor.options[:content_model] = content_model
951
+ block = processor.process(parent, block_reader || Reader.new(lines), attributes)
952
+ else
953
+ block = Block.new(parent, block_context, :content_model => content_model, :attributes => attributes, :source => lines)
795
954
  end
796
955
 
797
- block = Block.new(parent, block_context, buffer)
798
956
  # should supports_caption be necessary?
799
957
  if options.fetch(:supports_caption, false)
800
958
  block.title = attributes.delete('title') if attributes.has_key?('title')
801
959
  block.assign_caption attributes.delete('caption')
802
960
  end
803
961
 
804
- if buffer.nil?
962
+ if content_model == :compound
805
963
  # we can look for blocks until there are no more lines (and not worry
806
964
  # about sections) since the reader is confined within the boundaries of a
807
965
  # delimited block
808
- while block_reader.has_more_lines?
809
- parsed_block = next_block(block_reader, block)
810
- block.blocks << parsed_block unless parsed_block.nil?
811
- end
966
+ parse_blocks block_reader, block
812
967
  end
813
968
  block
814
969
  end
815
970
 
971
+ # Public: Parse blocks from this reader until there are no more lines.
972
+ #
973
+ # This method calls Lexer#next_block until there are no more lines in the
974
+ # Reader. It does not consider sections because it's assumed the Reader only
975
+ # has lines which are within a delimited block region.
976
+ #
977
+ # reader - The Reader containing the lines to process
978
+ # parent - The parent Block to which to attach the parsed blocks
979
+ #
980
+ # Returns nothing.
981
+ def self.parse_blocks(reader, parent)
982
+ while reader.has_more_lines?
983
+ block = Lexer.next_block(reader, parent)
984
+ parent << block unless block.nil?
985
+ end
986
+ end
987
+
816
988
  # Internal: Parse and construct an outline list Block from the current position of the Reader
817
989
  #
818
990
  # reader - The Reader from which to retrieve the outline list
@@ -821,39 +993,36 @@ class Lexer
821
993
  #
822
994
  # Returns the Block encapsulating the parsed outline (unordered or ordered) list
823
995
  def self.next_outline_list(reader, list_type, parent)
824
- list_block = Block.new(parent, list_type)
825
- items = []
826
- list_block.buffer = items
996
+ list_block = List.new(parent, list_type)
827
997
  if parent.context == list_type
828
998
  list_block.level = parent.level + 1
829
999
  else
830
1000
  list_block.level = 1
831
1001
  end
832
- Debug.debug { "Created #{list_type} block: #{list_block}" }
1002
+ #Debug.debug { "Created #{list_type} block: #{list_block}" }
833
1003
 
834
1004
  while reader.has_more_lines? && (match = reader.peek_line.match(REGEXP[list_type]))
835
-
836
1005
  marker = resolve_list_marker(list_type, match[1])
837
1006
 
838
1007
  # if we are moving to the next item, and the marker is different
839
1008
  # determine if we are moving up or down in nesting
840
- if items.size > 0 && marker != items.first.marker
1009
+ if list_block.items? && marker != list_block.items.first.marker
841
1010
  # assume list is nested by default, but then check to see if we are
842
1011
  # popping out of a nested list by matching an ancestor's list marker
843
1012
  this_item_level = list_block.level + 1
844
- p = parent
845
- while p.context == list_type
846
- if marker == p.buffer.first.marker
847
- this_item_level = p.level
1013
+ ancestor = parent
1014
+ while ancestor.context == list_type
1015
+ if marker == ancestor.items.first.marker
1016
+ this_item_level = ancestor.level
848
1017
  break
849
1018
  end
850
- p = p.parent
1019
+ ancestor = ancestor.parent
851
1020
  end
852
1021
  else
853
1022
  this_item_level = list_block.level
854
1023
  end
855
1024
 
856
- if items.size == 0 || this_item_level == list_block.level
1025
+ if !list_block.items? || this_item_level == list_block.level
857
1026
  list_item = next_list_item(reader, list_block, match)
858
1027
  elsif this_item_level < list_block.level
859
1028
  # leave this block
@@ -861,10 +1030,10 @@ class Lexer
861
1030
  elsif this_item_level > list_block.level
862
1031
  # If this next list level is down one from the
863
1032
  # current Block's, append it to content of the current list item
864
- items.last.blocks << next_block(reader, list_block)
1033
+ list_block.items.last << next_block(reader, list_block)
865
1034
  end
866
1035
 
867
- items << list_item unless list_item.nil?
1036
+ list_block << list_item unless list_item.nil?
868
1037
  list_item = nil
869
1038
 
870
1039
  reader.skip_blank_lines
@@ -878,14 +1047,21 @@ class Lexer
878
1047
  # text - The String of text in which to look for callouts
879
1048
  # document - The current document on which the callouts are stored
880
1049
  #
881
- # Returns nothing
1050
+ # Returns A Boolean indicating whether callouts were found
882
1051
  def self.catalog_callouts(text, document)
883
- text.scan(REGEXP[:callout_scan]) {
884
- # alias match for Ruby 1.8.7 compat
885
- m = $~
886
- next if m[0].start_with? '\\'
887
- document.callouts.register(m[1])
888
- }
1052
+ found = false
1053
+ if text.include? '<'
1054
+ text.scan(REGEXP[:callout_quick_scan]) {
1055
+ # alias match for Ruby 1.8.7 compat
1056
+ m = $~
1057
+ if m[0][0..0] != '\\'
1058
+ document.callouts.register(m[2])
1059
+ end
1060
+ # we have to mark as found even if it's escaped so it can be unescaped
1061
+ found = true
1062
+ }
1063
+ end
1064
+ found
889
1065
  end
890
1066
 
891
1067
  # Internal: Catalog any inline anchors found in the text, but don't process them
@@ -917,18 +1093,25 @@ class Lexer
917
1093
  #
918
1094
  # Returns the Block encapsulating the parsed labeled list
919
1095
  def self.next_labeled_list(reader, match, parent)
920
- pairs = []
921
- block = Block.new(parent, :dlist)
922
- block.buffer = pairs
1096
+ list_block = List.new(parent, :dlist)
1097
+ previous_pair = nil
923
1098
  # allows us to capture until we find a labeled item
924
1099
  # that uses the same delimiter (::, :::, :::: or ;;)
925
1100
  sibling_pattern = REGEXP[:dlist_siblings][match[2]]
926
1101
 
927
1102
  begin
928
- pairs << next_list_item(reader, block, match, sibling_pattern)
1103
+ term, item = next_list_item(reader, list_block, match, sibling_pattern)
1104
+ if !previous_pair.nil? && previous_pair.last.nil?
1105
+ previous_pair.pop
1106
+ previous_pair[0] << term
1107
+ previous_pair << item
1108
+ else
1109
+ # FIXME this misses the automatic parent assignment
1110
+ list_block.items << (previous_pair = [[term], item])
1111
+ end
929
1112
  end while reader.has_more_lines? && match = reader.peek_line.match(sibling_pattern)
930
1113
 
931
- block
1114
+ list_block
932
1115
  end
933
1116
 
934
1117
  # Internal: Parse and construct the next ListItem for the current bulleted
@@ -957,25 +1140,46 @@ class Lexer
957
1140
  has_text = !match[3].to_s.empty?
958
1141
  else
959
1142
  # Create list item using first line as the text of the list item
960
- list_item = ListItem.new(list_block, match[2])
1143
+ text = match[2]
1144
+ checkbox = false
1145
+ if list_type == :ulist && text.start_with?('[')
1146
+ if text.start_with? '[ ] '
1147
+ checkbox = true
1148
+ checked = false
1149
+ text = text[3..-1].lstrip
1150
+ elsif text.start_with?('[*] ') || text.start_with?('[x] ')
1151
+ checkbox = true
1152
+ checked = true
1153
+ text = text[3..-1].lstrip
1154
+ end
1155
+ end
1156
+ list_item = ListItem.new(list_block, text)
1157
+
1158
+ if checkbox
1159
+ # FIXME checklist never makes it into the options attribute
1160
+ list_block.attributes['checklist-option'] = ''
1161
+ list_item.attributes['checkbox'] = ''
1162
+ list_item.attributes['checked'] = '' if checked
1163
+ end
961
1164
 
962
1165
  if !sibling_trait
963
- sibling_trait = resolve_list_marker(list_type, match[1], list_block.buffer.size, true)
1166
+ sibling_trait = resolve_list_marker(list_type, match[1], list_block.items.size, true, reader)
964
1167
  end
965
1168
  list_item.marker = sibling_trait
966
1169
  has_text = true
967
1170
  end
968
1171
 
969
1172
  # first skip the line with the marker / term
970
- reader.get_line
971
- list_item_reader = Reader.new grab_lines_for_list_item(reader, list_type, sibling_trait, has_text)
1173
+ reader.advance
1174
+ cursor = reader.cursor
1175
+ list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), cursor
972
1176
  if list_item_reader.has_more_lines?
973
- comment_lines = list_item_reader.consume_line_comments
1177
+ comment_lines = list_item_reader.skip_line_comments
974
1178
  subsequent_line = list_item_reader.peek_line
975
- list_item_reader.unshift(*comment_lines) unless comment_lines.empty?
1179
+ list_item_reader.unshift_lines comment_lines unless comment_lines.empty?
976
1180
 
977
1181
  if !subsequent_line.nil?
978
- continuation_connects_first_block = (subsequent_line == "\n")
1182
+ continuation_connects_first_block = (subsequent_line == ::Asciidoctor::EOL)
979
1183
  # if there's no continuation connecting the first block, then
980
1184
  # treat the lines as paragraph text (activated when has_text = false)
981
1185
  if !continuation_connects_first_block && list_type != :dlist
@@ -995,7 +1199,7 @@ class Lexer
995
1199
  # list
996
1200
  while list_item_reader.has_more_lines?
997
1201
  new_block = next_block(list_item_reader, list_block, {}, options)
998
- list_item.blocks << new_block unless new_block.nil?
1202
+ list_item << new_block unless new_block.nil?
999
1203
  end
1000
1204
 
1001
1205
  list_item.fold_first(continuation_connects_first_block, content_adjacent)
@@ -1025,7 +1229,7 @@ class Lexer
1025
1229
  # has_text - Whether the list item has text defined inline (always true except for labeled lists)
1026
1230
  #
1027
1231
  # Returns an Array of lines belonging to the current list item.
1028
- def self.grab_lines_for_list_item(reader, list_type, sibling_trait = nil, has_text = true)
1232
+ def self.read_lines_for_list_item(reader, list_type, sibling_trait = nil, has_text = true)
1029
1233
  buffer = []
1030
1234
 
1031
1235
  # three states for continuation: :inactive, :active & :frozen
@@ -1043,7 +1247,7 @@ class Lexer
1043
1247
  detached_continuation = nil
1044
1248
 
1045
1249
  while reader.has_more_lines?
1046
- this_line = reader.get_line
1250
+ this_line = reader.read_line
1047
1251
 
1048
1252
  # if we've arrived at a sibling item in this list, we've captured
1049
1253
  # the complete list item and can begin processing it
@@ -1057,7 +1261,7 @@ class Lexer
1057
1261
  if continuation == :inactive
1058
1262
  continuation = :active
1059
1263
  has_text = true
1060
- buffer[-1] = "\n" unless within_nested_list
1264
+ buffer[-1] = ::Asciidoctor::EOL unless within_nested_list
1061
1265
  end
1062
1266
 
1063
1267
  # dealing with adjacent list continuations (which is really a syntax error)
@@ -1078,7 +1282,7 @@ class Lexer
1078
1282
  buffer << this_line
1079
1283
  # grab all the lines in the block, leaving the delimiters in place
1080
1284
  # we're being more strict here about the terminator, but I think that's a good thing
1081
- buffer.concat reader.grab_lines_until(:terminator => match.terminator, :grab_last_line => true)
1285
+ buffer.concat reader.read_lines_until(:terminator => match.terminator, :read_last_line => true)
1082
1286
  continuation = :inactive
1083
1287
  else
1084
1288
  break
@@ -1095,7 +1299,7 @@ class Lexer
1095
1299
  # list item will throw off the exit from it
1096
1300
  if this_line.match(REGEXP[:lit_par])
1097
1301
  reader.unshift_line this_line
1098
- buffer.concat reader.grab_lines_until(
1302
+ buffer.concat reader.read_lines_until(
1099
1303
  :preserve_last_line => true,
1100
1304
  :break_on_blank_lines => true,
1101
1305
  :break_on_list_continuation => true) {|line|
@@ -1122,7 +1326,7 @@ class Lexer
1122
1326
  # advance to the next line of content
1123
1327
  if this_line.chomp.empty?
1124
1328
  reader.skip_blank_lines
1125
- this_line = reader.get_line
1329
+ this_line = reader.read_line
1126
1330
  # if we hit eof or a sibling, stop reading
1127
1331
  break if this_line.nil? || is_sibling_list_item?(this_line, list_type, sibling_trait)
1128
1332
  end
@@ -1138,7 +1342,7 @@ class Lexer
1138
1342
  # slurp up any literal paragraph offset by blank lines
1139
1343
  if this_line.match(REGEXP[:lit_par])
1140
1344
  reader.unshift_line this_line
1141
- buffer.concat reader.grab_lines_until(
1345
+ buffer.concat reader.read_lines_until(
1142
1346
  :preserve_last_line => true,
1143
1347
  :break_on_blank_lines => true,
1144
1348
  :break_on_list_continuation => true) {|line|
@@ -1211,13 +1415,15 @@ class Lexer
1211
1415
  # parent - the parent Section or Document of this Section
1212
1416
  # attributes - a Hash of attributes to assign to this section (default: {})
1213
1417
  def self.initialize_section(reader, parent, attributes = {})
1214
- section = Section.new parent
1215
- section.id, section.title, section.level, _ = parse_section_title(reader, section.document)
1418
+ document = parent.document
1419
+ sect_id, sect_title, sect_level, _ = parse_section_title(reader, document)
1420
+ section = Section.new parent, sect_level, document.attributes.has_key?('numbered')
1421
+ section.id = sect_id
1422
+ section.title = sect_title
1216
1423
  # parse style, id and role from first positional attribute
1217
1424
  if attributes[1]
1218
- section.sectname, _ = parse_style_attribute(attributes)
1425
+ section.sectname, _ = parse_style_attribute(attributes, reader)
1219
1426
  section.special = true
1220
- document = parent.document
1221
1427
  # HACK needs to be refactored so it's driven by config
1222
1428
  if section.sectname == 'abstract' && document.doctype == 'book'
1223
1429
  section.sectname = "sect1"
@@ -1231,6 +1437,9 @@ class Lexer
1231
1437
  section.caption = "#{document.attributes['appendix-caption']} #{number}: "
1232
1438
  Document::AttributeEntry.new('appendix-number', number).save_to(attributes)
1233
1439
  end
1440
+ elsif sect_title.downcase == 'synopsis' && document.doctype == 'manpage'
1441
+ section.special = true
1442
+ section.sectname = 'synopsis'
1234
1443
  else
1235
1444
  section.sectname = "sect#{section.level}"
1236
1445
  end
@@ -1276,7 +1485,7 @@ class Lexer
1276
1485
  def self.is_next_line_section?(reader, attributes)
1277
1486
  return false if !(val = attributes[1]).nil? && ['float', 'discrete'].include?(val)
1278
1487
  return false if !reader.has_more_lines?
1279
- is_section_title?(*reader.peek_lines(2))
1488
+ Compliance.underline_style_section_titles ? is_section_title?(*reader.peek_lines(2)) : is_section_title?(reader.peek_line)
1280
1489
  end
1281
1490
 
1282
1491
  # Internal: Convenience API for checking if the next line on the Reader is the document title
@@ -1299,7 +1508,7 @@ class Lexer
1299
1508
  def self.is_section_title?(line1, line2 = nil)
1300
1509
  if (level = is_single_line_section_title?(line1))
1301
1510
  level
1302
- elsif (level = is_two_line_section_title?(line1, line2))
1511
+ elsif line2 && (level = is_two_line_section_title?(line1, line2))
1303
1512
  level
1304
1513
  else
1305
1514
  false
@@ -1307,7 +1516,8 @@ class Lexer
1307
1516
  end
1308
1517
 
1309
1518
  def self.is_single_line_section_title?(line1)
1310
- if !line1.nil? && (line1.start_with?('=') || (COMPLIANCE[:markdown_syntax] && line1.start_with?('#'))) &&
1519
+ first_char = line1.nil? ? nil : line1[0..0]
1520
+ if (first_char == '=' || (Compliance.markdown_syntax && first_char == '#')) &&
1311
1521
  (match = line1.match(REGEXP[:section_title]))
1312
1522
  single_line_section_level match[1]
1313
1523
  else
@@ -1319,7 +1529,7 @@ class Lexer
1319
1529
  if !line1.nil? && !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) &&
1320
1530
  line2.match(REGEXP[:section_underline]) && line1.match(REGEXP[:section_name]) &&
1321
1531
  # chomp so that a (non-visible) endline does not impact calculation
1322
- (line1.chomp.size - line2.chomp.size).abs <= 1
1532
+ (line_length(line1) - line_length(line2)).abs <= 1
1323
1533
  section_level line2
1324
1534
  else
1325
1535
  false
@@ -1370,23 +1580,24 @@ class Lexer
1370
1580
  #--
1371
1581
  # NOTE for efficiency, we don't reuse methods that check for a section title
1372
1582
  def self.parse_section_title(reader, document)
1373
- line1 = reader.get_line
1583
+ line1 = reader.read_line
1374
1584
  sect_id = nil
1375
1585
  sect_title = nil
1376
1586
  sect_level = -1
1377
1587
  single_line = true
1378
1588
 
1379
- if (line1.start_with?('=') || (COMPLIANCE[:markdown_syntax] && line1.start_with?('#'))) &&
1589
+ first_char = line1[0..0]
1590
+ if (first_char == '=' || (Compliance.markdown_syntax && first_char == '#')) &&
1380
1591
  (match = line1.match(REGEXP[:section_title]))
1381
1592
  sect_id = match[3]
1382
1593
  sect_title = match[2]
1383
1594
  sect_level = single_line_section_level match[1]
1384
- else
1385
- line2 = reader.peek_line
1595
+ elsif Compliance.underline_style_section_titles
1596
+ line2 = reader.peek_line true
1386
1597
  if !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) && line2.match(REGEXP[:section_underline]) &&
1387
1598
  (name_match = line1.match(REGEXP[:section_name])) &&
1388
1599
  # chomp so that a (non-visible) endline does not impact calculation
1389
- (line1.chomp.size - line2.chomp.size).abs <= 1
1600
+ (line_length(line1) - line_length(line2)).abs <= 1
1390
1601
  if anchor_match = name_match[1].match(REGEXP[:anchor_embedded])
1391
1602
  sect_id = anchor_match[2]
1392
1603
  sect_title = anchor_match[1]
@@ -1395,7 +1606,7 @@ class Lexer
1395
1606
  end
1396
1607
  sect_level = section_level line2
1397
1608
  single_line = false
1398
- reader.get_line
1609
+ reader.advance
1399
1610
  end
1400
1611
  end
1401
1612
  if sect_level >= 0
@@ -1404,6 +1615,15 @@ class Lexer
1404
1615
  [sect_id, sect_title, sect_level, single_line]
1405
1616
  end
1406
1617
 
1618
+ # Public: Calculate the number of unicode characters in the line, excluding the endline
1619
+ #
1620
+ # line - the String to calculate
1621
+ #
1622
+ # returns the number of unicode characters in the line
1623
+ def self.line_length(line)
1624
+ FORCE_UNICODE_LINE_LENGTH ? line.chomp.scan(/./u).length : line.chomp.length
1625
+ end
1626
+
1407
1627
  # Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info).
1408
1628
  #
1409
1629
  # Returns the Hash of header metadata. If a Document object is supplied, the metadata
@@ -1425,8 +1645,8 @@ class Lexer
1425
1645
  implicit_author = nil
1426
1646
  implicit_authors = nil
1427
1647
 
1428
- if reader.has_more_lines? && !reader.peek_line.chomp.empty?
1429
- author_metadata = process_authors reader.get_line
1648
+ if reader.has_more_lines? && !reader.next_line_empty?
1649
+ author_metadata = process_authors reader.read_line
1430
1650
 
1431
1651
  unless author_metadata.empty?
1432
1652
  # apply header subs and assign to document
@@ -1449,8 +1669,8 @@ class Lexer
1449
1669
 
1450
1670
  rev_metadata = {}
1451
1671
 
1452
- if reader.has_more_lines? && !reader.peek_line.chomp.empty?
1453
- rev_line = reader.get_line
1672
+ if reader.has_more_lines? && !reader.next_line_empty?
1673
+ rev_line = reader.read_line
1454
1674
  if match = rev_line.match(REGEXP[:revision_info])
1455
1675
  rev_metadata['revdate'] = match[2].strip
1456
1676
  rev_metadata['revnumber'] = match[1].rstrip unless match[1].nil?
@@ -1640,7 +1860,7 @@ class Lexer
1640
1860
  next_line = reader.peek_line
1641
1861
  if (commentish = next_line.start_with?('//')) && (match = next_line.match(REGEXP[:comment_blk]))
1642
1862
  terminator = match[0]
1643
- reader.grab_lines_until(:skip_first_line => true, :preserve_last_line => true, :terminator => terminator, :preprocess => false)
1863
+ reader.read_lines_until(:skip_first_line => true, :preserve_last_line => true, :terminator => terminator, :skip_processing => true)
1644
1864
  elsif commentish && next_line.match(REGEXP[:comment])
1645
1865
  # do nothing, we'll skip it
1646
1866
  elsif !options[:text] && (match = next_line.match(REGEXP[:attr_entry]))
@@ -1648,7 +1868,7 @@ class Lexer
1648
1868
  elsif match = next_line.match(REGEXP[:anchor])
1649
1869
  id, reftext = match[1].split(',')
1650
1870
  attributes['id'] = id
1651
- # AsciiDoc always use [id] as the reftext in HTML output,
1871
+ # AsciiDoc always uses [id] as the reftext in HTML output,
1652
1872
  # but I'd like to do better in Asciidoctor
1653
1873
  #parent.document.register(:ids, id)
1654
1874
  if reftext
@@ -1716,6 +1936,10 @@ class Lexer
1716
1936
  # a nil value signals the attribute should be deleted (undefined)
1717
1937
  value = nil
1718
1938
  name = name.chop
1939
+ elsif name.start_with?('!')
1940
+ # a nil value signals the attribute should be deleted (undefined)
1941
+ value = nil
1942
+ name = name[1..-1]
1719
1943
  end
1720
1944
 
1721
1945
  name = sanitize_attribute_name(name)
@@ -1747,9 +1971,9 @@ class Lexer
1747
1971
  # validate - Whether to validate the value of the marker
1748
1972
  #
1749
1973
  # Returns the String 0-index marker for this list item
1750
- def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false)
1974
+ def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil)
1751
1975
  if list_type == :olist && !marker.start_with?('.')
1752
- resolve_ordered_list_marker(marker, ordinal, validate)
1976
+ resolve_ordered_list_marker(marker, ordinal, validate, reader)
1753
1977
  elsif list_type == :colist
1754
1978
  '<1>'
1755
1979
  else
@@ -1778,7 +2002,7 @@ class Lexer
1778
2002
  # # => 'A.'
1779
2003
  #
1780
2004
  # Returns the String of the first marker in this number series
1781
- def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false)
2005
+ def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false, reader = nil)
1782
2006
  number_style = ORDERED_LIST_STYLES.detect {|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) }
1783
2007
  expected = actual = nil
1784
2008
  case number_style
@@ -1817,8 +2041,7 @@ class Lexer
1817
2041
  end
1818
2042
 
1819
2043
  if validate && expected != actual
1820
- # FIXME I need a reader reference or line number to report line number
1821
- puts "asciidoctor: WARNING: list item index: expected #{expected}, got #{actual}"
2044
+ warn "asciidoctor: WARNING: #{reader.line_info}: list item index: expected #{expected}, got #{actual}"
1822
2045
  end
1823
2046
 
1824
2047
  marker
@@ -1872,11 +2095,19 @@ class Lexer
1872
2095
  explicit_col_specs = false
1873
2096
  end
1874
2097
 
1875
- table_reader.skip_blank_lines
2098
+ skipped = table_reader.skip_blank_lines
1876
2099
 
1877
- parser_ctx = Table::ParserContext.new(table, attributes)
2100
+ parser_ctx = Table::ParserContext.new(table_reader, table, attributes)
2101
+ loop_idx = -1
1878
2102
  while table_reader.has_more_lines?
1879
- line = table_reader.get_line
2103
+ loop_idx += 1
2104
+ line = table_reader.read_line
2105
+
2106
+ if skipped == 0 && loop_idx.zero? && !attributes.has_key?('options') &&
2107
+ !(next_line = table_reader.peek_line).nil? && next_line == ::Asciidoctor::EOL
2108
+ table.has_header_option = true
2109
+ table.set_option 'header'
2110
+ end
1880
2111
 
1881
2112
  if parser_ctx.format == 'psv'
1882
2113
  if parser_ctx.starts_with_delimiter? line
@@ -1889,8 +2120,7 @@ class Lexer
1889
2120
  if !next_cell_spec.nil?
1890
2121
  parser_ctx.close_open_cell next_cell_spec
1891
2122
  else
1892
- # QUESTION do we not advance to next line? if so, when
1893
- # will we if we came into this block?
2123
+ # QUESTION do we not advance to next line? if so, when will we if we came into this block?
1894
2124
  end
1895
2125
  end
1896
2126
  end
@@ -1924,7 +2154,7 @@ class Lexer
1924
2154
  # no other delimiters to see here
1925
2155
  # suck up this line into the buffer and move on
1926
2156
  parser_ctx.buffer = %(#{parser_ctx.buffer}#{line})
1927
- # QUESTION make this an option? (unwrap-option?)
2157
+ # QUESTION make stripping endlines in csv data an option? (unwrap-option?)
1928
2158
  if parser_ctx.format == 'csv'
1929
2159
  parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} )
1930
2160
  end
@@ -1938,7 +2168,7 @@ class Lexer
1938
2168
  end
1939
2169
  end
1940
2170
 
1941
- table_reader.skip_blank_lines unless parser_ctx.cell_open?
2171
+ skipped = table_reader.skip_blank_lines unless parser_ctx.cell_open?
1942
2172
 
1943
2173
  if !table_reader.has_more_lines?
1944
2174
  parser_ctx.close_cell true
@@ -1995,7 +2225,8 @@ class Lexer
1995
2225
  end
1996
2226
  end
1997
2227
 
1998
- # TODO support percentage width
2228
+ # to_i permits us to support percentage width by stripping the %
2229
+ # NOTE this is slightly out of compliance w/ AsciiDoc, but makes way more sense
1999
2230
  spec['width'] = !m[3].nil? ? m[3].to_i : 1
2000
2231
 
2001
2232
  # make this an operation
@@ -2069,54 +2300,103 @@ class Lexer
2069
2300
  # both the original style attribute and the parsed value from the first
2070
2301
  # positional attribute.
2071
2302
  #
2072
- # attributes - The Hash of attributes to process
2303
+ # attributes - The Hash of attributes to process and update
2073
2304
  #
2074
2305
  # Examples
2075
2306
  #
2076
2307
  # puts attributes
2077
- # => {1 => "abstract#intro.lead", "style" => "preamble"}
2308
+ # => {1 => "abstract#intro.lead%fragment", "style" => "preamble"}
2078
2309
  #
2079
2310
  # parse_style_attribute(attributes)
2080
2311
  # => ["abstract", "preamble"]
2081
2312
  #
2082
2313
  # puts attributes
2083
- # => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro", "role" => "lead"}
2314
+ # => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro",
2315
+ # "role" => "lead", "options" => ["fragment"], "fragment-option" => ''}
2084
2316
  #
2085
2317
  # Returns a two-element Array of the parsed style from the
2086
2318
  # first positional attribute and the original style that was
2087
2319
  # replaced
2088
- def self.parse_style_attribute(attributes)
2320
+ def self.parse_style_attribute(attributes, reader = nil)
2089
2321
  original_style = attributes['style']
2090
2322
  raw_style = attributes[1]
2323
+ # NOTE spaces are not allowed in shorthand, so if we find one, this ain't shorthand
2091
2324
  if !raw_style || raw_style.include?(' ')
2092
2325
  attributes['style'] = raw_style
2093
2326
  [raw_style, original_style]
2094
- # FIXME this logic could be condensed
2095
2327
  else
2096
- hash_index = raw_style.index('#')
2097
- dot_index = raw_style.index('.')
2098
- if !hash_index.nil? && (dot_index.nil? || hash_index < dot_index)
2099
- parsed_style = attributes['style'] = (hash_index > 0 ? raw_style[0..(hash_index - 1)] : nil)
2100
- id = raw_style[(hash_index + 1)..-1]
2101
- if !dot_index.nil?
2102
- id, roles = id.split('.', 2)
2103
- attributes['id'] = id
2104
- attributes['role'] = roles.tr('.', ' ')
2328
+ type = :style
2329
+ collector = []
2330
+ parsed = {}
2331
+ # QUESTION should this be a private method? (though, it's never called if shorthand isn't used)
2332
+ save_current = lambda {
2333
+ if collector.empty?
2334
+ if type != :style
2335
+ warn "asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} invalid empty #{type} detected in style attribute"
2336
+ end
2105
2337
  else
2106
- attributes['id'] = id
2338
+ case type
2339
+ when :role, :option
2340
+ parsed[type] ||= []
2341
+ parsed[type].push collector.join
2342
+ when :id
2343
+ if parsed.has_key? :id
2344
+ warn "asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} multiple ids detected in style attribute"
2345
+ end
2346
+ parsed[type] = collector.join
2347
+ else
2348
+ parsed[type] = collector.join
2349
+ end
2350
+ collector = []
2107
2351
  end
2108
- elsif !dot_index.nil? && (hash_index.nil? || dot_index < hash_index)
2109
- parsed_style = attributes['style'] = (dot_index > 0 ? raw_style[0..(dot_index - 1)] : nil)
2110
- roles = raw_style[(dot_index + 1)..-1]
2111
- if !hash_index.nil?
2112
- roles, id = roles.split('#', 2)
2113
- attributes['id'] = id
2114
- attributes['role'] = roles.tr('.', ' ')
2352
+ }
2353
+
2354
+ raw_style.split('').each do |c|
2355
+ if c == '.' || c == '#' || c == '%'
2356
+ save_current.call
2357
+ case c
2358
+ when '.'
2359
+ type = :role
2360
+ when '#'
2361
+ type = :id
2362
+ when '%'
2363
+ type = :option
2364
+ end
2115
2365
  else
2116
- attributes['role'] = roles.tr('.', ' ')
2366
+ collector.push c
2117
2367
  end
2118
- else
2368
+ end
2369
+
2370
+ # small optimization if no shorthand is found
2371
+ if type == :style
2119
2372
  parsed_style = attributes['style'] = raw_style
2373
+ else
2374
+ save_current.call
2375
+
2376
+ if parsed.has_key? :style
2377
+ parsed_style = attributes['style'] = parsed[:style]
2378
+ else
2379
+ parsed_style = nil
2380
+ end
2381
+
2382
+ if parsed.has_key? :id
2383
+ attributes['id'] = parsed[:id]
2384
+ end
2385
+
2386
+ if parsed.has_key? :role
2387
+ attributes['role'] = parsed[:role] * ' '
2388
+ end
2389
+
2390
+ if parsed.has_key? :option
2391
+ (options = parsed[:option]).each do |option|
2392
+ attributes["#{option}-option"] = ''
2393
+ end
2394
+ if (existing_opts = attributes['options'])
2395
+ attributes['options'] = (options + existing_opts.split(',')) * ','
2396
+ else
2397
+ attributes['options'] = options * ','
2398
+ end
2399
+ end
2120
2400
  end
2121
2401
 
2122
2402
  [parsed_style, original_style]
@@ -2159,6 +2439,8 @@ class Lexer
2159
2439
  # # => end
2160
2440
  #
2161
2441
  # returns the Array of String lines with block offset removed
2442
+ #--
2443
+ # FIXME refactor gsub matchers into compiled regex
2162
2444
  def self.reset_block_indent!(lines, indent = 0)
2163
2445
  return if indent.nil? || lines.empty?
2164
2446