asciidoctor 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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