asciidoctor 0.0.1

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,188 @@
1
+ # Public: Methods for managing blocks of Asciidoc content in a section.
2
+ #
3
+ # Examples
4
+ #
5
+ # block = Asciidoctor::Block.new(:paragraph, ["`This` is a <test>"])
6
+ # block.content
7
+ # => ["<em>This</em> is a &lt;test&gt;"]
8
+ class Asciidoctor::Block
9
+ # Public: Get the Symbol context for this section block.
10
+ attr_reader :context
11
+
12
+ # Public: Get the Array of sub-blocks for this section block.
13
+ attr_reader :blocks
14
+
15
+ # Public: Get/Set the original Array content for this section block.
16
+ attr_accessor :buffer
17
+
18
+ # Public: Get/Set the String section anchor name.
19
+ attr_accessor :anchor
20
+
21
+ # Public: Get/Set the String block title.
22
+ attr_accessor :title
23
+
24
+ # Public: Get/Set the String block caption.
25
+ attr_accessor :caption
26
+
27
+ # Public: Initialize an Asciidoctor::Block object.
28
+ #
29
+ # parent - The parent Asciidoc Object.
30
+ # context - The Symbol context name for the type of content.
31
+ # buffer - The Array buffer of source data.
32
+ def initialize(parent, context, buffer=nil)
33
+ @parent = parent
34
+ @context = context
35
+ @buffer = buffer
36
+
37
+ @blocks = []
38
+ end
39
+
40
+ # Public: Get the Asciidoctor::Document instance to which this Block belongs
41
+ def document
42
+ @parent.is_a?(Asciidoctor::Document) ? @parent : @parent.document
43
+ end
44
+
45
+ # Public: Get the Asciidoctor::Renderer instance being used for the ancestor
46
+ # Asciidoctor::Document instance.
47
+ def renderer
48
+ @parent.renderer
49
+ end
50
+
51
+ # Public: Get the rendered String content for this Block. If the block
52
+ # has child blocks, the content method should cause them to be
53
+ # rendered and returned as content that can be included in the
54
+ # parent block's template.
55
+ def render
56
+ Asciidoctor.debug "Now attempting to render for #{context} my own bad #{self}"
57
+ Asciidoctor.debug "Parent is #{@parent}"
58
+ Asciidoctor.debug "Renderer is #{renderer}"
59
+ renderer.render("section_#{context}", self)
60
+ end
61
+
62
+ # Public: Get an HTML-ified version of the source buffer, with special
63
+ # Asciidoc characters and entities converted to their HTML equivalents.
64
+ #
65
+ # Examples
66
+ #
67
+ # block = Asciidoctor::Block.new(:paragraph, ['`This` is what happens when you <meet> a stranger in the <alps>!'])
68
+ # block.content
69
+ # => ["<em>This</em> is what happens when you &lt;meet&gt; a stranger in the &lt;alps&gt;!"]
70
+ #
71
+ # TODO:
72
+ # * forced line breaks
73
+ # * bold, mono
74
+ # * double/single quotes
75
+ # * super/sub script
76
+ def content
77
+ case @context
78
+ when :dlist
79
+ @buffer.map do |dt, dd|
80
+ if !dt.anchor.nil? && !dt.anchor.empty?
81
+ html_dt = "<a id=#{dt.anchor}></a>" + htmlify(dt.content)
82
+ else
83
+ html_dt = htmlify(dt.content)
84
+ end
85
+ if dd.content.empty?
86
+ html_dd = ''
87
+ else
88
+ html_dd = "<p>#{htmlify(dd.content)}</p>"
89
+ end
90
+ html_dd += dd.blocks.map{|block| block.render}.join
91
+
92
+ [html_dt, html_dd]
93
+ end
94
+ when :oblock, :quote
95
+ blocks.map{|block| block.render}.join
96
+ when :olist, :ulist, :colist
97
+ @buffer.map do |li|
98
+ htmlify(li.content) + li.blocks.map{|block| block.render}.join
99
+ end
100
+ when :listing
101
+ @buffer.map{|l| CGI.escapeHTML(l).gsub(/(<\d+>)/,'<b>\1</b>')}.join
102
+ when :literal
103
+ htmlify( @buffer.join.gsub( '*', '{asterisk}' ).gsub( '\'', '{apostrophe}' ))
104
+ when :verse
105
+ htmlify( @buffer.map{ |l| l.strip }.join( "\n" ) )
106
+ else
107
+ htmlify( @buffer.map{ |l| l.lstrip }.join )
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ # Private: Return a String HTML version of the source string, with
114
+ # Asciidoc characters converted and HTML entities escaped.
115
+ #
116
+ # string - The String source string in Asciidoc format.
117
+ #
118
+ # Examples
119
+ #
120
+ # asciidoc_string = "Make 'this' <emphasized>"
121
+ # htmlify(asciidoc_string)
122
+ # => "Make <em>this</em> &lt;emphasized&gt;"
123
+ def htmlify(string)
124
+ unless string.nil?
125
+ html = string.dup
126
+
127
+ # Convert reference links to "link:" asciidoc for later HTMLification.
128
+ # This ensures that eg. "<<some reference>>" is turned into a link but
129
+ # "`<<<<<` and `>>>>>` are conflict markers" is not. This is much
130
+ # easier before the HTML is escaped and <> are turned into entities.
131
+ html.gsub!( /(^|[^<])<<([^<>,]+)(,([^>]*))?>>/ ) { "#{$1}link:##{$2}[" + ($4.nil? ? document.references[$2] : $4).to_s + "]" }
132
+
133
+ # Do the same with URLs
134
+ html.gsub!( /(^|[^`])(https?:\/\/[^\[ ]+)(\[+[^\]]*\]+)?/ ) do
135
+ pre = $1
136
+ url = $2
137
+ link = ( $3 || $2 ).gsub( /(^\[|\]$)/,'' )
138
+ link = url if link.empty?
139
+
140
+ "#{pre}link:#{url}[#{link}]"
141
+ end
142
+
143
+ html = CGI.escapeHTML(html)
144
+ html.gsub!(Asciidoctor::REGEXP[:biblio], '<a name="\1">[\1]</a>')
145
+ html.gsub!(Asciidoctor::REGEXP[:ruler], '<hr>\n')
146
+ html.gsub!(/``(.*?)''/m, '&ldquo;\1&rdquo;')
147
+ html.gsub!(/`(.*?)'/m, '&lsquo;\1&rsquo;')
148
+ html.gsub!(/`([^`]+)`/m) { "<tt>#{$1.gsub( '*', '{asterisk}' ).gsub( '\'', '{apostrophe}' )}</tt>" }
149
+ html.gsub!(/([\s\W])#(.+?)#([\s\W])/, '\1\2\3')
150
+
151
+ # "Unconstrained" quotes
152
+ html.gsub!(/\_\_([^\_]+)\_\_/m, '<em>\1</em>')
153
+ html.gsub!(/\*\*([^\*]+)\*\*/m, '<strong>\1</strong>')
154
+ html.gsub!(/\+\+([^\+]+)\+\+/m, '<tt>\1</tt>')
155
+ html.gsub!(/\^\^([^\^]+)\^\^/m, '<sup>\1</sup>')
156
+ html.gsub!(/\~\~([^\~]+)\~\~/m, '<sub>\1</sub>')
157
+
158
+ # "Constrained" quotes, which must be bounded by white space or
159
+ # common punctuation characters
160
+ html.gsub!(/([\s\W])\*([^\*]+)\*([\s\W])/m, '\1<strong>\2</strong>\3')
161
+ html.gsub!(/([\s\W])'(.+?)'([\s\W])/m, '\1<em>\2</em>\3')
162
+ html.gsub!(/([\s\W])_([^_]+)_([\s\W])/m, '\1<em>\2</em>\3')
163
+ html.gsub!(/([\s\W])\+([^\+]+)\+([\s\W])/m, '\1<tt>\2</tt>\3')
164
+ html.gsub!(/([\s\W])\^([^\^]+)\^([\s\W])/m, '\1<sup>\2</sup>\3')
165
+ html.gsub!(/([\s\W])\~([^\~]+)\~([\s\W])/m, '\1<sub>\2</sub>\3')
166
+
167
+ # Don't have lookbehind so have to capture and re-insert
168
+ html.gsub!(/(^|[^\\])\{(\w[\w\-]+\w)\}/) do
169
+ if self.document.defines.has_key?($2)
170
+ # Substitute from user defines first
171
+ $1 + self.document.defines[$2]
172
+ elsif Asciidoctor::INTRINSICS.has_key?($2)
173
+ # Then do intrinsics
174
+ $1 + Asciidoctor::INTRINSICS[$2]
175
+ else
176
+ # leave everything else alone
177
+ "#{$1}{#{$2}}"
178
+ end
179
+ end
180
+
181
+ html.gsub!(/\\([\{\}\-])/, '\1')
182
+ html.gsub!(/linkgit:([^\]]+)\[(\d+)\]/, '<a href="\1.html">\1(\2)</a>')
183
+ html.gsub!(/link:([^\[]+)(\[+[^\]]*\]+)/ ) { "<a href=\"#{$1}\">#{$2.gsub( /(^\[|\]$)/,'' )}</a>" }
184
+ html
185
+ end
186
+ end
187
+ # end private
188
+ end
@@ -0,0 +1,6 @@
1
+ module Asciidoctor
2
+ def self.debug(*args)
3
+ puts *args unless ENV['SUPPRESS_DEBUG']
4
+ end
5
+ end
6
+
@@ -0,0 +1,717 @@
1
+ # Public: Methods for parsing Asciidoc documents and rendering them
2
+ # using erb templates.
3
+ class Asciidoctor::Document
4
+
5
+ include Asciidoctor
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
+ # Public: Get the Hash of defines
15
+ attr_reader :defines
16
+
17
+ # Public: Get the Hash of document references
18
+ attr_reader :references
19
+
20
+ # Need these for pseudo-template yum
21
+ attr_reader :header, :preamble
22
+
23
+ # Public: Get the Array of elements (really Blocks or Sections) for the document
24
+ attr_reader :elements
25
+
26
+ # Public: Initialize an Asciidoc object.
27
+ #
28
+ # data - The String Asciidoc source document.
29
+ # block - A block that can be used to retrieve external Asciidoc
30
+ # data to include in this document.
31
+ #
32
+ # Examples
33
+ #
34
+ # base = File.dirname(filename)
35
+ # data = File.read(filename)
36
+ # doc = Asciidoctor::Document.new(data)
37
+ def initialize(data, &block)
38
+ raw_source = []
39
+ @elements = []
40
+ @defines = {}
41
+ @references = {}
42
+
43
+ include_regexp = /^include::([^\[]+)\[\]\s*\n?\z/
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
51
+
52
+ ifdef_regexp = /^(ifdef|ifndef)::([^\[]+)\[\]/
53
+ endif_regexp = /^endif::/
54
+ defattr_regexp = /^:([^:]+):\s*(.*)\s*$/
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
124
+
125
+ # Now parse @lines into elements
126
+ while @lines.any?
127
+ skip_blank(@lines)
128
+
129
+ @elements << next_block(@lines) if @lines.any?
130
+ end
131
+
132
+ Asciidoctor.debug "Found #{@elements.size} elements:"
133
+ @elements.each do |el|
134
+ Asciidoctor.debug el
135
+ end
136
+
137
+ root = @elements.first
138
+ # Try to find a @header from the Section blocks we have (if any).
139
+ if root.is_a?(Section) && root.level == 0
140
+ @header = @elements.shift
141
+ @elements = @header.blocks + @elements
142
+ @header.clear_blocks
143
+ end
144
+
145
+ end
146
+
147
+ # We need to be able to return some semblance of a title
148
+ def title
149
+ return @title if @title
150
+
151
+ if @header
152
+ @title = @header.title || @header.name
153
+ elsif @elements.first
154
+ @title = @elements.first.title
155
+ # Blocks don't have a :name method, but Sections do
156
+ @title ||= @elements.first.name if @elements.first.respond_to? :name
157
+ end
158
+
159
+ @title
160
+ end
161
+
162
+ def splain
163
+ if @header
164
+ puts "Header is #{@header}"
165
+ else
166
+ puts "No header"
167
+ end
168
+
169
+ puts "I have #{@elements.count} elements"
170
+ @elements.each_with_index do |block, i|
171
+ puts "Block ##{i} is a #{block.class}"
172
+ puts "Name is #{block.name rescue 'n/a'}"
173
+ puts "=" * 40
174
+ end
175
+ end
176
+
177
+ # Public: Render the Asciidoc document using erb templates
178
+ #
179
+ def render
180
+ @renderer ||= Renderer.new
181
+ html = self.renderer.render('document', self, :header => @header, :preamble => @preamble)
182
+ end
183
+
184
+ def content
185
+ html_pieces = []
186
+ @elements.each do |element|
187
+ Asciidoctor::debug "Rendering element: #{element}"
188
+ html_pieces << element.render
189
+ end
190
+ html_pieces.join("\n")
191
+ end
192
+
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
+ end