asciidoctor 0.1.2 → 0.1.3

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +10 -0
  3. data/Guardfile +18 -0
  4. data/LICENSE +1 -1
  5. data/README.adoc +65 -21
  6. data/Rakefile +10 -0
  7. data/asciidoctor.gemspec +17 -35
  8. data/compat/asciidoc.conf +130 -13
  9. data/lib/asciidoctor.rb +107 -87
  10. data/lib/asciidoctor/abstract_block.rb +6 -2
  11. data/lib/asciidoctor/abstract_node.rb +21 -13
  12. data/lib/asciidoctor/attribute_list.rb +2 -5
  13. data/{stylesheets/asciidoctor.css → lib/asciidoctor/backends/_stylesheets.rb} +96 -46
  14. data/lib/asciidoctor/backends/base_template.rb +9 -4
  15. data/lib/asciidoctor/backends/docbook45.rb +246 -138
  16. data/lib/asciidoctor/backends/html5.rb +580 -381
  17. data/lib/asciidoctor/block.rb +2 -50
  18. data/lib/asciidoctor/cli/options.rb +9 -8
  19. data/lib/asciidoctor/document.rb +35 -45
  20. data/lib/asciidoctor/helpers.rb +10 -0
  21. data/lib/asciidoctor/lexer.rb +456 -148
  22. data/lib/asciidoctor/list_item.rb +0 -21
  23. data/lib/asciidoctor/path_resolver.rb +18 -12
  24. data/lib/asciidoctor/reader.rb +71 -26
  25. data/lib/asciidoctor/renderer.rb +2 -19
  26. data/lib/asciidoctor/section.rb +0 -1
  27. data/lib/asciidoctor/substituters.rb +150 -36
  28. data/lib/asciidoctor/table.rb +30 -24
  29. data/lib/asciidoctor/version.rb +1 -1
  30. data/man/asciidoctor.1 +22 -16
  31. data/man/asciidoctor.ad +24 -16
  32. data/test/attributes_test.rb +50 -0
  33. data/test/blocks_test.rb +660 -9
  34. data/test/document_test.rb +191 -14
  35. data/test/fixtures/encoding.asciidoc +8 -0
  36. data/test/invoker_test.rb +47 -0
  37. data/test/lexer_test.rb +172 -0
  38. data/test/links_test.rb +28 -0
  39. data/test/lists_test.rb +172 -13
  40. data/test/options_test.rb +29 -2
  41. data/test/paragraphs_test.rb +105 -47
  42. data/test/paths_test.rb +3 -3
  43. data/test/reader_test.rb +46 -0
  44. data/test/sections_test.rb +365 -12
  45. data/test/substitutions_test.rb +127 -11
  46. data/test/tables_test.rb +81 -14
  47. data/test/test_helper.rb +18 -7
  48. data/test/text_test.rb +17 -5
  49. metadata +9 -36
@@ -14,9 +14,6 @@ class Block < AbstractBlock
14
14
  # Public: Get/Set the original Array content for this section block.
15
15
  attr_accessor :buffer
16
16
 
17
- # Public: Get/Set the caption for this block
18
- attr_accessor :caption
19
-
20
17
  # Public: Initialize an Asciidoctor::Block object.
21
18
  #
22
19
  # parent - The parent Asciidoc Object.
@@ -26,7 +23,6 @@ class Block < AbstractBlock
26
23
  def initialize(parent, context, buffer = nil)
27
24
  super(parent, context)
28
25
  @buffer = buffer
29
- @caption = nil
30
26
  end
31
27
 
32
28
  # Public: Get the rendered String content for this Block. If the block
@@ -34,56 +30,12 @@ class Block < AbstractBlock
34
30
  # rendered and returned as content that can be included in the
35
31
  # parent block's template.
36
32
  def render
37
- Debug.debug { "Now rendering #{@context} block for #{self}" }
38
33
  @document.playback_attributes @attributes
39
34
  out = renderer.render("block_#{@context}", self)
40
35
  @document.callouts.next_list if @context == :colist
41
36
  out
42
37
  end
43
38
 
44
- def splain(parent_level = 0)
45
- parent_level += 1
46
- Debug.puts_indented(parent_level, "Block id: #{id}") unless self.id.nil?
47
- Debug.puts_indented(parent_level, "Block title: #{title}") unless self.title.nil?
48
- Debug.puts_indented(parent_level, "Block caption: #{caption}") unless self.caption.nil?
49
- Debug.puts_indented(parent_level, "Block level: #{level}") unless self.level.nil?
50
- Debug.puts_indented(parent_level, "Block context: #{context}") unless self.context.nil?
51
-
52
- Debug.puts_indented(parent_level, "Blocks: #{@blocks.count}")
53
-
54
- if buffer.is_a? Enumerable
55
- buffer.each_with_index do |buf, i|
56
- Debug.puts_indented(parent_level, "v" * (60 - parent_level*2))
57
- Debug.puts_indented(parent_level, "Buffer ##{i} is a #{buf.class}")
58
- Debug.puts_indented(parent_level, "Name is #{buf.title rescue 'n/a'}")
59
-
60
- if buf.respond_to? :splain
61
- buf.splain(parent_level)
62
- else
63
- Debug.puts_indented(parent_level, "Buffer: #{buf}")
64
- end
65
- Debug.puts_indented(parent_level, "^" * (60 - parent_level*2))
66
- end
67
- else
68
- if buffer.respond_to? :splain
69
- buffer.splain(parent_level)
70
- else
71
- Debug.puts_indented(parent_level, "Buffer: #{@buffer}")
72
- end
73
- end
74
-
75
- @blocks.each_with_index do |block, i|
76
- Debug.puts_indented(parent_level, "v" * (60 - parent_level*2))
77
- Debug.puts_indented(parent_level, "Block ##{i} is a #{block.class}")
78
- Debug.puts_indented(parent_level, "Name is #{block.title rescue 'n/a'}")
79
-
80
- block.splain(parent_level) if block.respond_to? :splain
81
- Debug.puts_indented(parent_level, "^" * (60 - parent_level*2))
82
- end
83
-
84
- nil
85
- end
86
-
87
39
  # Public: Get an HTML-ified version of the source buffer, with special
88
40
  # Asciidoc characters and entities converted to their HTML equivalents.
89
41
  #
@@ -96,7 +48,7 @@ class Block < AbstractBlock
96
48
  # => ["<em>This</em> is what happens when you &lt;meet&gt; a stranger in the &lt;alps&gt;!"]
97
49
  def content
98
50
  case @context
99
- when :preamble, :open
51
+ when :preamble
100
52
  @blocks.map {|b| b.render }.join
101
53
  # lists get iterated in the template (for now)
102
54
  # list items recurse into this block when their text and content methods are called
