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
data/lib/asciidoctor/debug.rb
CHANGED
@@ -1,6 +1,17 @@
|
|
1
1
|
module Asciidoctor
|
2
2
|
def self.debug(*args)
|
3
|
-
puts *args
|
3
|
+
puts *args if self.show_debug_output?
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.show_debug_output?
|
7
|
+
ENV['DEBUG'] == 'true' && ENV['SUPPRESS_DEBUG'] != 'true'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.puts_indented(level, *args)
|
11
|
+
thing = " "*level*2
|
12
|
+
args.each do |arg|
|
13
|
+
self.debug "#{thing}#{arg}"
|
14
|
+
end
|
4
15
|
end
|
5
16
|
end
|
6
17
|
|
data/lib/asciidoctor/document.rb
CHANGED
@@ -4,13 +4,6 @@ class Asciidoctor::Document
|
|
4
4
|
|
5
5
|
include Asciidoctor
|
6
6
|
|
7
|
-
# Public: Get the String document source.
|
8
|
-
attr_reader :source
|
9
|
-
|
10
|
-
# Public: Get the Asciidoctor::Renderer instance currently being used
|
11
|
-
# to render this Document.
|
12
|
-
attr_reader :renderer
|
13
|
-
|
14
7
|
# Public: Get the Hash of defines
|
15
8
|
attr_reader :defines
|
16
9
|
|
@@ -25,111 +18,32 @@ class Asciidoctor::Document
|
|
25
18
|
|
26
19
|
# Public: Initialize an Asciidoc object.
|
27
20
|
#
|
28
|
-
# data - The
|
21
|
+
# data - The Array of Strings holding the Asciidoc source document.
|
29
22
|
# block - A block that can be used to retrieve external Asciidoc
|
30
23
|
# data to include in this document.
|
31
24
|
#
|
32
25
|
# Examples
|
33
26
|
#
|
34
|
-
#
|
35
|
-
# data = File.read(filename)
|
27
|
+
# data = File.readlines(filename)
|
36
28
|
# doc = Asciidoctor::Document.new(data)
|
37
|
-
def initialize(data, &block)
|
38
|
-
raw_source = []
|
29
|
+
def initialize(data, options = {}, &block)
|
39
30
|
@elements = []
|
40
|
-
@
|
41
|
-
@references = {}
|
31
|
+
@options = options
|
42
32
|
|
43
|
-
|
44
|
-
data.each do |line|
|
45
|
-
if inc = line.match(include_regexp)
|
46
|
-
raw_source.concat(File.readlines(inc[1]))
|
47
|
-
else
|
48
|
-
raw_source << line
|
49
|
-
end
|
50
|
-
end
|
33
|
+
reader = Reader.new(data, &block)
|
51
34
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
conditional_regexp = /^\s*\{([^\?]+)\?\s*([^\}]+)\s*\}/
|
56
|
-
|
57
|
-
skip_to = nil
|
58
|
-
continuing_value = nil
|
59
|
-
continuing_key = nil
|
60
|
-
@lines = []
|
61
|
-
raw_source.each do |line|
|
62
|
-
if skip_to
|
63
|
-
skip_to = nil if line.match(skip_to)
|
64
|
-
elsif continuing_value
|
65
|
-
close_continue = false
|
66
|
-
# Lines that start with whitespace and end with a '+' are
|
67
|
-
# a continuation, so gobble them up into `value`
|
68
|
-
if match = line.match(/\s+(.+)\s+\+\s*$/)
|
69
|
-
continuing_value += match[1]
|
70
|
-
elsif match = line.match(/\s+(.+)/)
|
71
|
-
# If this continued line doesn't end with a +, then this
|
72
|
-
# is the end of the continuation, no matter what the next
|
73
|
-
# line does.
|
74
|
-
continuing_value += match[1]
|
75
|
-
close_continue = true
|
76
|
-
else
|
77
|
-
# If this line doesn't start with whitespace, then it's
|
78
|
-
# not a valid continuation line, so push it back for processing
|
79
|
-
close_continue = true
|
80
|
-
raw_source.unshift(line)
|
81
|
-
end
|
82
|
-
if close_continue
|
83
|
-
@defines[continuing_key] = continuing_value
|
84
|
-
continuing_key = nil
|
85
|
-
continuing_value = nil
|
86
|
-
end
|
87
|
-
elsif match = line.match(ifdef_regexp)
|
88
|
-
attr = match[2]
|
89
|
-
skip = case match[1]
|
90
|
-
when 'ifdef'; !@defines.has_key?(attr)
|
91
|
-
when 'ifndef'; @defines.has_key?(attr)
|
92
|
-
end
|
93
|
-
skip_to = /^endif::#{attr}\[\]\s*\n/ if skip
|
94
|
-
elsif match = line.match(defattr_regexp)
|
95
|
-
key = match[1]
|
96
|
-
value = match[2]
|
97
|
-
if match = value.match(/(.+)\s+\+\s*$/)
|
98
|
-
# continuation line, grab lines until we run out of continuation lines
|
99
|
-
continuing_key = key
|
100
|
-
continuing_value = match[1] # strip off the spaces and +
|
101
|
-
Asciidoctor.debug "continuing key: #{continuing_key} with partial value: '#{continuing_value}'"
|
102
|
-
else
|
103
|
-
@defines[key] = value
|
104
|
-
Asciidoctor.debug "Defines[#{key}] is '#{value}'"
|
105
|
-
end
|
106
|
-
elsif !line.match(endif_regexp)
|
107
|
-
while match = line.match(conditional_regexp)
|
108
|
-
value = @defines.has_key?(match[1]) ? match[2] : ''
|
109
|
-
line.sub!(conditional_regexp, value)
|
110
|
-
end
|
111
|
-
@lines << line unless line.match(REGEXP[:comment])
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
# Process bibliography references, so they're available when text
|
116
|
-
# before the reference is being rendered.
|
117
|
-
@lines.each do |line|
|
118
|
-
if biblio = line.match(REGEXP[:biblio])
|
119
|
-
references[biblio[1]] = "[#{biblio[1]}]"
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
@source = @lines.join
|
35
|
+
# pseudo-delegation :)
|
36
|
+
@defines = reader.defines
|
37
|
+
@references = reader.references
|
124
38
|
|
125
39
|
# Now parse @lines into elements
|
126
|
-
while
|
127
|
-
skip_blank
|
40
|
+
while reader.has_lines?
|
41
|
+
reader.skip_blank
|
128
42
|
|
129
|
-
@elements << next_block(
|
43
|
+
@elements << Lexer.next_block(reader, self) if reader.has_lines?
|
130
44
|
end
|
131
45
|
|
132
|
-
Asciidoctor.debug "Found #{@elements.size} elements:"
|
46
|
+
Asciidoctor.debug "Found #{@elements.size} elements in this document:"
|
133
47
|
@elements.each do |el|
|
134
48
|
Asciidoctor.debug el
|
135
49
|
end
|
@@ -161,24 +75,35 @@ class Asciidoctor::Document
|
|
161
75
|
|
162
76
|
def splain
|
163
77
|
if @header
|
164
|
-
|
78
|
+
Asciidoctor.debug "Header is #{@header}"
|
165
79
|
else
|
166
|
-
|
80
|
+
Asciidoctor.debug "No header"
|
167
81
|
end
|
168
82
|
|
169
|
-
|
83
|
+
Asciidoctor.debug "I have #{@elements.count} elements"
|
170
84
|
@elements.each_with_index do |block, i|
|
171
|
-
|
172
|
-
|
173
|
-
|
85
|
+
Asciidoctor.debug "v" * 60
|
86
|
+
Asciidoctor.debug "Block ##{i} is a #{block.class}"
|
87
|
+
Asciidoctor.debug "Name is #{block.name rescue 'n/a'}"
|
88
|
+
block.splain(0) if block.respond_to? :splain
|
89
|
+
Asciidoctor.debug "^" * 60
|
90
|
+
end
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def renderer
|
95
|
+
return @renderer if @renderer
|
96
|
+
render_options = {}
|
97
|
+
if @options[:template_dir]
|
98
|
+
render_options[:template_dir] = @options[:template_dir]
|
174
99
|
end
|
100
|
+
@renderer = Renderer.new(render_options)
|
175
101
|
end
|
176
102
|
|
177
103
|
# Public: Render the Asciidoc document using erb templates
|
178
104
|
#
|
179
105
|
def render
|
180
|
-
|
181
|
-
html = self.renderer.render('document', self, :header => @header, :preamble => @preamble)
|
106
|
+
html = renderer.render('document', self, :header => @header, :preamble => @preamble)
|
182
107
|
end
|
183
108
|
|
184
109
|
def content
|
@@ -190,528 +115,4 @@ class Asciidoctor::Document
|
|
190
115
|
html_pieces.join("\n")
|
191
116
|
end
|
192
117
|
|
193
|
-
private
|
194
|
-
|
195
|
-
# Private: Strip off leading blank lines in the Array of lines.
|
196
|
-
#
|
197
|
-
# lines - the Array of String lines.
|
198
|
-
#
|
199
|
-
# Returns nil.
|
200
|
-
#
|
201
|
-
# Examples
|
202
|
-
#
|
203
|
-
# content
|
204
|
-
# => ["\n", "\t\n", "Foo\n", "Bar\n", "\n"]
|
205
|
-
#
|
206
|
-
# skip_blank(content)
|
207
|
-
# => nil
|
208
|
-
#
|
209
|
-
# lines
|
210
|
-
# => ["Foo\n", "Bar\n"]
|
211
|
-
def skip_blank(lines)
|
212
|
-
while lines.any? && lines.first.strip.empty?
|
213
|
-
lines.shift
|
214
|
-
end
|
215
|
-
|
216
|
-
nil
|
217
|
-
end
|
218
|
-
|
219
|
-
# Private: Strip off and return the list item segment (one or more contiguous blocks) from the Array of lines.
|
220
|
-
#
|
221
|
-
# lines - the Array of String lines.
|
222
|
-
# options - an optional Hash of processing options:
|
223
|
-
# * :alt_ending may be used to specify a regular expression match other than
|
224
|
-
# a blank line to signify the end of the segment.
|
225
|
-
# Returns the Array of lines from the next segment.
|
226
|
-
#
|
227
|
-
# Examples
|
228
|
-
#
|
229
|
-
# content
|
230
|
-
# => ["First paragraph\n", "+\n", "Second paragraph\n", "--\n", "Open block\n", "\n", "Can have blank lines\n", "--\n", "\n", "In a different segment\n"]
|
231
|
-
#
|
232
|
-
# list_item_segment(content)
|
233
|
-
# => ["First paragraph\n", "+\n", "Second paragraph\n", "--\n", "Open block\n", "\n", "Can have blank lines\n", "--\n"]
|
234
|
-
#
|
235
|
-
# content
|
236
|
-
# => ["In a different segment\n"]
|
237
|
-
def list_item_segment(lines, options={})
|
238
|
-
alternate_ending = options[:alt_ending]
|
239
|
-
segment = []
|
240
|
-
|
241
|
-
skip_blank(lines)
|
242
|
-
|
243
|
-
# Grab lines until the first blank line not inside an open block
|
244
|
-
# or listing
|
245
|
-
in_oblock = false
|
246
|
-
in_listing = false
|
247
|
-
while lines.any?
|
248
|
-
this_line = lines.shift
|
249
|
-
in_oblock = !in_oblock if this_line.match(REGEXP[:oblock])
|
250
|
-
in_listing = !in_listing if this_line.match(REGEXP[:listing])
|
251
|
-
if !in_oblock && !in_listing
|
252
|
-
if this_line.strip.empty?
|
253
|
-
# From the Asciidoc user's guide:
|
254
|
-
# Another list or a literal paragraph immediately following
|
255
|
-
# a list item will be implicitly included in the list item
|
256
|
-
next_nonblank = lines.detect{|l| !l.strip.empty?}
|
257
|
-
if !next_nonblank.nil? &&
|
258
|
-
( alternate_ending.nil? ||
|
259
|
-
!next_nonblank.match(alternate_ending)
|
260
|
-
) && [:ulist, :olist, :colist, :dlist, :lit_par, :continue].
|
261
|
-
find { |pattern| next_nonblank.match(REGEXP[pattern]) }
|
262
|
-
|
263
|
-
# Pull blank lines into the segment, so the next thing up for processing
|
264
|
-
# will be the next nonblank line.
|
265
|
-
while lines.first.strip.empty?
|
266
|
-
segment << this_line
|
267
|
-
this_line = lines.shift
|
268
|
-
end
|
269
|
-
else
|
270
|
-
break
|
271
|
-
end
|
272
|
-
elsif !alternate_ending.nil? && this_line.match(alternate_ending)
|
273
|
-
lines.unshift this_line
|
274
|
-
break
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
segment << this_line
|
279
|
-
end
|
280
|
-
|
281
|
-
segment
|
282
|
-
end
|
283
|
-
|
284
|
-
# Private: Return all the lines from `lines` until we run out of lines,
|
285
|
-
# find a blank line with :break_on_blank_lines => true, or find a line
|
286
|
-
# for which the given block evals to true.
|
287
|
-
#
|
288
|
-
# lines - the Array of String lines.
|
289
|
-
# options - an optional Hash of processing options:
|
290
|
-
# * :break_on_blank_lines may be used to specify to break on blank lines
|
291
|
-
# * :preserve_last_line may be used to specify that the String
|
292
|
-
# causing the method to stop processing lines should be
|
293
|
-
# pushed back onto the `lines` Array.
|
294
|
-
#
|
295
|
-
# Returns the Array of lines from the next segment.
|
296
|
-
#
|
297
|
-
# Examples
|
298
|
-
#
|
299
|
-
# content
|
300
|
-
# => ["First paragraph\n", "Second paragraph\n", "Open block\n", "\n", "Can have blank lines\n", "--\n", "\n", "In a different segment\n"]
|
301
|
-
#
|
302
|
-
# grab_lines_until(content)
|
303
|
-
# => ["First paragraph\n", "Second paragraph\n", "Open block\n"]
|
304
|
-
#
|
305
|
-
# content
|
306
|
-
# => ["In a different segment\n"]
|
307
|
-
def grab_lines_until(lines, options = {}, &block)
|
308
|
-
buffer = []
|
309
|
-
|
310
|
-
while (this_line = lines.shift)
|
311
|
-
Asciidoctor.debug "Processing line: '#{this_line}'"
|
312
|
-
finis = this_line.nil?
|
313
|
-
finis ||= true if options[:break_on_blank_lines] && this_line.strip.empty?
|
314
|
-
finis ||= true if block && value = yield(this_line)
|
315
|
-
if finis
|
316
|
-
lines.unshift(this_line) if options[:preserve_last_line] and ! this_line.nil?
|
317
|
-
break
|
318
|
-
end
|
319
|
-
|
320
|
-
buffer << this_line
|
321
|
-
end
|
322
|
-
buffer
|
323
|
-
end
|
324
|
-
|
325
|
-
# Private: Return the next block from the section.
|
326
|
-
#
|
327
|
-
# * Skip over blank lines to find the start of the next content block.
|
328
|
-
# * Use defined regular expressions to determine the type of content block.
|
329
|
-
# * Based on the type of content block, grab lines to the end of the block.
|
330
|
-
# * Return a new Asciidoctor::Block or Asciidoctor::Section instance with the
|
331
|
-
# content set to the grabbed lines.
|
332
|
-
def next_block(lines, parent = self)
|
333
|
-
# Skip ahead to the block content
|
334
|
-
skip_blank(lines)
|
335
|
-
|
336
|
-
return nil if lines.empty?
|
337
|
-
|
338
|
-
# NOTE: An anchor looks like this:
|
339
|
-
# [[foo]]
|
340
|
-
# with the inside [foo] (including brackets) as match[1]
|
341
|
-
if match = lines.first.match(REGEXP[:anchor])
|
342
|
-
Asciidoctor.debug "Found an anchor in line:\n\t#{lines.first}"
|
343
|
-
# NOTE: This expression conditionally strips off the brackets from
|
344
|
-
# [foo], though REGEXP[:anchor] won't actually match without
|
345
|
-
# match[1] being bracketed, so the condition isn't necessary.
|
346
|
-
anchor = match[1].match(/^\[(.*)\]/) ? $1 : match[1]
|
347
|
-
# NOTE: Set @references['foo'] = '[foo]'
|
348
|
-
@references[anchor] = match[1]
|
349
|
-
lines.shift
|
350
|
-
else
|
351
|
-
anchor = nil
|
352
|
-
end
|
353
|
-
|
354
|
-
Asciidoctor.debug "/"*64
|
355
|
-
Asciidoctor.debug "#{__FILE__}:#{__LINE__} - First two lines are:"
|
356
|
-
Asciidoctor.debug lines.first
|
357
|
-
Asciidoctor.debug lines[1]
|
358
|
-
Asciidoctor.debug "/"*64
|
359
|
-
|
360
|
-
block = nil
|
361
|
-
title = nil
|
362
|
-
caption = nil
|
363
|
-
source_type = nil
|
364
|
-
buffer = []
|
365
|
-
while lines.any? && block.nil?
|
366
|
-
buffer.clear
|
367
|
-
this_line = lines.shift
|
368
|
-
next_line = lines.first || ''
|
369
|
-
|
370
|
-
if this_line.match(REGEXP[:comment])
|
371
|
-
next
|
372
|
-
|
373
|
-
elsif match = this_line.match(REGEXP[:title])
|
374
|
-
title = match[1]
|
375
|
-
skip_blank(lines)
|
376
|
-
|
377
|
-
elsif match = this_line.match(REGEXP[:listing_source])
|
378
|
-
source_type = match[1]
|
379
|
-
skip_blank(lines)
|
380
|
-
|
381
|
-
elsif match = this_line.match(REGEXP[:caption])
|
382
|
-
caption = match[1]
|
383
|
-
|
384
|
-
elsif is_section_heading?(this_line, next_line)
|
385
|
-
# If we've come to a new section, then we've found the end of this
|
386
|
-
# current block. Likewise if we'd found an unassigned anchor, push
|
387
|
-
# it back as well, so it can go with this next heading.
|
388
|
-
# NOTE - I don't think this will assign the anchor properly. Anchors
|
389
|
-
# only match with double brackets - [[foo]], but what's stored in
|
390
|
-
# `anchor` at this point is only the `foo` part that was stripped out
|
391
|
-
# after matching. TODO: Need a way to test this.
|
392
|
-
lines.unshift(this_line)
|
393
|
-
lines.unshift(anchor) unless anchor.nil?
|
394
|
-
Asciidoctor.debug "SENDING to next_section with lines[0] = #{lines.first}"
|
395
|
-
block = next_section(lines)
|
396
|
-
|
397
|
-
elsif this_line.match(REGEXP[:oblock])
|
398
|
-
# oblock is surrounded by '--' lines and has zero or more blocks inside
|
399
|
-
buffer = grab_lines_until(lines) { |line| line.match(REGEXP[:oblock]) }
|
400
|
-
|
401
|
-
while buffer.any? && buffer.last.strip.empty?
|
402
|
-
buffer.pop
|
403
|
-
end
|
404
|
-
|
405
|
-
block = Block.new(parent, :oblock, [])
|
406
|
-
while buffer.any?
|
407
|
-
block.blocks << next_block(buffer, block)
|
408
|
-
end
|
409
|
-
|
410
|
-
elsif list_type = [:olist, :ulist, :colist].detect{|l| this_line.match( REGEXP[l] )}
|
411
|
-
items = []
|
412
|
-
block = Block.new(parent, list_type)
|
413
|
-
while !this_line.nil? && match = this_line.match(REGEXP[list_type])
|
414
|
-
item = ListItem.new
|
415
|
-
|
416
|
-
lines.unshift match[2].lstrip.sub(/^\./, '\.')
|
417
|
-
item_segment = list_item_segment(lines, :alt_ending => REGEXP[list_type])
|
418
|
-
while item_segment.any?
|
419
|
-
item.blocks << next_block(item_segment, block)
|
420
|
-
end
|
421
|
-
|
422
|
-
if item.blocks.any? &&
|
423
|
-
item.blocks.first.is_a?(Block) &&
|
424
|
-
(item.blocks.first.context == :paragraph || item.blocks.first.context == :literal)
|
425
|
-
item.content = item.blocks.shift.buffer.map{|l| l.strip}.join("\n")
|
426
|
-
end
|
427
|
-
|
428
|
-
items << item
|
429
|
-
|
430
|
-
skip_blank(lines)
|
431
|
-
|
432
|
-
this_line = lines.shift
|
433
|
-
end
|
434
|
-
lines.unshift(this_line) unless this_line.nil?
|
435
|
-
|
436
|
-
block.buffer = items
|
437
|
-
|
438
|
-
elsif match = this_line.match(REGEXP[:dlist])
|
439
|
-
pairs = []
|
440
|
-
block = Block.new(parent, :dlist)
|
441
|
-
|
442
|
-
this_dlist = Regexp.new(/^#{match[1]}(.*)#{match[3]}\s*$/)
|
443
|
-
|
444
|
-
while !this_line.nil? && match = this_line.match(this_dlist)
|
445
|
-
if anchor = match[1].match( /\[\[([^\]]+)\]\]/ )
|
446
|
-
dt = ListItem.new( $` + $' )
|
447
|
-
dt.anchor = anchor[1]
|
448
|
-
else
|
449
|
-
dt = ListItem.new( match[1] )
|
450
|
-
end
|
451
|
-
dd = ListItem.new
|
452
|
-
lines.shift if lines.any? && lines.first.strip.empty? # workaround eg. git-config OPTIONS --get-colorbool
|
453
|
-
|
454
|
-
dd_segment = list_item_segment(lines, :alt_ending => this_dlist)
|
455
|
-
while dd_segment.any?
|
456
|
-
dd.blocks << next_block(dd_segment, block)
|
457
|
-
end
|
458
|
-
|
459
|
-
if dd.blocks.any? &&
|
460
|
-
dd.blocks.first.is_a?(Block) &&
|
461
|
-
(dd.blocks.first.context == :paragraph || dd.blocks.first.context == :literal)
|
462
|
-
dd.content = dd.blocks.shift.buffer.map{|l| l.strip}.join("\n")
|
463
|
-
end
|
464
|
-
|
465
|
-
pairs << [dt, dd]
|
466
|
-
|
467
|
-
skip_blank(lines)
|
468
|
-
|
469
|
-
this_line = lines.shift
|
470
|
-
end
|
471
|
-
lines.unshift(this_line) unless this_line.nil?
|
472
|
-
block.buffer = pairs
|
473
|
-
|
474
|
-
elsif this_line.match(REGEXP[:verse])
|
475
|
-
# verse is preceded by [verse] and lasts until a blank line
|
476
|
-
buffer = grab_lines_until(lines, :break_on_blank_lines => true)
|
477
|
-
block = Block.new(parent, :verse, buffer)
|
478
|
-
|
479
|
-
elsif this_line.match(REGEXP[:note])
|
480
|
-
# note is an admonition preceded by [NOTE] and lasts until a blank line
|
481
|
-
buffer = grab_lines_until(lines, :break_on_blank_lines => true) {|line| line.match( REGEXP[:continue] ) }
|
482
|
-
block = Block.new(parent, :note, buffer)
|
483
|
-
|
484
|
-
elsif block_type = [:listing, :example].detect{|t| this_line.match( REGEXP[t] )}
|
485
|
-
buffer = grab_lines_until(lines) {|line| line.match( REGEXP[block_type] )}
|
486
|
-
block = Block.new(parent, block_type, buffer)
|
487
|
-
|
488
|
-
elsif this_line.match( REGEXP[:quote] )
|
489
|
-
block = Block.new(parent, :quote)
|
490
|
-
buffer = grab_lines_until(lines) {|line| line.match( REGEXP[:quote] ) }
|
491
|
-
|
492
|
-
while buffer.any?
|
493
|
-
block.blocks << next_block(buffer, block)
|
494
|
-
end
|
495
|
-
|
496
|
-
elsif this_line.match(REGEXP[:lit_blk])
|
497
|
-
# example is surrounded by '....' (4 or more '.' chars) lines
|
498
|
-
buffer = grab_lines_until(lines) {|line| line.match( REGEXP[:lit_blk] ) }
|
499
|
-
block = Block.new(parent, :literal, buffer)
|
500
|
-
|
501
|
-
elsif this_line.match(REGEXP[:lit_par])
|
502
|
-
# literal paragraph is contiguous lines starting with
|
503
|
-
# one or more space or tab characters
|
504
|
-
|
505
|
-
# So we need to actually include this one in the grab_lines group
|
506
|
-
lines.unshift( this_line )
|
507
|
-
buffer = grab_lines_until(lines, :preserve_last_line => true) {|line| ! line.match( REGEXP[:lit_par] ) }
|
508
|
-
|
509
|
-
block = Block.new(parent, :literal, buffer)
|
510
|
-
|
511
|
-
elsif this_line.match(REGEXP[:sidebar_blk])
|
512
|
-
# example is surrounded by '****' (4 or more '*' chars) lines
|
513
|
-
buffer = grab_lines_until(lines) {|line| line.match( REGEXP[:sidebar_blk] ) }
|
514
|
-
block = Block.new(parent, :sidebar, buffer)
|
515
|
-
|
516
|
-
else
|
517
|
-
# paragraph is contiguous nonblank/noncontinuation lines
|
518
|
-
while !this_line.nil? && !this_line.strip.empty?
|
519
|
-
break if this_line.match(REGEXP[:continue])
|
520
|
-
if this_line.match( REGEXP[:listing] ) || this_line.match( REGEXP[:oblock] )
|
521
|
-
lines.unshift this_line
|
522
|
-
break
|
523
|
-
end
|
524
|
-
buffer << this_line
|
525
|
-
this_line = lines.shift
|
526
|
-
end
|
527
|
-
|
528
|
-
if buffer.any? && admonition = buffer.first.match(/^NOTE:\s*/)
|
529
|
-
buffer[0] = admonition.post_match
|
530
|
-
block = Block.new(parent, :note, buffer)
|
531
|
-
elsif source_type
|
532
|
-
block = Block.new(parent, :listing, buffer)
|
533
|
-
else
|
534
|
-
block = Block.new(parent, :paragraph, buffer)
|
535
|
-
end
|
536
|
-
end
|
537
|
-
end
|
538
|
-
|
539
|
-
block.anchor ||= anchor
|
540
|
-
block.title ||= title
|
541
|
-
block.caption ||= caption
|
542
|
-
|
543
|
-
block
|
544
|
-
end
|
545
|
-
|
546
|
-
# Private: Get the Integer section level based on the characters
|
547
|
-
# used in the ASCII line under the section name.
|
548
|
-
#
|
549
|
-
# line - the String line from under the section name.
|
550
|
-
def section_level(line)
|
551
|
-
char = line.strip.chars.to_a.uniq
|
552
|
-
case char
|
553
|
-
when ['=']; 0
|
554
|
-
when ['-']; 1
|
555
|
-
when ['~']; 2
|
556
|
-
when ['^']; 3
|
557
|
-
when ['+']; 4
|
558
|
-
end
|
559
|
-
end
|
560
|
-
|
561
|
-
# == is level 0, === is level 1, etc.
|
562
|
-
def single_line_section_level(line)
|
563
|
-
[line.length - 1, 0].max
|
564
|
-
end
|
565
|
-
|
566
|
-
def is_single_line_section_heading?(line)
|
567
|
-
!line.nil? && line.match(REGEXP[:level_title])
|
568
|
-
end
|
569
|
-
|
570
|
-
def is_two_line_section_heading?(line1, line2)
|
571
|
-
!line1.nil? && !line2.nil? &&
|
572
|
-
line1.match(REGEXP[:name]) && line2.match(REGEXP[:line]) &&
|
573
|
-
(line1.size - line2.size).abs <= 1
|
574
|
-
end
|
575
|
-
|
576
|
-
def is_section_heading?(line1, line2 = nil)
|
577
|
-
is_single_line_section_heading?(line1) ||
|
578
|
-
is_two_line_section_heading?(line1, line2)
|
579
|
-
end
|
580
|
-
|
581
|
-
# Private: Extracts the name, level and (optional) embedded anchor from a
|
582
|
-
# 1- or 2-line section heading.
|
583
|
-
#
|
584
|
-
# Returns an array of a String, Integer, and String or nil.
|
585
|
-
#
|
586
|
-
# Examples
|
587
|
-
#
|
588
|
-
# line1
|
589
|
-
# => "Foo\n"
|
590
|
-
# line2
|
591
|
-
# => "~~~\n"
|
592
|
-
#
|
593
|
-
# name, level, anchor = extract_section_heading(line1, line2)
|
594
|
-
#
|
595
|
-
# name
|
596
|
-
# => "Foo"
|
597
|
-
# level
|
598
|
-
# => 2
|
599
|
-
# anchor
|
600
|
-
# => nil
|
601
|
-
#
|
602
|
-
# line1
|
603
|
-
# => "==== Foo\n"
|
604
|
-
#
|
605
|
-
# name, level, anchor = extract_section_heading(line1)
|
606
|
-
#
|
607
|
-
# name
|
608
|
-
# => "Foo"
|
609
|
-
# level
|
610
|
-
# => 3
|
611
|
-
# anchor
|
612
|
-
# => nil
|
613
|
-
#
|
614
|
-
def extract_section_heading(line1, line2 = nil)
|
615
|
-
Asciidoctor.debug "Processing line1: #{line1.chomp rescue 'nil'}, line2: #{line2.chomp rescue 'nil'}"
|
616
|
-
sect_name = sect_anchor = nil
|
617
|
-
sect_level = 0
|
618
|
-
|
619
|
-
if is_single_line_section_heading?(line1)
|
620
|
-
header_match = line1.match(REGEXP[:level_title])
|
621
|
-
sect_name = header_match[2]
|
622
|
-
sect_level = single_line_section_level(header_match[1])
|
623
|
-
elsif is_two_line_section_heading?(line1, line2)
|
624
|
-
header_match = line1.match(REGEXP[:name])
|
625
|
-
if anchor_match = header_match[1].match(REGEXP[:anchor_embedded])
|
626
|
-
sect_name = anchor_match[1]
|
627
|
-
sect_anchor = anchor_match[2]
|
628
|
-
else
|
629
|
-
sect_name = header_match[1]
|
630
|
-
end
|
631
|
-
sect_level = section_level(line2)
|
632
|
-
end
|
633
|
-
Asciidoctor.debug "Returning #{sect_name}, #{sect_level}, and #{sect_anchor}"
|
634
|
-
return [sect_name, sect_level, sect_anchor]
|
635
|
-
end
|
636
|
-
|
637
|
-
# Private: Return the next section from the document.
|
638
|
-
#
|
639
|
-
# Examples
|
640
|
-
#
|
641
|
-
# source
|
642
|
-
# => "GREETINGS\n---------\nThis is my doc.\n\nSALUTATIONS\n-----------\nIt is awesome."
|
643
|
-
#
|
644
|
-
# doc = Asciidoctor::Document.new(source)
|
645
|
-
#
|
646
|
-
# doc.next_section
|
647
|
-
# ["GREETINGS", [:paragraph, "This is my doc."]]
|
648
|
-
#
|
649
|
-
# doc.next_section
|
650
|
-
# ["SALUTATIONS", [:paragraph, "It is awesome."]]
|
651
|
-
def next_section(lines)
|
652
|
-
section = Section.new(self)
|
653
|
-
|
654
|
-
Asciidoctor.debug "%"*64
|
655
|
-
Asciidoctor.debug "#{__FILE__}:#{__LINE__} - First two lines are:"
|
656
|
-
Asciidoctor.debug lines.first
|
657
|
-
Asciidoctor.debug lines[1]
|
658
|
-
Asciidoctor.debug "%"*64
|
659
|
-
|
660
|
-
# Skip ahead to the next section definition
|
661
|
-
while lines.any? && section.name.nil?
|
662
|
-
this_line = lines.shift
|
663
|
-
next_line = lines.first || ''
|
664
|
-
if match = this_line.match(REGEXP[:anchor])
|
665
|
-
section.anchor = match[1]
|
666
|
-
elsif is_section_heading?(this_line, next_line)
|
667
|
-
section.name, section.level, section.anchor = extract_section_heading(this_line, next_line)
|
668
|
-
lines.shift unless is_single_line_section_heading?(this_line)
|
669
|
-
end
|
670
|
-
end
|
671
|
-
|
672
|
-
if !section.anchor.nil?
|
673
|
-
anchor_id = section.anchor.match(/^\[(.*)\]/) ? $1 : section.anchor
|
674
|
-
@references[anchor_id] = section.anchor
|
675
|
-
section.anchor = anchor_id
|
676
|
-
end
|
677
|
-
|
678
|
-
# Grab all the lines that belong to this section
|
679
|
-
section_lines = []
|
680
|
-
while lines.any?
|
681
|
-
this_line = lines.shift
|
682
|
-
next_line = lines.first
|
683
|
-
|
684
|
-
if is_section_heading?(this_line, next_line)
|
685
|
-
_, this_level, _ = extract_section_heading(this_line, next_line)
|
686
|
-
# A section can't contain a section level lower than itself,
|
687
|
-
# so this signifies the end of the section.
|
688
|
-
if this_level <= section.level
|
689
|
-
lines.unshift this_line
|
690
|
-
lines.unshift section_lines.pop if section_lines.any? && section_lines.last.match(REGEXP[:anchor])
|
691
|
-
break
|
692
|
-
else
|
693
|
-
section_lines << this_line
|
694
|
-
section_lines << lines.shift unless is_single_line_section_heading?(this_line)
|
695
|
-
end
|
696
|
-
elsif this_line.match(REGEXP[:listing])
|
697
|
-
section_lines << this_line
|
698
|
-
section_lines.concat grab_lines_until(lines) {|line| line.match( REGEXP[:listing] ) }
|
699
|
-
# Also grab the last line, if there is one
|
700
|
-
this_line = lines.shift
|
701
|
-
section_lines << this_line unless this_line.nil?
|
702
|
-
else
|
703
|
-
section_lines << this_line
|
704
|
-
end
|
705
|
-
end
|
706
|
-
|
707
|
-
# Now parse section_lines into Blocks
|
708
|
-
while section_lines.any?
|
709
|
-
skip_blank(section_lines)
|
710
|
-
|
711
|
-
section << next_block(section_lines, section) if section_lines.any?
|
712
|
-
end
|
713
|
-
|
714
|
-
section
|
715
|
-
end
|
716
|
-
# end private
|
717
118
|
end
|