asciidoctor 0.0.5 → 0.0.6

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