@@ -106,7 +58,7 @@ class Block < AbstractBlock
106
58
  apply_literal_subs(@buffer)
107
59
  when :pass
108
60
  apply_passthrough_subs(@buffer)
109
- when :admonition, :example, :sidebar, :quote, :verse
61
+ when :admonition, :example, :sidebar, :quote, :verse, :open
110
62
  if !@buffer.nil?
111
63
  apply_para_subs(@buffer)
112
64
  else
@@ -44,11 +44,11 @@ Example: asciidoctor -b html5 source.asciidoc
44
44
  opts.on('-v', '--verbose', 'enable verbose mode (default: false)') do |verbose|
45
45
  self[:verbose] = true
46
46
  end
47
- opts.on('-b', '--backend BACKEND', ['html5', 'docbook45'], 'set output format (i.e., backend): [html5, docbook45] (default: html5)') do |backend|
47
+ opts.on('-b', '--backend BACKEND', 'set output format backend (default: html5)') do |backend|
48
48
  self[:attributes]['backend'] = backend
49
49
  end
50
- opts.on('-d', '--doctype DOCTYPE', ['article', 'book'],
51
- 'document type to use when rendering output: [article, book] (default: article)') do |doc_type|
50
+ opts.on('-d', '--doctype DOCTYPE', ['article', 'book', 'inline'],
51
+ 'document type to use when rendering output: [article, book, inline] (default: article)') do |doc_type|
52
52
  self[:attributes]['doctype'] = doc_type
53
53
  end
54
54
  opts.on('-o', '--out-file FILE', 'output file (default: based on input file path); use - to output to STDOUT') do |output_file|
@@ -78,12 +78,13 @@ Example: asciidoctor -b html5 source.asciidoc
78
78
  opts.on('-C', '--compact', 'compact the output by removing blank lines (default: false)') do
79
79
  self[:compact] = true
80
80
  end
81
- opts.on('-a', '--attribute key1=value,key2=value2,...', Array,
82
- 'a list of attributes, in the form key or key=value pair, to set on the document',
83
- 'these attributes take precedence over attributes defined in the source file') do |attribs|
81
+ opts.on('-a', '--attribute key[=value],key2[=value2],...', Array,
82
+ 'a list of document attributes to set in the form of key, key! or key=value pair',
83
+ 'unless @ is appended to the value, these attributes take precedence over attributes',
84
+ 'defined in the source document') do |attribs|
84
85
  attribs.each do |attrib|
85
- tokens = attrib.split('=')
86
- self[:attributes][tokens[0]] = tokens[1] || ''
86
+ key, val = attrib.split '=', 2
87
+ self[:attributes][key] = val || ''
87
88
  end
88
89
  end
89
90
  opts.on('-T', '--template-dir DIR', 'directory containing custom render templates the override the built-in set') do |template_dir|
@@ -104,13 +104,14 @@ class Document < AbstractBlock
104
104
 
105
105
  if options[:parent]
106
106
  @parent_document = options.delete(:parent)
107
- # should we dup here?
107
+ # should we dup attributes here?
108
108
  options[:attributes] = @parent_document.attributes
109
- options[:safe] ||= @parent_document.safe
110
109
  options[:base_dir] ||= @parent_document.base_dir
110
+ @safe = @parent_document.safe
111
111
  @renderer = @parent_document.renderer
112
112
  else
113
113
  @parent_document = nil
114
+ @safe = nil
114
115
  end
115
116
 
116
117
  @header = nil
@@ -124,7 +125,19 @@ class Document < AbstractBlock
124
125
  @counters = {}
125
126
  @callouts = Callouts.new
126
127
  @options = options
127
- @safe = @options.fetch(:safe, SafeMode::SECURE).to_i
128
+ # safely resolve the safe mode from const, int or string
129
+ if @safe.nil? && !(safe_mode = @options[:safe])
130
+ @safe = SafeMode::SECURE
131
+ elsif safe_mode.is_a?(Fixnum)
132
+ # be permissive in case API user wants to define new levels
133
+ @safe = safe_mode
134
+ else
135
+ begin
136
+ @safe = SafeMode.const_get(safe_mode.to_s.upcase).to_i
137
+ rescue
138
+ @safe = SafeMode::SECURE.to_i
139
+ end
140
+ end
128
141
  @options[:header_footer] = @options.fetch(:header_footer, false)
129
142
 
130
143
  @attributes['encoding'] = 'UTF-8'
@@ -184,11 +197,11 @@ class Document < AbstractBlock
184
197
 
185
198
  # allow common attributes backend and doctype to be set using options hash
186
199
  unless @options[:backend].nil?
187
- @attribute_overrides['backend'] = @options[:backend]
200
+ @attribute_overrides['backend'] = @options[:backend].to_s
188
201
  end
189
202
 
190
203
  unless @options[:doctype].nil?
191
- @attribute_overrides['doctype'] = @options[:doctype]
204
+ @attribute_overrides['doctype'] = @options[:doctype].to_s
192
205
  end
193
206
 
194
207
  if @safe >= SafeMode::SERVER
@@ -269,15 +282,6 @@ class Document < AbstractBlock
269
282
  Lexer.parse(@reader, self, :header_only => @options.fetch(:parse_header_only, false))
270
283
 
271
284
  @callouts.rewind
272
-
273
- Debug.debug {
274
- msg = []
275
- msg << "Found #{@blocks.size} blocks in this document:"
276
- @blocks.each {|b|
277
- msg << b
278
- }
279
- msg * "\n"
280
- }
281
285
  end
282
286
 
283
287
  # Public: Get the named counter and take the next number in the sequence.
@@ -441,6 +445,10 @@ class Document < AbstractBlock
441
445
  # Internal: Branch the attributes so that the original state can be restored
442
446
  # at a future time.
443
447
  def save_attributes
448
+ unless @attributes.has_key?('doctitle') || (val = doctitle).nil?
449
+ @attributes['doctitle'] = val
450
+ end
451
+
444
452
  # css-signature cannot be updated after header attributes are processed
445
453
  if @id.nil? && @attributes.has_key?('css-signature')
446
454
  @id = @attributes['css-signature']
@@ -578,27 +586,6 @@ class Document < AbstractBlock
578
586
  @attributes["filetype-#{file_type}"] = ''
579
587
  end
580
588
 
581
- def splain
582
- Debug.debug {
583
- msg = ''
584
- if @header
585
- msg = "Header is #{@header}"
586
- else
587
- msg = "No header"
588
- end
589
-
590
- msg += "I have #{@blocks.count} blocks"
591
- @blocks.each_with_index do |block, i|
592
- msg += "v" * 60
593
- msg += "Block ##{i} is a #{block.class}"
594
- msg += "Name is #{block.title rescue 'n/a'}"
595
- block.splain(0) if block.respond_to? :splain
596
- msg += "^" * 60
597
- end
598
- }
599
- nil
600
- end
601
-
602
589
  def renderer(opts = {})
