asciidoctor 0.0.5 → 0.0.6

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.

@@ -4,14 +4,14 @@ class Asciidoctor::Document
4
4
 
5
5
  include Asciidoctor
6
6
 
7
- # Public: Get the Hash of defines
8
- attr_reader :defines
7
+ # Public: Get the Hash of attributes
8
+ attr_reader :attributes
9
9
 
10
10
  # Public: Get the Hash of document references
11
11
  attr_reader :references
12
12
 
13
- # Need these for pseudo-template yum
14
- attr_reader :header, :preamble
13
+ # The section level 0 element
14
+ attr_reader :header
15
15
 
16
16
  # Public: Get the Array of elements (really Blocks or Sections) for the document
17
17
  attr_reader :elements
@@ -29,13 +29,25 @@ class Asciidoctor::Document
29
29
  def initialize(data, options = {}, &block)
30
30
  @elements = []
31
31
  @options = options
32
+ @options[:header_footer] = @options.fetch(:header_footer, true)
32
33
 
33
- @reader = Reader.new(data, &block)
34
+ @attributes = {}
35
+ @attributes['sectids'] = nil
36
+
37
+ @reader = Reader.new(data, @attributes, &block)
34
38
 
35
39
  # pseudo-delegation :)
36
- @defines = @reader.defines
40
+ #@attributes = @reader.attributes
37
41
  @references = @reader.references
38
42
 
43
+ # dynamic intrinstic attribute values
44
+ @attributes['doctype'] ||= DEFAULT_DOCTYPE
45
+ now = Time.new
46
+ @attributes['localdate'] ||= now.strftime('%Y-%m-%d')
47
+ @attributes['localtime'] ||= now.strftime('%H:%m:%S %Z')
48
+ @attributes['localdatetime'] ||= [@attributes['localdate'], @attributes['localtime']].join(' ')
49
+ @attributes['asciidoctor-version'] = VERSION
50
+
39
51
  # Now parse @lines into elements
40
52
  while @reader.has_lines?
41
53
  @reader.skip_blank
@@ -48,11 +60,11 @@ class Asciidoctor::Document
48
60
  Asciidoctor.debug el
49
61
  end
50
62
 
63
+ # split off the level 0 section, if present
51
64
  root = @elements.first
52
- # Try to find a @header from the Section blocks we have (if any).
53
65
  if root.is_a?(Section) && root.level == 0
54
66
  @header = @elements.shift
55
- @elements = @header.blocks + @elements
67
+ @elements = @header.blocks
56
68
  @header.clear_blocks
57
69
  end
58
70
 
@@ -63,21 +75,43 @@ class Asciidoctor::Document
63
75
  @reader.source if @reader
64
76
  end
65
77
 
66
- # We need to be able to return some semblance of a title
78
+ def attr(name, default = nil)
79
+ default.nil? ? @attributes[name.to_s] : @attributes.fetch(name.to_s, default)
80
+ #default.nil? ? @attributes[name.to_s.tr('_', '-')] : @attributes.fetch(name.to_s.tr('_', '-'), default)
81
+ end
82
+
83
+ def attr?(name)
84
+ @attributes.has_key? name.to_s
85
+ #@attributes.has_key? name.to_s.tr('_', '-')
86
+ end
87
+
88
+ def level
89
+ 0
90
+ end
91
+
92
+ # The title explicitly defined in the document attributes
67
93
  def title
68
- return @title if @title
94
+ @attributes['title']
95
+ end
96
+
97
+ # We need to be able to return some semblance of a title
98
+ def doctitle
99
+ # cached value
100
+ return @doctitle if @doctitle
69
101
 
70
102
  if @header
71
- @title = @header.title || @header.name
103
+ @doctitle = @header.title
72
104
  elsif @elements.first
73
- @title = @elements.first.title
74
- # Blocks don't have a :name method, but Sections do
75
- @title ||= @elements.first.name if @elements.first.respond_to? :name
105
+ @doctitle = @elements.first.title
76
106
  end
77
107
 
78
- @title
108
+ @doctitle
109
+ end
110
+ alias :name :doctitle
111
+
112
+ def notitle
113
+ @attributes.has_key? 'notitle'
79
114
  end
80
- alias :name :title
81
115
 
82
116
  def splain
83
117
  if @header
