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.
- data/LICENSE +1 -1
- data/README.md +136 -55
- data/asciidoctor.gemspec +10 -4
- data/lib/asciidoctor.rb +33 -7
- data/lib/asciidoctor/block.rb +161 -24
- data/lib/asciidoctor/debug.rb +12 -1
- data/lib/asciidoctor/document.rb +31 -630
- data/lib/asciidoctor/lexer.rb +654 -0
- data/lib/asciidoctor/list_item.rb +30 -0
- data/lib/asciidoctor/reader.rb +236 -0
- data/lib/asciidoctor/render_templates.rb +3 -3
- data/lib/asciidoctor/renderer.rb +22 -2
- data/lib/asciidoctor/version.rb +1 -1
- data/test/attributes_test.rb +88 -0
- data/test/document_test.rb +2 -8
- data/test/lexer_test.rb +12 -0
- data/test/list_elements_test.rb +1 -1
- data/test/reader_test.rb +56 -0
- data/test/test_helper.rb +7 -2
- data/test/text_test.rb +26 -20
- metadata +113 -95
@@ -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
|