603
590
  return @renderer if @renderer
604
591
 
@@ -627,11 +614,21 @@ class Document < AbstractBlock
627
614
  def render(opts = {})
628
615
  restore_attributes
629
616
  r = renderer(opts)
630
- @options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self)
617
+ if doctype == 'inline'
618
+ # QUESTION should we warn if @blocks.size > 0 and the first block is not a paragraph?
619
+ if @blocks.size > 0 && (block = @blocks.first).context == :paragraph
620
+ block.content
621
+ else
622
+ ''
623
+ end
624
+ else
625
+ @options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self)
626
+ end
631
627
  end
632
628
 
633
629
  def content
634
- # per AsciiDoc-spec, remove the title after rendering the header
630
+ # per AsciiDoc-spec, remove the title before rendering the body,
631
+ # regardless of whether the header is rendered)
635
632
  @attributes.delete('title')
636
633
  @blocks.map {|b| b.render }.join
637
634
  end
@@ -651,14 +648,7 @@ class Document < AbstractBlock
651
648
  if safe >= SafeMode::SECURE
652
649
  ''
653
650
  else
654
- if ext.nil?
655
- case @attributes['basebackend']
656
- when 'docbook'
657
- ext = '.xml'
658
- when 'html'
659
- ext = '.html'
660
- end
661
- end
651
+ ext = @attributes['outfilesuffix'] if ext.nil?
662
652
 
663
653
  content = nil
664
654
 
@@ -37,6 +37,16 @@ module Helpers
37
37
  end
38
38
  end
39
39
 
40
+ def self.mkdir_p(dir)
41
+ unless File.directory? dir
42
+ parent_dir = File.dirname(dir)
43
+ if !File.directory?(parent_dir = File.dirname(dir)) && parent_dir != '.'
44
+ mkdir_p(parent_dir)
45
+ end
46
+ Dir.mkdir(dir)
47
+ end
48
+ end
49
+
40
50
  # Public: A generic capture output routine to be used in templates
41
51
  #def self.capture_output(*args, &block)
42
52
  # Proc.new { block.call(*args) }
@@ -74,10 +74,33 @@ class Lexer
74
74
  # that precede first block
75
75
  block_attributes = parse_block_metadata_lines(reader, document)
76
76
 
77
+ # special case, block title is not allowed above document title,
78
+ # carry attributes over to the document body
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
84
+ end
85
+
86
+ # yep, document title logic in AsciiDoc is just insanity
87
+ # definitely an area for spec refinement
88
+ assigned_doctitle = nil
89
+ unless (val = document.attributes.fetch('doctitle', '')).empty?
90
+ document.title = val
91
+ assigned_doctitle = val
92
+ end
93
+
94
+ section_title = nil
77
95
  # check if the first line is the document title
78
96
  # if so, add a header to the document and parse the header metadata
79
97
  if is_next_line_document_title?(reader, block_attributes)
80
- document.id, document.title, _, _ = parse_section_title(reader, document)
98
+ document.id, doctitle, _, _ = parse_section_title(reader, document)
99
+ unless assigned_doctitle
100
+ document.title = doctitle
101
+ assigned_doctitle = doctitle
102
+ end
103
+ document.attributes['doctitle'] = section_title = doctitle
81
104
  # QUESTION: should this be encapsulated in document?
82
105
  if document.id.nil? && block_attributes.has_key?('id')
83
106
  document.id = block_attributes.delete('id')
@@ -85,8 +108,15 @@ class Lexer
85
108
  parse_header_metadata(reader, document)
86
109
  end
87
110
 
88
- if document.attributes.has_key? 'doctitle'
89
- document.title = document.attributes['doctitle']
111
+ if !(val = document.attributes.fetch('doctitle', '')).empty? &&
112
+ val != section_title
113
+ document.title = val
114
+ assigned_doctitle = val
115
+ end
116
+
117
+ # restore doctitle attribute to original assignment
118
+ if assigned_doctitle
119
+ document.attributes['doctitle'] = assigned_doctitle
90
120
  end
91
121
 
92
122
  document.clear_playback_attributes block_attributes
@@ -143,7 +173,7 @@ class Lexer
143
173
  # NOTE we could drop a hint in the attributes to indicate
144
174
  # that we are at a section title (so we don't have to check)
145
175
  if parent.is_a?(Document) && parent.blocks.empty? &&
146
- (parent.has_header? || !is_next_line_section?(reader, attributes))
176
+ (parent.has_header? || attributes.delete('invalid-header') || !is_next_line_section?(reader, attributes))
147
177
 
148
178
  if parent.has_header?
149
179
  preamble = Block.new(parent, :preamble)
@@ -193,11 +223,10 @@ class Lexer
193
223
  if next_level
194
224
  next_level += section.document.attr('leveloffset', 0).to_i
195
225
  doctype = parent.document.doctype
196
- if next_level == 0 && doctype != 'book'
197
- puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
198
- end
199
226
  if next_level > current_level || (section.is_a?(Document) && next_level == 0)
200
- unless expected_next_levels.nil? || expected_next_levels.include?(next_level)
227
+ if next_level == 0 && doctype != 'book'
228
+ puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
229
+ elsif !expected_next_levels.nil? && !expected_next_levels.include?(next_level)
201
230
  puts "asciidoctor: WARNING: line #{reader.lineno + 1}: section title out of sequence: " +
202
231
  "expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, " +
203
232
  "got level #{next_level}"
@@ -206,12 +235,15 @@ class Lexer
206
235
  new_section, attributes = next_section(reader, section, attributes)
207
236
  section << new_section
208
237
  else
238
+ if next_level == 0 && doctype != 'book'
239
+ puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
240
+ end
209
241
  # close this section (and break out of the nesting) to begin a new one
210
242
  break
211
243
  end
212
244
  else
213
245
  # just take one block or else we run the risk of overrunning section boundaries
214
- new_block = next_block(reader, section, attributes, :parse_metadata => false)
246
+ new_block = next_block(reader, (preamble || section), attributes, :parse_metadata => false)
215
247
  if !new_block.nil?
216
248
  (preamble || section) << new_block
217
249
  attributes = {}
@@ -224,8 +256,8 @@ class Lexer
224
256
  reader.skip_blank_lines
225
257
  end
226
258
 
227
- # drop the preamble if it has no content
228
- if preamble && preamble.blocks.empty?
259
+ if preamble && !preamble.blocks?
260
+ # drop the preamble if it has no content
229
261
  section.delete_at(0)
230
262
  end