@@ -110,11 +144,13 @@ class Asciidoctor::Document
110
144
  @renderer = Renderer.new(render_options)
111
145
  end
112
146
 
113
- # Public: Render the Asciidoc document using erb templates
114
- #
147
+ # Public: Render the Asciidoc document using the templates
148
+ # loaded by Renderer. If a :template_dir is not specified,
149
+ # or a template is missing, the renderer will fall back to
150
+ # using the appropriate built-in template.
115
151
  def render(options = {})
116
152
  r = renderer(options)
117
- html = r.render('document', self, :header => @header, :preamble => @preamble)
153
+ @options.merge(options)[:header_footer] ? r.render('document', self) : content
118
154
  end
119
155
 
120
156
  def content
@@ -123,7 +159,7 @@ class Asciidoctor::Document
123
159
  Asciidoctor::debug "Rendering element: #{element}"
124
160
  html_pieces << element.render
125
161
  end
126
- html_pieces.join("\n")
162
+ html_pieces.join
127
163
  end
128
164
 
129
165
  end
@@ -28,6 +28,7 @@ class Asciidoctor::Lexer
28
28
  reader.skip_blank
29
29
 
30
30
  return nil unless reader.has_lines?
31
+ context = parent.is_a?(Block) ? parent.context : nil
31
32
 
32
33
  # NOTE: An anchor looks like this:
33
34
  # [[foo]]
@@ -45,6 +46,11 @@ class Asciidoctor::Lexer
45
46
  anchor = nil
46
47
  end
47
48
 
49
+ # skip a list continuation character if we're processing a list
50
+ if LIST_CONTEXTS.include?(context)
51
+ reader.skip_list_continuation
52
+ end
53
+
48
54
  Asciidoctor.debug "/"*64
49
55
  Asciidoctor.debug "#{File.basename(__FILE__)}:#{__LINE__} -> #{__method__} - First two lines are:"
50
56
  Asciidoctor.debug reader.peek_line
@@ -56,8 +62,9 @@ class Asciidoctor::Lexer
56
62
  block = nil
57
63
  title = nil
58
64
  caption = nil
59
- source_type = nil
60
65
  buffer = []
66
+ attributes = {}
67
+ context = parent.is_a?(Block) ? parent.context : nil
61
68
  while reader.has_lines? && block.nil?
62
69
  buffer.clear
63
70
  this_line = reader.get_line
@@ -65,35 +72,36 @@ class Asciidoctor::Lexer
65
72
 
66
73
  if this_line.match(REGEXP[:comment_blk])
67
74
  Reader.new(reader.grab_lines_until {|line| line.match( REGEXP[:comment_blk] ) })
68
- next
69
75
 
70
76
  elsif this_line.match(REGEXP[:comment])
71
- next
72
-
73
- elsif match = this_line.match(REGEXP[:title])
74
- title = match[1]
75
77
  reader.skip_blank
76
78
 
77
- elsif match = this_line.match(REGEXP[:listing_source])
78
- source_type = match[1]
79
+ elsif match = this_line.match(REGEXP[:attr_list_blk])
80
+ collect_attributes(match[1], attributes)
79
81
  reader.skip_blank
80
82
 
81
- elsif match = this_line.match(REGEXP[:caption])
82
- caption = match[1]
83
-
84
83
  elsif is_section_heading?(this_line, next_line)
85
84
  # If we've come to a new section, then we've found the end of this
86
85
  # current block. Likewise if we'd found an unassigned anchor, push
87
- # it back as well, so it can go with this next heading.
88
- # NOTE - I don't think this will assign the anchor properly. Anchors
89
- # only match with double brackets - [[foo]], but what's stored in
90
- # `anchor` at this point is only the `foo` part that was stripped out
91
- # after matching. TODO: Need a way to test this.
86
+ #
87
+ # FIXME when slurping up next section, give back trailing anchor to following section
92
88
  reader.unshift(this_line)
93
- reader.unshift(anchor) unless anchor.nil?
94
89
  Asciidoctor.debug "#{__method__}: SENDING to next_section with lines[0] = #{reader.peek_line}"
95
90
  block = next_section(reader, parent)
96
91
 
