asciidoctor 0.0.1 → 0.0.2

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.

@@ -0,0 +1,654 @@
1
+ # Public: Methods to parse and build objects from Asciidoc lines
2
+ class Asciidoctor::Lexer
3
+
4
+ include Asciidoctor
5
+
6
+ # Public: Make sure the Lexer object doesn't get initialized.
7
+ def initialize
8
+ raise 'Au contraire, mon frere. No lexer instances will be running around.'
9
+ end
10
+
11
+ # Return the next block from the Reader.
12
+ #
13
+ # * Skip over blank lines to find the start of the next content block.
14
+ # * Use defined regular expressions to determine the type of content block.
15
+ # * Based on the type of content block, grab lines to the end of the block.
16
+ # * Return a new Asciidoctor::Block or Asciidoctor::Section instance with the
17
+ # content set to the grabbed lines.
18
+ def self.next_block(reader, parent = self)
19
+ # Skip ahead to the block content
20
+ reader.skip_blank
21
+
22
+ return nil unless reader.has_lines?
23
+
24
+ # NOTE: An anchor looks like this:
25
+ # [[foo]]
26
+ # with the inside [foo] (including brackets) as match[1]
27
+ if match = reader.peek_line.match(REGEXP[:anchor])
28
+ Asciidoctor.debug "Found an anchor in line:\n\t#{reader.peek_line}"
29
+ # NOTE: This expression conditionally strips off the brackets from
30
+ # [foo], though REGEXP[:anchor] won't actually match without
31
+ # match[1] being bracketed, so the condition isn't necessary.
32
+ anchor = match[1].match(/^\[(.*)\]/) ? $1 : match[1]
33
+ # NOTE: Set @references['foo'] = '[foo]'
34
+ parent.document.references[anchor] = match[1]
35
+ reader.get_line
36
+ else
37
+ anchor = nil
38
+ end
39
+
40
+ Asciidoctor.debug "/"*64
41
+ Asciidoctor.debug "#{File.basename(__FILE__)}:#{__LINE__} -> #{__method__} - First two lines are:"
42
+ Asciidoctor.debug reader.peek_line
43
+ tmp_line = reader.get_line
44
+ Asciidoctor.debug reader.peek_line
45
+ reader.unshift tmp_line
46
+ Asciidoctor.debug "/"*64
47
+
48
+ block = nil
49
+ title = nil
50
+ caption = nil
51
+ source_type = nil
52
+ buffer = []
53
+ while reader.has_lines? && block.nil?
54
+ buffer.clear
55
+ this_line = reader.get_line
56
+ next_line = reader.peek_line || ''
57
+
58
+ if this_line.match(REGEXP[:comment])
59
+ next
60
+
61
+ elsif match = this_line.match(REGEXP[:title])
62
+ title = match[1]
63
+ reader.skip_blank
64
+
65
+ elsif match = this_line.match(REGEXP[:listing_source])
66
+ source_type = match[1]
67
+ reader.skip_blank
68
+
69
+ elsif match = this_line.match(REGEXP[:caption])
70
+ caption = match[1]
71
+
72
+ elsif is_section_heading?(this_line, next_line)
73
+ # If we've come to a new section, then we've found the end of this
74
+ # current block. Likewise if we'd found an unassigned anchor, push
75
+ # it back as well, so it can go with this next heading.
76
+ # NOTE - I don't think this will assign the anchor properly. Anchors
77
+ # only match with double brackets - [[foo]], but what's stored in
78
+ # `anchor` at this point is only the `foo` part that was stripped out
79
+ # after matching. TODO: Need a way to test this.
80
+ reader.unshift(this_line)
81
+ reader.unshift(anchor) unless anchor.nil?
82
+ Asciidoctor.debug "#{__method__}: SENDING to next_section with lines[0] = #{reader.peek_line}"
83
+ block = next_section(reader, parent)
84
+
85
+ elsif this_line.match(REGEXP[:oblock])
86
+ # oblock is surrounded by '--' lines and has zero or more blocks inside
87
+ buffer = Reader.new(reader.grab_lines_until { |line| line.match(REGEXP[:oblock]) })
88
+
89
+ # Strip lines off end of block - not implemented yet
90
+ # while buffer.has_lines? && buffer.last.strip.empty?
91
+ # buffer.pop
92
+ # end
93
+
94
+ block = Block.new(parent, :oblock, [])
95
+ while buffer.has_lines?
96
+ block.blocks << next_block(buffer, block)
97
+ end
98
+
99
+ elsif list_type = [:olist, :colist].detect{|l| this_line.match( REGEXP[l] )}
100
+ items = []
101
+ Asciidoctor.debug "Creating block of type: #{list_type}"
102
+ block = Block.new(parent, list_type)
103
+ while !this_line.nil? && match = this_line.match(REGEXP[list_type])
104
+ item = ListItem.new
105
+
106
+ reader.unshift match[2].lstrip.sub(/^\./, '\.')
107
+ item_segment = Reader.new(list_item_segment(reader, :alt_ending => REGEXP[list_type]))
108
+ while item_segment.has_lines?
109
+ item.blocks << next_block(item_segment, block)
110
+ end
111
+
112
+ if item.blocks.any? &&
113
+ item.blocks.first.is_a?(Block) &&
114
+ (item.blocks.first.context == :paragraph || item.blocks.first.context == :literal)
115
+ item.content = item.blocks.shift.buffer.map{|l| l.strip}.join("\n")
116
+ end
117
+
118
+ items << item
119
+
120
+ reader.skip_blank
121
+
122
+ this_line = reader.get_line
123
+ end
124
+ reader.unshift(this_line) unless this_line.nil?
125
+
126
+ block.buffer = items
127
+
128
+ elsif match = this_line.match(REGEXP[:ulist])
129
+
130
+ reader.unshift(this_line)
131
+ block = build_ulist(reader, parent)
132
+
133
+ elsif match = this_line.match(REGEXP[:dlist])
134
+ pairs = []
135
+ block = Block.new(parent, :dlist)
136
+
137
+ this_dlist = Regexp.new(/^#{match[1]}(.*)#{match[3]}\s*$/)
138
+
139
+ while !this_line.nil? && match = this_line.match(this_dlist)
140
+ if anchor = match[1].match( /\[\[([^\]]+)\]\]/ )
141
+ dt = ListItem.new( $` + $' )
142
+ dt.anchor = anchor[1]
143
+ else
144
+ dt = ListItem.new( match[1] )
145
+ end
146
+ dd = ListItem.new
147
+ # workaround eg. git-config OPTIONS --get-colorbool
148
+ reader.get_line if reader.has_lines? && reader.peek_line.strip.empty?
149
+
150
+ dd_segment = Reader.new(list_item_segment(reader, :alt_ending => this_dlist))
151
+ while dd_segment.any?
152
+ dd.blocks << next_block(dd_segment, block)
153
+ end
154
+
155
+ if dd.blocks.any? &&
156
+ dd.blocks.first.is_a?(Block) &&
157
+ (dd.blocks.first.context == :paragraph || dd.blocks.first.context == :literal)
158
+ dd.content = dd.blocks.shift.buffer.map{|l| l.strip}.join("\n")
159
+ end
160
+
161
+ pairs << [dt, dd]
162
+
163
+ reader.skip_blank
164
+
165
+ this_line = reader.get_line
166
+ end
167
+ reader.unshift(this_line) unless this_line.nil?
168
+ block.buffer = pairs
169
+
170
+ elsif this_line.match(REGEXP[:verse])
171
+ # verse is preceded by [verse] and lasts until a blank line
172
+ buffer = reader.grab_lines_until(:break_on_blank_lines => true)
173
+ block = Block.new(parent, :verse, buffer)
174
+
175
+ elsif this_line.match(REGEXP[:note])
176
+ # note is an admonition preceded by [NOTE] and lasts until a blank line
177
+ buffer = reader.grab_lines_until(:break_on_blank_lines => true)
178
+ block = Block.new(parent, :note, buffer)
179
+
180
+ elsif block_type = [:listing, :example].detect{|t| this_line.match( REGEXP[t] )}
181
+ buffer = reader.grab_lines_until {|line| line.match( REGEXP[block_type] )}
182
+ block = Block.new(parent, block_type, buffer)
183
+
184
+ elsif this_line.match( REGEXP[:quote] )
185
+ block = Block.new(parent, :quote)
186
+ buffer = Reader.new(reader.grab_lines_until {|line| line.match( REGEXP[:quote] ) })
187
+
188
+ while buffer.any?
189
+ block.blocks << next_block(reader, block)
190
+ end
191
+
192
+ elsif this_line.match(REGEXP[:lit_blk])
193
+ # example is surrounded by '....' (4 or more '.' chars) lines
194
+ buffer = reader.grab_lines_until {|line| line.match( REGEXP[:lit_blk] ) }
195
+ block = Block.new(parent, :literal, buffer)
196
+
197
+ elsif this_line.match(REGEXP[:lit_par])
198
+ # literal paragraph is contiguous lines starting with
199
+ # one or more space or tab characters
200
+
201
+ # So we need to actually include this one in the grab_lines group
202
+ reader.unshift this_line
203
+ buffer = reader.grab_lines_until(:preserve_last_line => true) {|line| ! line.match( REGEXP[:lit_par] ) }
204
+
205
+ block = Block.new(parent, :literal, buffer)
206
+
207
+ elsif this_line.match(REGEXP[:sidebar_blk])
208
+ # example is surrounded by '****' (4 or more '*' chars) lines
209
+ buffer = reader.grab_lines_until {|line| line.match( REGEXP[:sidebar_blk] ) }
210
+ block = Block.new(parent, :sidebar, buffer)
211
+
212
+ else
213
+ # paragraph is contiguous nonblank/noncontinuation lines
214
+ while !this_line.nil? && !this_line.strip.empty?
215
+ if this_line.match( REGEXP[:listing] ) || this_line.match( REGEXP[:oblock] )
216
+ reader.unshift this_line
217
+ break
218
+ end
219
+ buffer << this_line
220
+ this_line = reader.get_line
221
+ end
222
+
223
+ if buffer.any? && admonition = buffer.first.match(/^NOTE:\s*/)
224
+ buffer[0] = admonition.post_match
225
+ block = Block.new(parent, :note, buffer)
226
+ elsif source_type
227
+ block = Block.new(parent, :listing, buffer)
228
+ else
229
+ Asciidoctor.debug "Proud parent #{parent} getting a new paragraph with buffer: #{buffer}"
230
+ block = Block.new(parent, :paragraph, buffer)
231
+ end
232
+ end
233
+ end
234
+
235
+ block.anchor ||= anchor
236
+ block.title ||= title
237
+ block.caption ||= caption
238
+
239
+ block
240
+ end
241
+
242
+ # Private: Return the Array of lines constituting the next list item
243
+ # segment, removing them from the 'lines' Array passed in.
244
+ #
245
+ # reader - the Reader instance from which to get input.
246
+ # options - an optional Hash of processing options:
247
+ # * :alt_ending may be used to specify a regular expression match
248
+ # other than a blank line to signify the end of the segment.
249
+ # * :list_types may be used to specify list item patterns to
250
+ # include. May be either a single Symbol or an Array of Symbols.
251
+ # * :list_level may be used to specify a mimimum list item level
252
+ # to include. If this is specified, then break if we find a list
253
+ # item of a lower level.
254
+ #
255
+ # Returns the Array of lines forming the next segment.
256
+ #
257
+ # Examples
258
+ #
259
+ # reader = Asciidoctor::Reader.new(
260
+ # ["First paragraph\n", "+\n", "Second paragraph\n", "--\n",
261
+ # "Open block\n", "\n", "Can have blank lines\n", "--\n", "\n",
262
+ # "In a different segment\n"])
263
+ #
264
+ # list_item_segment(reader)
265
+ # => ["First paragraph\n", "+\n", "Second paragraph\n", "--\n",
266
+ # "Open block\n", "\n", "Can have blank lines\n", "--\n"]
267
+ #
268
+ # reader.peek_line
269
+ # => "In a different segment\n"
270
+ def self.list_item_segment(reader, options={})
271
+ alternate_ending = options[:alt_ending]
272
+ list_types = Array(options[:list_types]) || [:ulist, :olist, :colist, :dlist]
273
+ list_level = options[:list_level].to_i
274
+
275
+ # We know we want to include :lit_par types, even if we have specified,
276
+ # say, only :ulist type list entries.
277
+ list_types << :lit_par unless list_types.include? :lit_par
278
+ segment = []
279
+
280
+ reader.skip_blank
281
+
282
+ # Grab lines until the first blank line not inside an open block
283
+ # or listing
284
+ in_oblock = false
285
+ in_listing = false
286
+ while reader.has_lines?
287
+ this_line = reader.get_line
288
+ Asciidoctor.debug "-----> Processing: #{this_line}"
289
+ in_oblock = !in_oblock if this_line.match(REGEXP[:oblock])
290
+ in_listing = !in_listing if this_line.match(REGEXP[:listing])
291
+ if !in_oblock && !in_listing
292
+ if this_line.strip.empty?
293
+ # TODO - FIX THIS BEFORE ANY MORE KITTENS DIE AUGGGHHH!!!
294
+ next_nonblank = reader.instance_variable_get(:@lines).detect{|l| !l.strip.empty?}
295
+
296
+ # If there are blank lines ahead, but there's at least one
297
+ # more non-blank line that doesn't trigger an alternate_ending
298
+ # for the block of lines, then vacuum up all the blank lines
299
+ # into this segment and continue with the next non-blank line.
300
+ if next_nonblank &&
301
+ ( alternate_ending.nil? ||
302
+ !next_nonblank.match(alternate_ending)
303
+ ) && list_types.find { |list_type| next_nonblank.match(REGEXP[list_type]) }
304
+
305
+ while reader.has_lines? and reader.peek_line.strip.empty?
306
+ segment << this_line
307
+ this_line = reader.get_line
308
+ end
309
+ else
310
+ break
311
+ end
312
+
313
+ # Have we come to a line matching an alternate_ending regexp?
314
+ elsif alternate_ending && this_line.match(alternate_ending)
315
+ reader.unshift this_line
316
+ break
317
+
318
+ # Do we have a minimum list_level, and have come to a list item
319
+ # line with a lower level?
320
+ elsif list_level &&
321
+ list_types.find { |list_type| this_line.match(REGEXP[list_type]) } &&
322
+ ($1.length < list_level)
323
+ reader.unshift this_line
324
+ break
325
+ end
326
+
327
+ # From the Asciidoc user's guide:
328
+ # Another list or a literal paragraph immediately following
329
+ # a list item will be implicitly included in the list item
330
+
331
+ # Thus, the list_level stuff may be wrong here.
332
+ end
333
+
334
+ segment << this_line
335
+ end
336
+
337
+ Asciidoctor.debug "*"*40
338
+ Asciidoctor.debug "#{File.basename(__FILE__)}:#{__LINE__} -> #{__method__}: Returning this:"
339
+ Asciidoctor.debug segment.inspect
340
+ Asciidoctor.debug "*"*10
341
+ Asciidoctor.debug "Leaving #{__method__}: Top of reader queue is:"
342
+ Asciidoctor.debug reader.peek_line
343
+ Asciidoctor.debug "*"*40
344
+ segment
345
+ end
346
+
347
+ # Private: Get the Integer ulist level based on the characters
348
+ # in front of the list item text.
349
+ #
350
+ # line - the String line containing the list item
351
+ def self.ulist_level(line)
352
+ if m = line.strip.match(/^(- | \*{1,5})\s+/x)
353
+ return m[1].length
354
+ end
355
+ end
356
+
357
+ def self.build_ulist_item(reader, block, match = nil)
358
+ list_type = :ulist
359
+ this_line = reader.get_line
360
+ return nil unless this_line
361
+
362
+ match ||= this_line.match(REGEXP[list_type])
363
+ if match.nil?
364
+ reader.unshift(this_line)
365
+ return nil
366
+ end
367
+
368
+ level = match[1].length
369
+
370
+ list_item = ListItem.new
371
+ list_item.level = level
372
+ Asciidoctor.debug "#{__FILE__}:#{__LINE__}: Created ListItem #{list_item} with match[2]: #{match[2]} and level: #{list_item.level}"
373
+
374
+ # Prevent bullet list text starting with . from being treated as a paragraph
375
+ # title or some other unseemly thing in list_item_segment. I think. (NOTE)
376
+ reader.unshift match[2].lstrip.sub(/^\./, '\.')
377
+
378
+ item_segment = Reader.new(list_item_segment(reader, :alt_ending => REGEXP[list_type]))
379
+ # item_segment = list_item_segment(reader)
380
+ while item_segment.has_lines?
381
+ list_item.blocks << next_block(item_segment, block)
382
+ end
383
+
384
+ 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"
385
+
386
+ first_block = list_item.blocks.first
387
+ if first_block.is_a?(Block) &&
388
+ (first_block.context == :paragraph || first_block.context == :literal)
389
+ list_item.content = first_block.buffer.map{|l| l.strip}.join("\n")
390
+ list_item.blocks.shift
391
+ end
392
+
393
+ list_item
394
+ end
395
+
396
+ def self.build_ulist(reader, parent = nil)
397
+ items = []
398
+ list_type = :ulist
399
+ block = Block.new(parent, list_type)
400
+ Asciidoctor.debug "Created :ulist block: #{block}"
401
+ first_item_level = nil
402
+
403
+ while reader.has_lines? && match = reader.peek_line.match(REGEXP[list_type])
404
+
405
+ this_item_level = match[1].length
406
+
407
+ if first_item_level && first_item_level < this_item_level
408
+ # If this next :uline level is down one from the
409
+ # current Block's, put it in a Block of its own
410
+ list_item = next_block(reader, block)
411
+ else
412
+ list_item = build_ulist_item(reader, block, match)
413
+ # Set the base item level for this Block
414
+ first_item_level ||= list_item.level
415
+ end
416
+
417
+ items << list_item
418
+
419
+ reader.skip_blank
420
+ end
421
+
422
+ block.buffer = items
423
+ block
424
+ end
425
+
426
+ def self.build_ulist_ref(lines, parent = nil)
427
+ items = []
428
+ list_type = :ulist
429
+ block = Block.new(parent, list_type)
430
+ Asciidoctor.debug "Created :ulist block: #{block}"
431
+ last_item_level = nil
432
+ this_line = lines.shift
433
+
434
+ while this_line && match = this_line.match(REGEXP[list_type])
435
+ level = match[1].length
436
+
437
+ list_item = ListItem.new
438
+ list_item.level = level
439
+ Asciidoctor.debug "Created ListItem #{list_item} with match[2]: #{match[2]} and level: #{list_item.level}"
440
+
441
+ lines.unshift match[2].lstrip.sub(/^\./, '\.')
442
+ item_segment = list_item_segment(lines, :alt_ending => REGEXP[list_type], :list_level => level)
443
+ while item_segment.any?
444
+ list_item.blocks << next_block(item_segment, block)
445
+ end
446
+
447
+ first_block = list_item.blocks.first
448
+ if first_block.is_a?(Block) &&
449
+ (first_block.context == :paragraph || first_block.context == :literal)
450
+ list_item.content = first_block.buffer.map{|l| l.strip}.join("\n")
451
+ list_item.blocks.shift
452
+ end
453
+
454
+ if items.any? && (level > items.last.level)
455
+ Asciidoctor.debug "--> Putting this new level #{level} ListItem under my pops, #{items.last} (level: #{items.last.level})"
456
+ items.last.blocks << list_item
457
+ else
458
+ Asciidoctor.debug "Stacking new list item in parent block's blocks"
459
+ items << list_item
460
+ end
461
+
462
+ last_item_level = list_item.level
463
+
464
+ # TODO: This has to come from a Reader object
465
+ skip_blank(lines)
466
+
467
+ this_line = lines.shift
468
+ end
469
+ lines.unshift(this_line) unless this_line.nil?
470
+
471
+ block.buffer = items
472
+ block
473
+ end
474
+
475
+ # Private: Get the Integer section level based on the characters
476
+ # used in the ASCII line under the section name.
477
+ #
478
+ # line - the String line from under the section name.
479
+ def self.section_level(line)
480
+ char = line.strip.chars.to_a.uniq
481
+ case char
482
+ when ['=']; 0
483
+ when ['-']; 1
484
+ when ['~']; 2
485
+ when ['^']; 3
486
+ when ['+']; 4
487
+ end
488
+ end
489
+
490
+ # == is level 0, === is level 1, etc.
491
+ def self.single_line_section_level(line)
492
+ [line.length - 1, 0].max
493
+ end
494
+
495
+ def self.is_single_line_section_heading?(line)
496
+ !line.nil? && line.match(REGEXP[:level_title])
497
+ end
498
+
499
+ def self.is_two_line_section_heading?(line1, line2)
500
+ !line1.nil? && !line2.nil? &&
501
+ line1.match(REGEXP[:name]) && line2.match(REGEXP[:line]) &&
502
+ (line1.size - line2.size).abs <= 1
503
+ end
504
+
505
+ def self.is_section_heading?(line1, line2 = nil)
506
+ is_single_line_section_heading?(line1) ||
507
+ is_two_line_section_heading?(line1, line2)
508
+ end
509
+
510
+ # Private: Extracts the name, level and (optional) embedded anchor from a
511
+ # 1- or 2-line section heading.
512
+ #
513
+ # Returns an array of a String, Integer, and String or nil.
514
+ #
515
+ # Examples
516
+ #
517
+ # line1
518
+ # => "Foo\n"
519
+ # line2
520
+ # => "~~~\n"
521
+ #
522
+ # name, level, anchor = extract_section_heading(line1, line2)
523
+ #
524
+ # name
525
+ # => "Foo"
526
+ # level
527
+ # => 2
528
+ # anchor
529
+ # => nil
530
+ #
531
+ # line1
532
+ # => "==== Foo\n"
533
+ #
534
+ # name, level, anchor = extract_section_heading(line1)
535
+ #
536
+ # name
537
+ # => "Foo"
538
+ # level
539
+ # => 3
540
+ # anchor
541
+ # => nil
542
+ #
543
+ def self.extract_section_heading(line1, line2 = nil)
544
+ Asciidoctor.debug "#{__method__} -> line1: #{line1.chomp rescue 'nil'}, line2: #{line2.chomp rescue 'nil'}"
545
+ sect_name = sect_anchor = nil
546
+ sect_level = 0
547
+
548
+ if is_single_line_section_heading?(line1)
549
+ header_match = line1.match(REGEXP[:level_title])
550
+ sect_name = header_match[2]
551
+ sect_level = single_line_section_level(header_match[1])
552
+ elsif is_two_line_section_heading?(line1, line2)
553
+ header_match = line1.match(REGEXP[:name])
554
+ if anchor_match = header_match[1].match(REGEXP[:anchor_embedded])
555
+ sect_name = anchor_match[1]
556
+ sect_anchor = anchor_match[2]
557
+ else
558
+ sect_name = header_match[1]
559
+ end
560
+ sect_level = section_level(line2)
561
+ end
562
+ Asciidoctor.debug "#{__method__} -> Returning #{sect_name}, #{sect_level} (anchor: '#{sect_anchor || '<none>'}')"
563
+ return [sect_name, sect_level, sect_anchor]
564
+ end
565
+
566
+ # Private: Return the next section from the document.
567
+ #
568
+ # Examples
569
+ #
570
+ # source
571
+ # => "GREETINGS\n---------\nThis is my doc.\n\nSALUTATIONS\n-----------\nIt is awesome."
572
+ #
573
+ # doc = Asciidoctor::Document.new(source)
574
+ #
575
+ # doc.next_section
576
+ # ["GREETINGS", [:paragraph, "This is my doc."]]
577
+ #
578
+ # doc.next_section
579
+ # ["SALUTATIONS", [:paragraph, "It is awesome."]]
580
+ def self.next_section(reader, parent = self)
581
+ section = Section.new(parent)
582
+
583
+ Asciidoctor.debug "%"*64
584
+ Asciidoctor.debug "#{File.basename(__FILE__)}:#{__LINE__} -> #{__method__} - First two lines are:"
585
+ Asciidoctor.debug reader.peek_line
586
+ tmp_line = reader.get_line
587
+ Asciidoctor.debug reader.peek_line
588
+ reader.unshift tmp_line
589
+ Asciidoctor.debug "%"*64
590
+
591
+ # Skip ahead to the next section definition
592
+ while reader.has_lines? && section.name.nil?
593
+ this_line = reader.get_line
594
+ next_line = reader.peek_line || ''
595
+ if match = this_line.match(REGEXP[:anchor])
596
+ section.anchor = match[1]
597
+ elsif is_section_heading?(this_line, next_line)
598
+ section.name, section.level, section.anchor = extract_section_heading(this_line, next_line)
599
+ reader.get_line unless is_single_line_section_heading?(this_line)
600
+ end
601
+ end
602
+
603
+ if !section.anchor.nil?
604
+ anchor_id = section.anchor.match(/^\[(.*)\]/) ? $1 : section.anchor
605
+ @references[anchor_id] = section.anchor
606
+ section.anchor = anchor_id
607
+ end
608
+
609
+ # Grab all the lines that belong to this section
610
+ section_lines = []
611
+ while reader.has_lines?
612
+ this_line = reader.get_line
613
+ next_line = reader.peek_line
614
+
615
+ if is_section_heading?(this_line, next_line)
616
+ _, this_level, _ = extract_section_heading(this_line, next_line)
617
+
618
+ if this_level <= section.level
619
+ # A section can't contain a section level lower than itself,
620
+ # so this signifies the end of the section.
621
+ reader.unshift this_line
622
+ if section_lines.any? && section_lines.last.match(REGEXP[:anchor])
623
+ # Put back the anchor that came before this new-section line
624
+ # on which we're bailing.
625
+ reader.unshift section_lines.pop
626
+ end
627
+ break
628
+ else
629
+ section_lines << this_line
630
+ section_lines << reader.get_line unless is_single_line_section_heading?(this_line)
631
+ end
632
+ elsif this_line.match(REGEXP[:listing])
633
+ section_lines << this_line
634
+ section_lines.concat reader.grab_lines_until {|line| line.match( REGEXP[:listing] ) }
635
+ # Also grab the last line, if there is one
636
+ this_line = lines.shift
637
+ section_lines << this_line unless this_line.nil?
638
+ else
639
+ section_lines << this_line
640
+ end
641
+ end
642
+
643
+ section_reader = Reader.new(section_lines)
644
+ # Now parse section_lines into Blocks belonging to the current Section
645
+ while section_reader.has_lines?
646
+ section_reader.skip_blank
647
+
648
+ section << next_block(section_reader, section) if section_reader.has_lines?
649
+ end
650
+
651
+ section
652
+ end
653
+
654
+ end