231
263
 
@@ -260,11 +292,13 @@ class Lexer
260
292
  # bail if we've reached the end of the parent block or document
261
293
  return nil unless reader.has_more_lines?
262
294
 
295
+ text_only = options[:text]
263
296
  # check for option to find list item text only
264
297
  # if skipped a line, assume a list continuation was
265
298
  # used and block content is acceptable
266
- if options[:text] && skipped > 0
299
+ if text_only && skipped > 0
267
300
  options.delete(:text)
301
+ text_only = false
268
302
  end
269
303
 
270
304
  parse_metadata = options.fetch(:parse_metadata, true)
@@ -292,9 +326,8 @@ class Lexer
292
326
  block_context = nil
293
327
  terminator = nil
294
328
  # QUESTION put this inside call to rekey attributes?
295
- if attributes.has_key? 1
296
- explicit_style = attributes['style']
297
- style = attributes['style'] = attributes[1]
329
+ if attributes[1]
330
+ style, explicit_style = parse_style_attribute(attributes)
298
331
  end
299
332
 
300
333
  if delimited_blk_match = is_delimited_block?(this_line, true)
@@ -332,7 +365,7 @@ class Lexer
332
365
  end
333
366
 
334
367
  # process lines normally
335
- if !options[:text]
368
+ if !text_only
336
369
  # NOTE we're letting break lines (ruler, page_break, etc) have attributes
337
370
  if (match = this_line.match(REGEXP[:break_line]))
338
371
  block = Block.new(parent, BREAK_LINES[match[0][0..2]])
@@ -461,8 +494,7 @@ class Lexer
461
494
  break
462
495
 
463
496
  # FIXME create another set for "passthrough" styles
464
- # though partintro should likely be a dedicated block
465
- elsif !style.nil? && style != 'normal' && style != 'partintro'
497
+ elsif !style.nil? && style != 'normal'
466
498
  if PARAGRAPH_STYLES.include?(style)
467
499
  block_context = style.to_sym
468
500
  reader.unshift_line this_line
@@ -498,13 +530,7 @@ class Lexer
498
530
  (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
499
531
  }
500
532
 
501
- # trim off the indentation equivalent to the size of the least indented line
502
- if !buffer.empty?
503
- offset = buffer.map {|line| line.match(REGEXP[:leading_blanks])[1].length }.min
504
- if offset > 0
505
- buffer = buffer.map {|l| l.sub(/^\s{1,#{offset}}/, '') }
506
- end
507
- end
533
+ reset_block_indent! buffer
508
534
 
509
535
  block = Block.new(parent, :literal, buffer)
510
536
  # a literal gets special meaning inside of a definition list
@@ -541,12 +567,49 @@ class Lexer
541
567
 
542
568
  catalog_inline_anchors(buffer.join, document)
543
569
 
544
- if !options[:text] && (admonition_match = buffer.first.match(REGEXP[:admonition_inline]))
570
+ first_line = buffer.first
571
+ if !text_only && (admonition_match = first_line.match(REGEXP[:admonition_inline]))
545
572
  buffer[0] = admonition_match.post_match.lstrip
546
573
  block = Block.new(parent, :admonition, buffer)
547
574
  attributes['style'] = admonition_match[1]
548
575
  attributes['name'] = admonition_name = admonition_match[1].downcase
549
576
  attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
577
+ elsif !text_only && COMPLIANCE[:markdown_syntax] && first_line.start_with?('> ')
578
+ buffer.map! {|line|
579
+ if line.start_with?('> ')
580
+ line[2..-1]
581
+ elsif line.chomp == '>'
582
+ line[1..-1]
583
+ else
584
+ line
585
+ end
586
+ }
587
+
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
592
+ else
593
+ attribution, citetitle = nil
594
+ end
595
+ attributes['style'] = 'quote'
596
+ attributes['attribution'] = attribution unless attribution.nil?
597
+ attributes['citetitle'] = citetitle unless citetitle.nil?
598
+ # NOTE will only detect headings that are floating titles (not section titles)
599
+ # 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
607
+ attributes['style'] = 'quote'
608
+ attributes['attribution'] = attribution unless attribution.nil?
609
+ 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)
550
613
  else
551
614
  # QUESTION is this necessary?
552
615
  #if style == 'normal' && [' ', "\t"].include?(buffer.first[0..0])
@@ -565,6 +628,10 @@ class Lexer
565
628
 
566
629
  # either delimited block or styled paragraph
567
630
  if block.nil? && !block_context.nil?
631
+ # abstract and partintro should be handled by open block
632
+ # FIXME kind of hackish...need to sort out how to generalize this
633
+ block_context = :open if block_context == :abstract || block_context == :partintro
634
+
568
635
  case block_context
569
636
  when :admonition
570
637
  attributes['name'] = admonition_name = style.downcase
@@ -576,7 +643,7 @@ class Lexer
576
643
  return nil
577
644
 
578
645
  when :example
579
- block = build_block(block_context, :complex, terminator, parent, reader, attributes, true)
646
+ block = build_block(block_context, :complex, terminator, parent, reader, attributes, {:supports_caption => true})
580
647
 
581
648
  when :listing, :fenced_code, :source
582
649
  if block_context == :fenced_code
@@ -587,7 +654,7 @@ class Lexer
587
654
  elsif block_context == :source
588
655
  AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
589
656
  end
590
- block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, true)
657
+ block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, {:supports_caption => true})
591
658
 
592
659
  when :literal
593
660
  block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
@@ -600,6 +667,12 @@ class Lexer
600
667
 
601
668
  when :table
602
669
  block_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
670
+ case terminator[0..0]
671
+ when ','
672
+ attributes['format'] = 'csv'
673
+ when ':'
674
+ attributes['format'] = 'dsv'
675
+ end
603
676
  block = next_table(block_reader, parent, attributes)
604
677
 
605
678
  when :quote, :verse
@@ -621,7 +694,7 @@ class Lexer
621
694
  # REVIEW seems like there is a better way to organize this wrap-up
622
695
  block.id ||= attributes['id'] if attributes.has_key?('id')
623
696
  block.title = attributes['title'] unless block.title?
624
- block.caption ||= attributes['caption'] unless block.is_a?(Section)
697
+ block.caption ||= attributes.delete('caption')
625
698
  # AsciiDoc always use [id] as the reftext in HTML output,
626
699
  # but I'd like to do better in Asciidoctor
627
700
  if block.id && block.title? && !attributes.has_key?('reftext')
@@ -651,11 +724,13 @@ class Lexer
651
724
  tip = line[0..3]
652
725
  tl = 4
653
726
 
654
- # special case for fenced code blocks
655
- tip_alt = tip.chop
656
- if tip_alt == '```' || tip_alt == '~~~'
657
- tip = tip_alt
658
- tl = 3
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
733
+ end
659
734
  end