92
+ elsif match = this_line.match(REGEXP[:title])
93
+ title = match[1]
94
+ reader.skip_blank
95
+
96
+ elsif match = this_line.match(REGEXP[:image_blk])
97
+ collect_attributes(match[2], attributes, ['alt', 'width', 'height'])
98
+ block = Block.new(parent, :image)
99
+ # FIXME this seems kind of one-off here
100
+ target = block.sub_attributes(match[1])
101
+ attributes['target'] = target
102
+ attributes['alt'] ||= File.basename(target, File.extname(target))
103
+ reader.skip_blank
104
+
97
105
  elsif this_line.match(REGEXP[:oblock])
98
106
  # oblock is surrounded by '--' lines and has zero or more blocks inside
99
107
  buffer = Reader.new(reader.grab_lines_until { |line| line.match(REGEXP[:oblock]) })
@@ -109,12 +117,25 @@ class Asciidoctor::Lexer
109
117
  block.blocks << new_block unless new_block.nil?
110
118
  end
111
119
 
120
+ # needs to come before list detection
121
+ elsif this_line.match(REGEXP[:sidebar_blk])
122
+ # sidebar is surrounded by '****' (4 or more '*' chars) lines
123
+ # FIXME violates DRY because it's a duplication of quote parsing
124
+ block = Block.new(parent, :sidebar)
125
+ buffer = Reader.new(reader.grab_lines_until {|line| line.match( REGEXP[:sidebar_blk] ) })
126
+
127
+ while buffer.has_lines?
128
+ new_block = next_block(buffer, block)
129
+ block.blocks << new_block unless new_block.nil?
130
+ end
131
+
112
132
  elsif list_type = [:olist, :colist].detect{|l| this_line.match( REGEXP[l] )}
113
133
  items = []
114
134
  Asciidoctor.debug "Creating block of type: #{list_type}"
115
135
  block = Block.new(parent, list_type)
136
+ attributes['style'] ||= 'arabic'
116
137
  while !this_line.nil? && match = this_line.match(REGEXP[list_type])
117
- item = ListItem.new
138
+ item = ListItem.new(block)
118
139
 
119
140
  reader.unshift match[2].lstrip.sub(/^\./, '\.')
120
141
  item_segment = Reader.new(list_item_segment(reader, :alt_ending => REGEXP[list_type]))
@@ -123,11 +144,7 @@ class Asciidoctor::Lexer
123
144
  item.blocks << new_block unless new_block.nil?
124
145
  end
125
146
 
126
- if item.blocks.any? &&
127
- item.blocks.first.is_a?(Block) &&
128
- (item.blocks.first.context == :paragraph || item.blocks.first.context == :literal)
129
- item.content = item.blocks.shift.buffer.map{|l| l.strip}.join("\n")
130
- end
147
+ item.fold_first
131
148
 
132
149
  items << item
133
150
 
@@ -140,74 +157,87 @@ class Asciidoctor::Lexer
140
157
  block.buffer = items
141
158
 
142
159
  elsif match = this_line.match(REGEXP[:ulist])
143
-
144
160
  reader.unshift(this_line)
145
161
  block = build_ulist(reader, parent)
146
162
 
147
163
  elsif match = this_line.match(REGEXP[:dlist])
164
+ # TODO build_dlist method?
148
165
  pairs = []
149
166
  block = Block.new(parent, :dlist)
167
+ # allows us to capture until we find a labeled item using the same delimiter (::, :::, :::: or ;;)
168
+ sibling_matcher = REGEXP[:dlist_siblings][match[3]]
150
169
 