660
735
  end
661
736
 
@@ -689,8 +764,9 @@ class Lexer
689
764
  end
690
765
 
691
766
  # whether a block supports complex content should be a config setting
767
+ # if terminator is false, that means the all the lines in the reader should be parsed
692
768
  # NOTE could invoke filter in here, before and after parsing
693
- def self.build_block(block_context, content_type, terminator, parent, reader, attributes, supports_caption = false)
769
+ def self.build_block(block_context, content_type, terminator, parent, reader, attributes, options = {})
694
770
  if terminator.nil?
695
771
  if content_type == :verbatim
696
772
  buffer = reader.grab_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true)
@@ -706,14 +782,21 @@ class Lexer
706
782
  end
707
783
  elsif content_type != :complex
708
784
  buffer = reader.grab_lines_until(:terminator => terminator, :chomp_last_line => true)
785
+ elsif terminator == false
786
+ buffer = nil
787
+ block_reader = reader
709
788
  else
710
789
  buffer = nil
711
790
  block_reader = Reader.new reader.grab_lines_until(:terminator => terminator)
712
791
  end
713
792
 
793
+ if content_type == :verbatim && attributes.has_key?('indent')
794
+ reset_block_indent! buffer, attributes['indent'].to_i
795
+ end
796
+
714
797
  block = Block.new(parent, block_context, buffer)
715
798
  # should supports_caption be necessary?
716
- if supports_caption
799
+ if options.fetch(:supports_caption, false)
717
800
  block.title = attributes.delete('title') if attributes.has_key?('title')
718
801
  block.assign_caption attributes.delete('caption')
719
802
  end
@@ -1114,7 +1197,7 @@ class Lexer
1114
1197
  end
1115
1198
 
1116
1199
  #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.join}<BUFFER"
1117
- #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer}<BUFFER"
1200
+ #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.inspect}<BUFFER"
1118
1201
 
1119
1202
  buffer
1120
1203
  end
@@ -1130,33 +1213,39 @@ class Lexer
1130
1213
  def self.initialize_section(reader, parent, attributes = {})
1131
1214
  section = Section.new parent
1132
1215
  section.id, section.title, section.level, _ = parse_section_title(reader, section.document)
1133
- if section.id.nil? && attributes.has_key?('id')
1134
- section.id = attributes['id']
1135
- else
1136
- # generate an id if one was not *embedded* in the heading line
1137
- # or as an anchor above the section
1138
- section.id ||= section.generate_id
1139
- end
1140
-
1141
- if section.id
1142
- section.document.register(:ids, [section.id, section.title])
1143
- end
1144
-
1216
+ # parse style, id and role from first positional attribute
1145
1217
  if attributes[1]
1146
- section.sectname = attributes[1]
1218
+ section.sectname, _ = parse_style_attribute(attributes)
1147
1219
  section.special = true
1148
1220
  document = parent.document
1221
+ # HACK needs to be refactored so it's driven by config
1222
+ if section.sectname == 'abstract' && document.doctype == 'book'
1223
+ section.sectname = "sect1"
1224
+ section.special = false
1225
+ section.level = 1
1149
1226
  # FIXME refactor to use assign_caption (also check requirements)
1150
- if section.sectname == 'appendix' &&
1227
+ elsif section.sectname == 'appendix' &&
1151
1228
  !attributes.has_key?('caption') &&
1152
1229
  !document.attributes.has_key?('caption')
1153
1230
  number = document.counter('appendix-number', 'A')
1154
- attributes['caption'] = "#{document.attributes['appendix-caption']} #{number}: "
1231
+ section.caption = "#{document.attributes['appendix-caption']} #{number}: "
1155
1232
  Document::AttributeEntry.new('appendix-number', number).save_to(attributes)
1156
1233
  end
1157
1234
  else
1158
1235
  section.sectname = "sect#{section.level}"
1159
1236
  end
1237
+
1238
+ if section.id.nil? && (id = attributes['id'])
1239
+ section.id = id
1240
+ else
1241
+ # generate an id if one was not *embedded* in the heading line
1242
+ # or as an anchor above the section
1243
+ section.id ||= section.generate_id
1244
+ end
1245
+
1246
+ if section.id
1247
+ section.document.register(:ids, [section.id, section.title])
1248
+ end
1160
1249
  section.update_attributes(attributes)
1161
1250
  reader.skip_blank_lines
1162
1251
 
@@ -1173,8 +1262,8 @@ class Lexer
1173
1262
 
1174
1263
  #--
1175
1264
  # = is level 0, == is level 1, etc.
1176
- def self.single_line_section_level(line)
1177
- [line.length - 1, 0].max
1265
+ def self.single_line_section_level(marker)
1266
+ marker.length - 1
1178
1267
  end
1179
1268
 
1180
1269
  # Internal: Checks if the next line on the Reader is a section title
@@ -1185,7 +1274,7 @@ class Lexer
1185
1274
  # returns the section level if the Reader is positioned at a section title,
1186
1275
  # false otherwise
1187
1276
  def self.is_next_line_section?(reader, attributes)
1188
- return false if !attributes[1].nil? && ['float', 'discrete'].include?(attributes[1])
1277
+ return false if !(val = attributes[1]).nil? && ['float', 'discrete'].include?(val)
1189
1278
  return false if !reader.has_more_lines?
1190
1279
  is_section_title?(*reader.peek_lines(2))
1191
1280
  end
@@ -1218,7 +1307,8 @@ class Lexer
1218
1307
  end
1219
1308
 
1220
1309
  def self.is_single_line_section_title?(line1)
1221
- if !line1.nil? && (match = line1.match(REGEXP[:section_title]))
1310
+ if !line1.nil? && (line1.start_with?('=') || (COMPLIANCE[:markdown_syntax] && line1.start_with?('#'))) &&
1311
+ (match = line1.match(REGEXP[:section_title]))
1222
1312
  single_line_section_level match[1]
1223
1313
  else
1224
1314
  false
@@ -1226,8 +1316,8 @@ class Lexer
1226
1316
  end
1227
1317
 
1228
1318
  def self.is_two_line_section_title?(line1, line2)
1229
- if !line1.nil? && !line2.nil? && line1.match(REGEXP[:section_name]) &&
1230
- line2.match(REGEXP[:section_underline]) &&
1319
+ if !line1.nil? && !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) &&
1320
+ line2.match(REGEXP[:section_underline]) && line1.match(REGEXP[:section_name]) &&
1231
1321
  # chomp so that a (non-visible) endline does not impact calculation
1232
1322
  (line1.chomp.size - line2.chomp.size).abs <= 1
1233
1323
  section_level line2
@@ -1286,14 +1376,15 @@ class Lexer
1286
1376
  sect_level = -1
1287
1377
  single_line = true
1288
1378
 
1289
- if match = line1.match(REGEXP[:section_title])
1379
+ if (line1.start_with?('=') || (COMPLIANCE[:markdown_syntax] && line1.start_with?('#'))) &&
1380
+ (match = line1.match(REGEXP[:section_title]))
1290
1381
  sect_id = match[3]
1291
1382
  sect_title = match[2]
1292
1383
  sect_level = single_line_section_level match[1]
1293
1384
  else
1294
1385
  line2 = reader.peek_line
1295
- if !line2.nil? && (name_match = line1.match(REGEXP[:section_name])) &&
1296
- line2.match(REGEXP[:section_underline]) &&
1386
+ if !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) && line2.match(REGEXP[:section_underline]) &&
1387
+ (name_match = line1.match(REGEXP[:section_name])) &&
1297
1388
  # chomp so that a (non-visible) endline does not impact calculation
1298
1389
  (line1.chomp.size - line2.chomp.size).abs <= 1
1299
1390
  if anchor_match = name_match[1].match(REGEXP[:anchor_embedded])
@@ -1331,70 +1422,28 @@ class Lexer
1331
1422
  process_attribute_entries(reader, document)
1332
1423
 
1333
1424
  metadata = {}
1425
+ implicit_author = nil
1426
+ implicit_authors = nil
1334
1427
 
1335
1428
  if reader.has_more_lines? && !reader.peek_line.chomp.empty?
1336
- author_metadata = {}
1337
- keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
1338
- author_line = reader.get_line
1339
- author_line.split(REGEXP[:semicolon_delim]).each_with_index do |author_entry, idx|
1340
- author_entry.strip!
1341
- next if author_entry.empty?
1342
- map = {}
1343
- if idx.zero?
1344
- keys.each do |key|
1345
- map[key.to_sym] = key
1346
- end
1347
- else
1348
- keys.each do |key|
1349
- map[key.to_sym] = "#{key}_#{idx + 1}"
1429
+ author_metadata = process_authors reader.get_line
1430
+
1431
+ unless author_metadata.empty?
1432
+ # apply header subs and assign to document
1433
+ if !document.nil?
1434
+ author_metadata.map do |key, val|
1435
+ val = val.is_a?(String) ? document.apply_header_subs(val) : val
1436
+ document.attributes[key] = val if !document.attributes.has_key?(key)
1437
+ val
1350
1438
  end
1351
- end
1352
1439
 
1353
- if match = author_entry.match(REGEXP[:author_info])
1354
- author_metadata[map[:firstname]] = fname = match[1].tr('_', ' ')
1355
- author_metadata[map[:author]] = fname
1356
- author_metadata[map[:authorinitials]] = fname[0, 1]
1357
- if !match[2].nil? && !match[3].nil?
1358
- author_metadata[map[:middlename]] = mname = match[2].tr('_', ' ')
1359
- author_metadata[map[:lastname]] = lname = match[3].tr('_', ' ')
1360
- author_metadata[map[:author]] = [fname, mname, lname].join ' '
1361
- author_metadata[map[:authorinitials]] = [fname[0, 1], mname[0, 1], lname[0, 1]].join
1362
- elsif !match[2].nil?
1363
- author_metadata[map[:lastname]] = lname = match[2].tr('_', ' ')
1364
- author_metadata[map[:author]] = [fname, lname].join ' '
1365
- author_metadata[map[:authorinitials]] = [fname[0, 1], lname[0, 1]].join
1366
- end
1367
- author_metadata[map[:email]] = match[4] unless match[4].nil?
1368
- else
1369
- author_metadata[map[:author]] = author_metadata[map[:firstname]] = author_entry.strip.squeeze(' ')
1370
- author_metadata[map[:authorinitials]] = author_metadata[map[:firstname]][0, 1]
1440
+ implicit_author = document.attributes['author']
1441
+ implicit_authors = document.attributes['authors']
1371
1442
  end
1372
1443
 
1373
- author_metadata['authorcount'] = idx + 1
1374
- # only assign the _1 attributes if there are multiple authors
1375
- if idx == 1
1376
- keys.each do |key|
1377
- author_metadata["#{key}_1"] = author_metadata[key] if author_metadata.has_key? key
1378
- end
1379
- end
1380
- if idx.zero?
1381
- author_metadata['authors'] = author_metadata[map[:author]]
1382
- else
1383
- author_metadata['authors'] = "#{author_metadata['authors']}, #{author_metadata[map[:author]]}"
1384
- end
1444
+ metadata = author_metadata
1385
1445
  end
1386
1446
 
1387
- # apply header subs and assign to document
1388
- if !document.nil?
1389
- author_metadata.map do |key, val|
1390
- val = val.is_a?(String) ? document.apply_header_subs(val) : val
1391
- document.attributes[key] = val if !document.attributes.has_key?(key)
1392
- val
1393
- end
1394
- end
1395
-
1396
- metadata = author_metadata.dup
1397
-
1398
1447
  # NOTE this will discard any comment lines, but not skip blank lines
1399
1448
  process_attribute_entries(reader, document)
1400
1449
 
@@ -1412,18 +1461,18 @@ class Lexer
1412
1461
  end
1413
1462
  end
1414
1463
 
1415
- # apply header subs and assign to document
1416
- if !document.nil?
1417
- rev_metadata.map do |key, val|
1418
- val = document.apply_header_subs(val)
1419
- document.attributes[key] = val if !document.attributes.has_key?(key)
1420
- val
1464
+ unless rev_metadata.empty?
1465
+ # apply header subs and assign to document
1466
+ if !document.nil?
1467
+ rev_metadata.map do |key, val|
1468
+ val = document.apply_header_subs(val)
1469
+ document.attributes[key] = val if !document.attributes.has_key?(key)
1470
+ val
1471
+ end
1421
1472
  end
1422
- end
1423
1473
 
1424
- rev_metadata.each {|k, v|
1425
- metadata[k] = v
1426
- }
1474
+ metadata.update rev_metadata
1475
+ end
1427
1476
 
1428
1477
  # NOTE this will discard any comment lines, but not skip blank lines
1429
1478
  process_attribute_entries(reader, document)
@@ -1431,9 +1480,119 @@ class Lexer
1431
1480
  reader.skip_blank_lines
1432
1481
  end
1433
1482
 