151
- this_dlist = Regexp.new(/^#{match[1]}(.*)#{match[3]}\s*$/)
152
-
153
- while !this_line.nil? && match = this_line.match(this_dlist)
154
- if anchor = match[1].match( /\[\[([^\]]+)\]\]/ )
155
- dt = ListItem.new( $` + $' )
156
- dt.anchor = anchor[1]
157
- else
158
- dt = ListItem.new( match[1] )
159
- end
160
- dd = ListItem.new
161
- # workaround eg. git-config OPTIONS --get-colorbool
162
- reader.get_line if reader.has_lines? && reader.peek_line.strip.empty?
170
+ begin
171
+ dt = ListItem.new(block, match[2])
172
+ dt.anchor = match[1] unless match[1].nil?
173
+ dd = ListItem.new(block, match[5])
163
174
 
164
- dd_segment = Reader.new(list_item_segment(reader, :alt_ending => this_dlist))
175
+ dd_segment = Reader.new(list_item_segment(reader, :alt_ending => sibling_matcher))
165
176
  while dd_segment.has_lines?
166
177
  new_block = next_block(dd_segment, block)
167
178
  dd.blocks << new_block unless new_block.nil?
168
179
  end
169
180
 
170
- if dd.blocks.any? &&
171
- dd.blocks.first.is_a?(Block) &&
172
- (dd.blocks.first.context == :paragraph || dd.blocks.first.context == :literal)
173
- dd.content = dd.blocks.shift.buffer.map{|l| l.strip}.join("\n")
174
- end
181
+ dd.fold_first
175
182
 
176
183
  pairs << [dt, dd]
177
184
 
185
+ # this skip_blank might be redundant
178
186
  reader.skip_blank
179
-
180
187
  this_line = reader.get_line
181
- end
188
+ end while !this_line.nil? && match = this_line.match(sibling_matcher)
189
+
182
190
  reader.unshift(this_line) unless this_line.nil?
183
191
  block.buffer = pairs
184
-
185
- elsif this_line.match(REGEXP[:verse])
186
- # verse is preceded by [verse] and lasts until a blank line
187
- buffer = reader.grab_lines_until(:break_on_blank_lines => true)
188
- block = Block.new(parent, :verse, buffer)
189
-
190
- elsif this_line.match(REGEXP[:note])
191
- # note is an admonition preceded by [NOTE] and lasts until a blank line
192
- buffer = reader.grab_lines_until(:break_on_blank_lines => true)
193
- block = Block.new(parent, :note, buffer)
194
-
195
- elsif block_type = [:listing, :example].detect{|t| this_line.match( REGEXP[t] )}
196
- buffer = reader.grab_lines_until {|line| line.match( REGEXP[block_type] )}
197
- block = Block.new(parent, block_type, buffer)
198
-
199
- elsif this_line.match( REGEXP[:quote] )
200
- block = Block.new(parent, :quote)
201
- buffer = Reader.new(reader.grab_lines_until {|line| line.match( REGEXP[:quote] ) })
192
+
193
+ # FIXME violates DRY because it's a duplication of other block parsing
194
+ elsif this_line.match(REGEXP[:example])
195
+ # example is surrounded by lines with 4 or more '=' chars
196
+ rekey_positional_attributes(attributes, ['style'])
197
+ if admonition_style = ADMONITION_STYLES.detect {|s| attributes['style'] == s}
198
+ block = Block.new(parent, :admonition)
199
+ attributes['name'] = admonition_style.downcase
200
+ attributes['caption'] ||= admonition_style.capitalize
201
+ else
202
+ block = Block.new(parent, :example)
203
+ end
204
+ buffer = Reader.new(reader.grab_lines_until {|line| line.match( REGEXP[:example] ) })
202
205
 
203
206
  while buffer.has_lines?
204
207
  new_block = next_block(buffer, block)
205
208
  block.blocks << new_block unless new_block.nil?
206
209
  end
207
210
 
211
+ # FIXME violates DRY w/ non-delimited block listing
212
+ elsif this_line.match(REGEXP[:listing])
213
+ rekey_positional_attributes(attributes, ['style', 'language', 'linenums'])
214
+ buffer = reader.grab_lines_until {|line| line.match( REGEXP[:listing] )}
215
+ buffer.last.chomp! unless buffer.empty?
216
+ block = Block.new(parent, :listing, buffer)
217
+
218
+ elsif this_line.match(REGEXP[:quote])
219
+ # multi-line verse or quote is surrounded by a block delimiter
220
+ rekey_positional_attributes(attributes, ['style', 'attribution', 'citetitle'])
221
+ quote_context = (attributes['style'] == 'verse' ? :verse : :quote)
222
+ buffer = Reader.new(reader.grab_lines_until {|line| line.match( REGEXP[:quote] ) })
223
+
224
+ # only quote can have other section elements (as as section block)
225
+ section_body = (quote_context == :quote)
226
+
227
+ if section_body
228
+ block = Block.new(parent, quote_context)
229
+ while buffer.has_lines?
230
+ new_block = next_block(buffer, block)
231
+ block.blocks << new_block unless new_block.nil?
232
+ end
233
+ else
234
+ block = Block.new(parent, quote_context, buffer.lines)
235
+ end
236
+
208
237
  elsif this_line.match(REGEXP[:lit_blk])
209
238
  # example is surrounded by '....' (4 or more '.' chars) lines
210
239
  buffer = reader.grab_lines_until {|line| line.match( REGEXP[:lit_blk] ) }
240
+ buffer.last.chomp! unless buffer.empty?
211
241
  block = Block.new(parent, :literal, buffer)
212
242
 
213
243
  elsif this_line.match(REGEXP[:lit_par])
@@ -216,41 +246,80 @@ class Asciidoctor::Lexer
216
246
 
217
247
  # So we need to actually include this one in the grab_lines group
218
248
  reader.unshift this_line
219
- buffer = reader.grab_lines_until(:preserve_last_line => true) {|line| ! line.match( REGEXP[:lit_par] ) }
249
+ buffer = reader.grab_lines_until(:preserve_last_line => true) {|line|
250
+ (context == :dlist && line.match(REGEXP[:dlist])) || !line.match(REGEXP[:lit_par])
251
+ }
252
+
253
+ # trim off the indentation that put us in this literal paragraph
254
+ if !buffer.empty? && match = buffer.first.match(/^([[:blank:]]+)/)
255
+ offset = match[1].length
256
+ buffer = buffer.map {|l| l.slice(offset..-1)}
257
+ buffer.last.chomp!
258
+ end
220
259
 
221
260
  block = Block.new(parent, :literal, buffer)
222
261
 
223
- elsif this_line.match(REGEXP[:sidebar_blk])
224
- # example is surrounded by '****' (4 or more '*' chars) lines
225
- buffer = reader.grab_lines_until {|line| line.match( REGEXP[:sidebar_blk] ) }
226
- block = Block.new(parent, :sidebar, buffer)
262
+ ## these switches based on style need to come immediately before the else ##
263
+
264
+ elsif attributes[0] == 'source'
265
+ rekey_positional_attributes(attributes, ['style', 'language', 'linenums'])
266
+ reader.unshift(this_line)
267
+ buffer = reader.grab_lines_until(:break_on_blank_lines => true)
268
+ buffer.last.chomp! unless buffer.empty?
269
+ block = Block.new(parent, :listing, buffer)
270
+
271
+ elsif admonition_style = ADMONITION_STYLES.detect{|s| attributes[0] == s}
272
+ # an admonition preceded by [*TYPE*] and lasts until a blank line
273
+ reader.unshift(this_line)
274
+ buffer = reader.grab_lines_until(:break_on_blank_lines => true)
275
+ block = Block.new(parent, :admonition, buffer)
276
+ attributes['style'] = admonition_style
277
+ attributes['name'] = admonition_style.downcase
278
+ attributes['caption'] ||= admonition_style.capitalize
279
+
280
+ elsif quote_context = [:quote, :verse].detect{|s| attributes[0] == s.to_s}
281
+ # single-paragraph verse or quote is preceded by [verse] or [quote], respectively, and lasts until a blank line
282
+ rekey_positional_attributes(attributes, ['style', 'attribution', 'citetitle'])
283
+ reader.unshift(this_line)
284
+ buffer = reader.grab_lines_until(:break_on_blank_lines => true)
285
+ block = Block.new(parent, quote_context, buffer)
227
286
 
228
287
  else
229
288
  # paragraph is contiguous nonblank/noncontinuation lines
230
- while !this_line.nil? && !this_line.strip.empty?
231
- if this_line.match( REGEXP[:listing] ) || this_line.match( REGEXP[:oblock] )
232
- reader.unshift this_line
233
- break
234
- end
235
- buffer << this_line
236
- this_line = reader.get_line
289
+ reader.unshift this_line
290
+ buffer = reader.grab_lines_until(:break_on_blank_lines => true, :preserve_last_line => true) {|line|
291
+ (context == :dlist && line.match(REGEXP[:dlist])) ||
292
+ ([:ulist, :olist, :dlist].include?(context) && line.chomp == LIST_CONTINUATION) ||
293
+ line.match(REGEXP[:oblock])
294
+ }
295
+
296
+ if LIST_CONTEXTS.include?(context)
297
+ reader.skip_list_continuation
237
298
  end
238
299
 
239
- if buffer.any? && admonition = buffer.first.match(/^NOTE:\s*/)
300
+ if !buffer.empty? && admonition = buffer.first.match(Regexp.new('^(' + ADMONITION_STYLES.join('|') + '):\s+'))
240
301
  buffer[0] = admonition.post_match
241
- block = Block.new(parent, :note, buffer)
242
- elsif source_type
243
- block = Block.new(parent, :listing, buffer)
302
+ block = Block.new(parent, :admonition, buffer)
303
+ attributes['style'] = admonition[1]
304
+ attributes['name'] = admonition[1].downcase
305
+ attributes['caption'] ||= admonition[1].capitalize
244
306
  else
307
+ buffer.last.chomp! unless buffer.empty?
245
308
  Asciidoctor.debug "Proud parent #{parent} getting a new paragraph with buffer: #{buffer}"
246
309
  block = Block.new(parent, :paragraph, buffer)
247
310
  end
248
311
  end
249
312
  end
250
313
 
251
- block.anchor ||= anchor
252
- block.title ||= title
253
- block.caption ||= caption
314
+ # when looking for nested content, a series of
315
+ # line comments or a comment block could leave us
316
+ # without a block
317
+ if !block.nil?
318
+ block.anchor ||= (anchor || attributes['id'])
319
+ block.title ||= title
320
+ block.caption ||= caption
321
+ block.update_attributes(attributes)
322
+ end
254
323
 
255
324
  block
256
325
  end
@@ -352,7 +421,7 @@ class Asciidoctor::Lexer
352
421
 
353
422
  Asciidoctor.debug "*"*40
354
423
  Asciidoctor.debug "#{File.basename(__FILE__)}:#{__LINE__} -> #{__method__}: Returning this:"
355
- Asciidoctor.debug segment.inspect
424
+ #Asciidoctor.debug segment.inspect
356
425
  Asciidoctor.debug "*"*10
357
426
  Asciidoctor.debug "Leaving #{__method__}: Top of reader queue is:"
358
427
  Asciidoctor.debug reader.peek_line
@@ -383,11 +452,12 @@ class Asciidoctor::Lexer
383
452
 
384
453
  level = match[1].length
385
454
 
386
- list_item = ListItem.new
455
+ list_item = ListItem.new(block)
387
456
  list_item.level = level
388
457
  Asciidoctor.debug "#{__FILE__}:#{__LINE__}: Created ListItem #{list_item} with match[2]: #{match[2]} and level: #{list_item.level}"
389
458
 
390
- # Prevent bullet list text starting with . from being treated as a paragraph
459
+ # Restore first line of list item
460
+ # Also prevent bullet list text starting with . from being treated as a paragraph
391
461
  # title or some other unseemly thing in list_item_segment. I think. (NOTE)
392
462
  reader.unshift match[2].lstrip.sub(/^\./, '\.')
393
463
 
@@ -400,12 +470,7 @@ class Asciidoctor::Lexer
400
470
 
401
471
  Asciidoctor.debug "\n\nlist_item has #{list_item.blocks.count} blocks, and first is a #{list_item.blocks.first.class} with context #{list_item.blocks.first.context rescue 'n/a'}\n\n"
402
472
 
403
- first_block = list_item.blocks.first
404
- if first_block.is_a?(Block) &&
405
- (first_block.context == :paragraph || first_block.context == :literal)
406
- list_item.content = first_block.buffer.map{|l| l.strip}.join("\n")
407
- list_item.blocks.shift
408
- end
473
+ list_item.fold_first
409
474
 
410
475
  list_item
411
476
  end
@@ -423,15 +488,18 @@ class Asciidoctor::Lexer
423
488
 
424
489
  if first_item_level && first_item_level < this_item_level
425
490
  # If this next :uline level is down one from the
426
- # current Block's, put it in a Block of its own
427
- list_item = next_block(reader, block)
491
+ # current Block's, append it to content of the current list item
492
+ items.last.blocks << next_block(reader, block)
493
+ elsif first_item_level && first_item_level > this_item_level
494
+ break
428
495
  else
429
496
  list_item = build_ulist_item(reader, block, match)
430
497
  # Set the base item level for this Block
431
498
  first_item_level ||= list_item.level
432
499
  end
433
500
 
434
- items << list_item
501
+ items << list_item unless list_item.nil?
502
+ list_item = nil
435
503
 
436
504
  reader.skip_blank
437
505
  end
@@ -451,7 +519,7 @@ class Asciidoctor::Lexer
451
519
  while this_line && match = this_line.match(REGEXP[list_type])
452
520
  level = match[1].length
453
521
 
454
- list_item = ListItem.new
522
+ list_item = ListItem.new(block)
455
523
  list_item.level = level
456
524
  Asciidoctor.debug "Created ListItem #{list_item} with match[2]: #{match[2]} and level: #{list_item.level}"
457
525
 
@@ -462,12 +530,7 @@ class Asciidoctor::Lexer
462
530
  list_item.blocks << new_block unless new_block.nil?
463
531
  end
464
532
 
465
- first_block = list_item.blocks.first
466
- if first_block.is_a?(Block) &&
467
- (first_block.context == :paragraph || first_block.context == :literal)
468
- list_item.content = first_block.buffer.map{|l| l.strip}.join("\n")
469
- list_item.blocks.shift
470
- end
533
+ list_item.fold_first
471
534
 
472
535
  if items.any? && (level > items.last.level)
473
536
  Asciidoctor.debug "--> Putting this new level #{level} ListItem under my pops, #{items.last} (level: #{items.last.level})"
@@ -490,6 +553,32 @@ class Asciidoctor::Lexer
490
553
  block
491
554
  end
492
555
 
556
+ def self.collect_attributes(attrs, attributes, posattrs = [])
557
+ # TODO walk be properly rather than using split
558
+ attrs.split(/\s*,\s*/).each_with_index do |entry, i|
559
+ key, val = entry.split(/\s*=\s*/)
560
+ if !val.nil?
561
+ val.gsub!(/^(['"])(.*)\1$/, '\2') unless val.nil?
562
+ attributes[key] = val
563
+ else
564
+ attributes[i] = key
565
+ # positional attribute has a known key
566
+ if posattrs.size >= (i + 1)
567
+ attributes[posattrs[i]] = key
568
+ end
569
+ end
570
+ end
571
+ end
572
+
573
+ def self.rekey_positional_attributes(attributes, posattrs)
574
+ posattrs.each_with_index do |key, i|
575
+ val = attributes[i]
576
+ if !val.nil?
577
+ attributes[key] = val
578
+ end
579
+ end
580
+ end
581
+
493
582
  # Private: Get the Integer section level based on the characters
494
583
  # used in the ASCII line under the section name.
495
584
  #
@@ -517,7 +606,8 @@ class Asciidoctor::Lexer
517
606
  def self.is_two_line_section_heading?(line1, line2)
518
607
  !line1.nil? && !line2.nil? &&
519
608
  line1.match(REGEXP[:name]) && line2.match(REGEXP[:line]) &&
520
- (line1.size - line2.size).abs <= 1
609
+ # chomp so that a (non-visible) endline does not impact calculation
610
+ (line1.chomp.size - line2.chomp.size).abs <= 1
521
611
  end
522
612
 
523
613
  def self.is_section_heading?(line1, line2 = nil)
@@ -647,12 +737,6 @@ class Asciidoctor::Lexer
647
737
  section_lines << this_line
648
738
  section_lines << reader.get_line unless is_single_line_section_heading?(this_line)
649
739
  end
650
- elsif this_line.match(REGEXP[:listing])
651
- section_lines << this_line
652
- section_lines.concat reader.grab_lines_until {|line| line.match( REGEXP[:listing] ) }
653
- # Also grab the last line, if there is one
654
- this_line = reader.get_line
655
- section_lines << this_line unless this_line.nil?
656
740
  else
657
741
  section_lines << this_line
658
742
  end
@@ -669,6 +753,19 @@ class Asciidoctor::Lexer
669
753
  end
670
754
  end
671
755
 
756
+ # detect preamble and push it into a block
757
+ # QUESTION make this an operation on Section?
758
+ if section.level == 0
759
+ blocks = section.blocks.take_while {|b| !b.is_a? Section}
760
+ if !blocks.empty?
761
+ # QUESTION Should we propagate the buffer?
762
+ #preamble = Block.new(section, :preamble, blocks.reduce {|a, b| a.buffer + b.buffer})
763
+ preamble = Block.new(section, :preamble)
764
+ blocks.each { preamble << section.delete_at(0) }
765
+ section.insert(0, preamble)
766
+ end
767
+ end
768
+
672
769
  section
673
770
  end
674
771