1483
+ if !document.nil?
1484
+ # process author attribute entries that override (or stand in for) the implicit author line
1485
+ author_metadata = nil
1486
+ if document.attributes.has_key?('author') &&
1487
+ (author_line = document.attributes['author']) != implicit_author
1488
+ # do not allow multiple, process as names only
1489
+ author_metadata = process_authors author_line, true, false
1490
+ elsif document.attributes.has_key?('authors') &&
1491
+ (author_line = document.attributes['authors']) != implicit_authors
1492
+ # allow multiple, process as names only
1493
+ author_metadata = process_authors author_line, true
1494
+ else
1495
+ authors = []
1496
+ author_key = "author_#{authors.size + 1}"
1497
+ while document.attributes.has_key? author_key
1498
+ authors << document.attributes[author_key]
1499
+ author_key = "author_#{authors.size + 1}"
1500
+ end
1501
+ if authors.size == 1
1502
+ # do not allow multiple, process as names only
1503
+ author_metadata = process_authors authors.first, true, false
1504
+ elsif authors.size > 1
1505
+ # allow multiple, process as names only
1506
+ author_metadata = process_authors authors.join('; '), true
1507
+ end
1508
+ end
1509
+
1510
+ unless author_metadata.nil?
1511
+ document.attributes.update author_metadata
1512
+
1513
+ # special case
1514
+ if !document.attributes.has_key?('email') && document.attributes.has_key?('email_1')
1515
+ document.attributes['email'] = document.attributes['email_1']
1516
+ end
1517
+ end
1518
+ end
1519
+
1434
1520
  metadata
1435
1521
  end
1436
1522
 
1523
+ # Internal: Parse the author line into a Hash of author metadata
1524
+ #
1525
+ # author_line - the String author line
1526
+ # names_only - a Boolean flag that indicates whether to process line as
1527
+ # names only or names with emails (default: false)
1528
+ # multiple - a Boolean flag that indicates whether to process multiple
1529
+ # semicolon-separated entries in the author line (default: true)
1530
+ #
1531
+ # returns a Hash of author metadata
1532
+ def self.process_authors(author_line, names_only = false, multiple = true)
1533
+ author_metadata = {}
1534
+ keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
1535
+ author_entries = multiple ? author_line.split(';').map(&:strip) : [author_line]
1536
+ author_entries.each_with_index do |author_entry, idx|
1537
+ author_entry.strip!
1538
+ next if author_entry.empty?
1539
+ key_map = {}
1540
+ if idx.zero?
1541
+ keys.each do |key|
1542
+ key_map[key.to_sym] = key
1543
+ end
1544
+ else
1545
+ keys.each do |key|
1546
+ key_map[key.to_sym] = "#{key}_#{idx + 1}"
1547
+ end
1548
+ end
1549
+
1550
+ segments = nil
1551
+ if names_only
1552
+ # splitting on ' ' will collapse repeating spaces
1553
+ segments = author_entry.split(' ', 3)
1554
+ elsif (match = author_entry.match(REGEXP[:author_info]))
1555
+ segments = match.to_a
1556
+ segments.shift
1557
+ end
1558
+
1559
+ unless segments.nil?
1560
+ author_metadata[key_map[:firstname]] = fname = segments[0].tr('_', ' ')
1561
+ author_metadata[key_map[:author]] = fname
1562
+ author_metadata[key_map[:authorinitials]] = fname[0, 1]
1563
+ if !segments[1].nil? && !segments[2].nil?
1564
+ author_metadata[key_map[:middlename]] = mname = segments[1].tr('_', ' ')
1565
+ author_metadata[key_map[:lastname]] = lname = segments[2].tr('_', ' ')
1566
+ author_metadata[key_map[:author]] = [fname, mname, lname].join ' '
1567
+ author_metadata[key_map[:authorinitials]] = [fname[0, 1], mname[0, 1], lname[0, 1]].join
1568
+ elsif !segments[1].nil?
1569
+ author_metadata[key_map[:lastname]] = lname = segments[1].tr('_', ' ')
1570
+ author_metadata[key_map[:author]] = [fname, lname].join ' '
1571
+ author_metadata[key_map[:authorinitials]] = [fname[0, 1], lname[0, 1]].join
1572
+ end
1573
+ author_metadata[key_map[:email]] = segments[3] unless names_only || segments[3].nil?
1574
+ else
1575
+ author_metadata[key_map[:author]] = author_metadata[key_map[:firstname]] = fname = author_entry.strip.squeeze(' ')
1576
+ author_metadata[key_map[:authorinitials]] = fname[0, 1]
1577
+ end
1578
+
1579
+ author_metadata['authorcount'] = idx + 1
1580
+ # only assign the _1 attributes if there are multiple authors
1581
+ if idx == 1
1582
+ keys.each do |key|
1583
+ author_metadata["#{key}_1"] = author_metadata[key] if author_metadata.has_key? key
1584
+ end
1585
+ end
1586
+ if idx.zero?
1587
+ author_metadata['authors'] = author_metadata[key_map[:author]]
1588
+ else
1589
+ author_metadata['authors'] = "#{author_metadata['authors']}, #{author_metadata[key_map[:author]]}"
1590
+ end
1591
+ end
1592
+
1593
+ author_metadata
1594
+ end
1595
+
1437
1596
  # Internal: Parse lines of metadata until a line of metadata is not found.
1438
1597
  #
1439
1598
  # This method processes sequential lines containing block metadata, ignoring
@@ -1537,29 +1696,41 @@ class Lexer
1537
1696
  end
1538
1697
  end
1539
1698
 
1540
- if name.end_with?('!')
1541
- # a nil value signals the attribute should be deleted (undefined)
1542
- value = nil
1543
- name = name.chop
1544
- end
1545
-
1546
- name = sanitize_attribute_name(name)
1547
- accessible = true
1548
- if !parent.nil?
1549
- accessible = value.nil? ?
1550
- parent.document.delete_attribute(name) :
1551
- parent.document.set_attribute(name, value)
1552
- end
1553
-
1554
- if !attributes.nil?
1555
- Document::AttributeEntry.new(name, value).save_to(attributes) if accessible
1556
- end
1699
+ store_attribute(name, value, parent.nil? ? nil : parent.document, attributes)
1557
1700
  true
1558
1701
  else
1559
1702
  false
1560
1703
  end
1561
1704
  end
1562
1705
 
1706
+ # Public: Store the attribute in the document and register attribute entry if accessible
1707
+ #
1708
+ # name - the String name of the attribute to store
1709
+ # value - the String value of the attribute to store
1710
+ # doc - the Document being parsed
1711
+ # attrs - the attributes for the current context
1712
+ #
1713
+ # returns a 2-element array containing the attribute name and value
1714
+ def self.store_attribute(name, value, doc = nil, attrs = nil)
1715
+ if name.end_with?('!')
1716
+ # a nil value signals the attribute should be deleted (undefined)
1717
+ value = nil
1718
+ name = name.chop
1719
+ end
1720
+
1721
+ name = sanitize_attribute_name(name)
1722
+ accessible = true
1723
+ unless doc.nil?
1724
+ accessible = value.nil? ? doc.delete_attribute(name) : doc.set_attribute(name, value)
1725
+ end
1726
+
1727
+ unless !accessible || attrs.nil?
1728
+ Document::AttributeEntry.new(name, value).save_to(attrs)
1729
+ end
1730
+
1731
+ [name, value]
1732
+ end
1733
+
1563
1734
  # Internal: Resolve the 0-index marker for this list item
1564
1735
  #
1565
1736
  # For ordered lists, match the marker used for this list item against the
@@ -1742,9 +1913,9 @@ class Lexer
1742
1913
  if parser_ctx.format == 'psv'
1743
1914
  next_cell_spec, cell_text = parse_cell_spec(m.pre_match, :end)
1744
1915
  parser_ctx.push_cell_spec next_cell_spec
1745
- parser_ctx.buffer << cell_text
1916
+ parser_ctx.buffer = %(#{parser_ctx.buffer}#{cell_text})
1746
1917
  else
1747
- parser_ctx.buffer << m.pre_match
1918
+ parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match})
1748
1919
  end
1749
1920
 
1750
1921
  line = m.post_match
@@ -1752,10 +1923,10 @@ class Lexer
1752
1923
  else
1753
1924
  # no other delimiters to see here
1754
1925
  # suck up this line into the buffer and move on
1755
- parser_ctx.buffer << line
1926
+ parser_ctx.buffer = %(#{parser_ctx.buffer}#{line})
1756
1927
  # QUESTION make this an option? (unwrap-option?)
1757
1928
  if parser_ctx.format == 'csv'
1758
- parser_ctx.buffer.rstrip!.concat(' ')
1929
+ parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} )
1759
1930
  end
1760
1931
  line = ''
1761
1932
  if parser_ctx.format == 'psv' || (parser_ctx.format == 'csv' &&
@@ -1891,6 +2062,143 @@ class Lexer
1891
2062
  [spec, rest]
1892
2063
  end
1893
2064
 
2065
+ # Public: Parse the first positional attribute and assign named attributes
2066
+ #
2067
+ # Parse the first positional attribute to extract the style, role and id
2068
+ # parts, assign the values to their cooresponding attribute keys and return
2069
+ # both the original style attribute and the parsed value from the first
2070
+ # positional attribute.
2071
+ #
2072
+ # attributes - The Hash of attributes to process
2073
+ #
2074
+ # Examples
2075
+ #
2076
+ # puts attributes
2077
+ # => {1 => "abstract#intro.lead", "style" => "preamble"}
2078
+ #
2079
+ # parse_style_attribute(attributes)
2080
+ # => ["abstract", "preamble"]
2081
+ #
2082
+ # puts attributes
2083
+ # => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro", "role" => "lead"}
2084
+ #
2085
+ # Returns a two-element Array of the parsed style from the
2086
+ # first positional attribute and the original style that was
2087
+ # replaced
2088
+ def self.parse_style_attribute(attributes)
2089
+ original_style = attributes['style']
2090
+ raw_style = attributes[1]
2091
+ if !raw_style || raw_style.include?(' ')
2092
+ attributes['style'] = raw_style
2093
+ [raw_style, original_style]
2094
+ # FIXME this logic could be condensed
2095
+ 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('.', ' ')
2105
+ else
2106
+ attributes['id'] = id
2107
+ 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('.', ' ')
2115
+ else
2116
+ attributes['role'] = roles.tr('.', ' ')
2117
+ end
2118
+ else
2119
+ parsed_style = attributes['style'] = raw_style
2120
+ end
2121
+
2122
+ [parsed_style, original_style]
2123
+ end
2124
+ end
2125
+
2126
+ # Remove the indentation (block offset) shared by all the lines, then
2127
+ # indent the lines by the specified amount if specified
2128
+ #
2129
+ # Trim the leading whitespace (indentation) equivalent to the length
2130
+ # of the indent on the least indented line. If the indent argument
2131
+ # is specified, indent the lines by this many spaces (columns).
2132
+ #
2133
+ # The purpose of this method is to shift a block of text to
2134
+ # align to the left margin, while still preserving the relative
2135
+ # indentation between lines
2136
+ #
2137
+ # lines - the Array of String lines to process
2138
+ # indent - the integer number of spaces to add to the beginning
2139
+ # of each line; if this value is nil, the existing
2140
+ # space is preserved (optional, default: 0)
2141
+ #
2142
+ # Examples
2143
+ #
2144
+ # source = <<EOS
2145
+ # def names
2146
+ # @name.split ' ')
2147
+ # end
2148
+ # EOS
2149
+ #
2150
+ # source.lines.entries
2151
+ # # => [" def names\n", " @names.split ' '\n", " end\n"]
2152
+ #
2153
+ # Lexer.reset_block_indent(source.lines.entries)
2154
+ # # => ["def names\n", " @names.split ' '\n", "end\n"]
2155
+ #
2156
+ # puts Lexer.reset_block_indent(source.lines.entries).join
2157
+ # # => def names
2158
+ # # => @names.split ' '
2159
+ # # => end
2160
+ #
2161
+ # returns the Array of String lines with block offset removed
2162
+ def self.reset_block_indent!(lines, indent = 0)
2163
+ return if indent.nil? || lines.empty?
2164
+
2165
+ tab_detected = false
2166
+ # TODO make tab size configurable
2167
+ tab_expansion = ' '
2168
+ # strip leading block indent
2169
+ offsets = lines.map do |line|
2170
+ # break if the first char is non-whitespace
2171
+ break [] unless line.chomp[0..0].lstrip.empty?
2172
+ if line.include? "\t"
2173
+ tab_detected = true
2174
+ line = line.gsub("\t", tab_expansion)
2175
+ end
2176
+ if (flush_line = line.lstrip).empty?
2177
+ nil
2178
+ elsif (offset = line.length - flush_line.length) == 0
2179
+ break []
2180
+ else
2181
+ offset
2182
+ end
2183
+ end
2184
+
2185
+ unless offsets.empty? || (offsets = offsets.compact).empty?
2186
+ if (offset = offsets.min) > 0
2187
+ lines.map! {|line|
2188
+ line = line.gsub("\t", tab_expansion) if tab_detected
2189
+ line[offset..-1] || "\n"
2190
+ }
2191
+ end
2192
+ end
2193
+
2194
+ if indent > 0
2195
+ padding = ' ' * indent
2196
+ lines.map! {|line| %(#{padding}#{line}) }
2197
+ end
2198
+
2199
+ nil
2200
+ end
2201
+
1894
2202
  # Public: Convert a string to a legal attribute name.
1895
2203
  #
1896
2204
  # name - the String name of the attribute