asciidoctor 0.1.3 → 0.1.4

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.

Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +387 -0
  3. data/README.adoc +358 -348
  4. data/asciidoctor.gemspec +30 -9
  5. data/bin/asciidoctor +3 -0
  6. data/bin/asciidoctor-safe +3 -0
  7. data/compat/asciidoc.conf +76 -4
  8. data/lib/asciidoctor.rb +174 -79
  9. data/lib/asciidoctor/abstract_block.rb +131 -101
  10. data/lib/asciidoctor/abstract_node.rb +108 -26
  11. data/lib/asciidoctor/attribute_list.rb +1 -1
  12. data/lib/asciidoctor/backends/_stylesheets.rb +204 -62
  13. data/lib/asciidoctor/backends/base_template.rb +11 -22
  14. data/lib/asciidoctor/backends/docbook45.rb +158 -163
  15. data/lib/asciidoctor/backends/docbook5.rb +103 -0
  16. data/lib/asciidoctor/backends/html5.rb +662 -445
  17. data/lib/asciidoctor/block.rb +54 -44
  18. data/lib/asciidoctor/cli/invoker.rb +41 -20
  19. data/lib/asciidoctor/cli/options.rb +66 -20
  20. data/lib/asciidoctor/debug.rb +1 -1
  21. data/lib/asciidoctor/document.rb +265 -100
  22. data/lib/asciidoctor/extensions.rb +443 -0
  23. data/lib/asciidoctor/helpers.rb +38 -6
  24. data/lib/asciidoctor/inline.rb +5 -5
  25. data/lib/asciidoctor/lexer.rb +532 -250
  26. data/lib/asciidoctor/{list_item.rb → list.rb} +33 -13
  27. data/lib/asciidoctor/path_resolver.rb +28 -2
  28. data/lib/asciidoctor/reader.rb +814 -455
  29. data/lib/asciidoctor/renderer.rb +128 -42
  30. data/lib/asciidoctor/section.rb +55 -41
  31. data/lib/asciidoctor/substituters.rb +380 -107
  32. data/lib/asciidoctor/table.rb +40 -30
  33. data/lib/asciidoctor/version.rb +1 -1
  34. data/man/asciidoctor.1 +32 -96
  35. data/man/{asciidoctor.ad → asciidoctor.adoc} +57 -48
  36. data/test/attributes_test.rb +200 -27
  37. data/test/blocks_test.rb +361 -22
  38. data/test/document_test.rb +496 -81
  39. data/test/extensions_test.rb +448 -0
  40. data/test/fixtures/basic-docinfo-footer.html +6 -0
  41. data/test/fixtures/basic-docinfo-footer.xml +8 -0
  42. data/test/fixtures/basic-docinfo.xml +3 -3
  43. data/test/fixtures/basic.asciidoc +1 -0
  44. data/test/fixtures/child-include.adoc +5 -0
  45. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
  46. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
  47. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
  48. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
  49. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
  50. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
  51. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
  52. data/test/fixtures/docinfo-footer.html +1 -0
  53. data/test/fixtures/docinfo-footer.xml +9 -0
  54. data/test/fixtures/docinfo.xml +1 -0
  55. data/test/fixtures/grandchild-include.adoc +3 -0
  56. data/test/fixtures/parent-include-restricted.adoc +5 -0
  57. data/test/fixtures/parent-include.adoc +5 -0
  58. data/test/invoker_test.rb +82 -8
  59. data/test/lexer_test.rb +21 -3
  60. data/test/links_test.rb +34 -2
  61. data/test/lists_test.rb +304 -7
  62. data/test/options_test.rb +19 -3
  63. data/test/paragraphs_test.rb +13 -0
  64. data/test/paths_test.rb +22 -0
  65. data/test/preamble_test.rb +20 -0
  66. data/test/reader_test.rb +1096 -644
  67. data/test/renderer_test.rb +152 -12
  68. data/test/sections_test.rb +417 -76
  69. data/test/substitutions_test.rb +339 -138
  70. data/test/tables_test.rb +109 -4
  71. data/test/test_helper.rb +79 -13
  72. data/test/text_test.rb +111 -11
  73. metadata +54 -18
@@ -1,4 +1,28 @@
1
1
  module Asciidoctor
2
+ # Public: Methods for managing AsciiDoc lists (ordered, unordered and labeled lists)
3
+ class List < AbstractBlock
4
+
5
+ # Public: Create alias for blocks
6
+ alias :items :blocks
7
+ alias :items? :blocks?
8
+
9
+ def initialize(parent, context)
10
+ super(parent, context)
11
+ end
12
+
13
+ # Public: Get the items in this list as an Array
14
+ def content
15
+ @blocks
16
+ end
17
+
18
+ def render
19
+ result = super
20
+ @document.callouts.next_list if @context == :colist
21
+ result
22
+ end
23
+
24
+ end
25
+
2
26
  # Public: Methods for managing items for AsciiDoc olists, ulist, and dlists.
3
27
  class ListItem < AbstractBlock
4
28
 
@@ -20,12 +44,7 @@ class ListItem < AbstractBlock
20
44
  end
21
45
 
22
46
  def text
23
- # this will allow the text to be processed
24
- Block.new(self, nil, [@text]).content
25
- end
26
-
27
- def content
28
- blocks? ? blocks.map {|b| b.render }.join : nil
47
+ apply_subs @text
29
48
  end
30
49
 
31
50
  # Public: Fold the first paragraph block into the text
@@ -40,23 +59,24 @@ class ListItem < AbstractBlock
40
59
  #
41
60
  # Returns nothing
42
61
  def fold_first(continuation_connects_first_block = false, content_adjacent = false)
43
- if !blocks.empty? && blocks.first.is_a?(Block) &&
44
- ((blocks.first.context == :paragraph && !continuation_connects_first_block) ||
45
- ((content_adjacent || !continuation_connects_first_block) && blocks.first.context == :literal &&
46
- blocks.first.attr('options', []).include?('listparagraph')))
62
+ if !(first_block = @blocks.first).nil? && first_block.is_a?(Block) &&
63
+ ((first_block.context == :paragraph && !continuation_connects_first_block) ||
64
+ ((content_adjacent || !continuation_connects_first_block) && first_block.context == :literal &&
65
+ first_block.option?('listparagraph')))
47
66
 
48
67
  block = blocks.shift
49
68
  unless @text.to_s.empty?
50
- block.buffer.unshift("#@text\n")
69
+ block.lines.unshift("#@text\n")
51
70
  end
52
71
 
53
- @text = block.buffer.join
72
+ @text = block.source
54
73
  end
55
74
  nil
56
75
  end
57
76
 
58
77
  def to_s
59
- "#{super.to_s} - #@context [text:#@text, blocks:#{(@blocks || []).size}]"
78
+ "#@context [text:#@text, blocks:#{(@blocks || []).size}]"
60
79
  end
80
+
61
81
  end
62
82
  end
@@ -312,7 +312,7 @@ class PathResolver
312
312
  elsif !recover
313
313
  raise SecurityError, "#{opts[:target_name] || 'path'} #{target} refers to location outside jail: #{jail} (disallowed in safe mode)"
314
314
  elsif !warned
315
- puts "asciidoctor: WARNING: #{opts[:target_name] || 'path'} has illegal reference to ancestor of jail, auto-recovering"
315
+ warn "asciidoctor: WARNING: #{opts[:target_name] || 'path'} has illegal reference to ancestor of jail, auto-recovering"
316
316
  warned = true
317
317
  end
318
318
  else
@@ -339,9 +339,14 @@ class PathResolver
339
339
  def web_path(target, start = nil)
340
340
  target = posixfy(target)
341
341
  start = posixfy(start)
342
+ uri_prefix = nil
342
343
 
343
344
  unless is_web_root?(target) || start.empty?
344
345
  target = "#{start}#{SLASH}#{target}"
346
+ if target.include?(':') && target.match(Asciidoctor::REGEXP[:uri_sniff])
347
+ uri_prefix = $~[0]
348
+ target = target[uri_prefix.length..-1]
349
+ end
345
350
  end
346
351
 
347
352
  target_segments, target_root, _ = partition_path(target, true)
@@ -360,7 +365,28 @@ class PathResolver
360
365
  accum
361
366
  end
362
367
 
363
- join_path resolved_segments, target_root
368
+ if uri_prefix.nil?
369
+ join_path resolved_segments, target_root
370
+ else
371
+ "#{uri_prefix}#{join_path resolved_segments, target_root}"
372
+ end
373
+ end
374
+
375
+ # Public: Calculate the relative path to this absolute filename from the specified base directory
376
+ #
377
+ # If either the filename or the base_directory are not absolute paths, no work is done.
378
+ #
379
+ # filename - An absolute file name as a String
380
+ # base_directory - An absolute base directory as a String
381
+ #
382
+ # Return the relative path String of the filename calculated from the base directory
383
+ def relative_path(filename, base_directory)
384
+ if (is_root? filename) && (is_root? base_directory)
385
+ offset = base_directory.chomp(@file_separator).length + 1
386
+ filename[offset..-1]
387
+ else
388
+ filename
389
+ end
364
390
  end
365
391
  end
366
392
  end
@@ -1,90 +1,284 @@
1
1
  module Asciidoctor
2
2
  # Public: Methods for retrieving lines from AsciiDoc source files
3
3
  class Reader
4
+ class Cursor
5
+ attr_accessor :file
6
+ attr_accessor :dir
7
+ attr_accessor :path
8
+ attr_accessor :lineno
9
+
10
+ def initialize file, dir = nil, path = nil, lineno = nil
11
+ @file = file
12
+ @dir = dir
13
+ @path = path
14
+ @lineno = lineno
15
+ end
4
16
 
5
- # Public: Get the document source as a String Array of lines.
6
- attr_reader :source
17
+ def line_info
18
+ %(#{path}: line #{lineno})
19
+ end
20
+ end
21
+
22
+ attr_reader :file
23
+ attr_reader :dir
24
+ attr_reader :path
7
25
 
8
26
  # Public: Get the 1-based offset of the current line.
9
27
  attr_reader :lineno
10
28
 
11
- # Public: Initialize the Reader object.
29
+ # Public: Get the document source as a String Array of lines.
30
+ attr_reader :source_lines
31
+
32
+ # Public: Control whether lines are processed using Reader#process_line on first visit (default: true)
33
+ attr_accessor :process_lines
34
+
35
+ # Public: Initialize the Reader object
36
+ def initialize data = nil, cursor = nil
37
+ if cursor.nil?
38
+ @file = @dir = nil
39
+ @path = '<stdin>'
40
+ @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
41
+ elsif cursor.is_a? String
42
+ @file = cursor
43
+ @dir = File.dirname @file
44
+ @path = File.basename @file
45
+ @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
46
+ else
47
+ @file = cursor.file
48
+ @dir = cursor.dir
49
+ @path = cursor.path || '<stdin>'
50
+ unless @file.nil?
51
+ if @dir.nil?
52
+ # REVIEW might to look at this assignment closer
53
+ @dir = File.dirname @file
54
+ @dir = nil if @dir == '.' # right?
55
+ end
56
+
57
+ if cursor.path.nil?
58
+ @path = File.basename @file
59
+ end
60
+ end
61
+ @lineno = cursor.lineno || 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
62
+ end
63
+ @lines = data.nil? ? [] : (prepare_lines data)
64
+ @source_lines = @lines.dup
65
+ @eof = @lines.empty?
66
+ @look_ahead = 0
67
+ @process_lines = true
68
+ @unescape_next_line = false
69
+ end
70
+
71
+ # Internal: Prepare the lines from the provided data
72
+ #
73
+ # This method strips whitespace from the end of every line of
74
+ # the source data and appends a LF (i.e., Unix endline). This
75
+ # whitespace substitution is very important to how Asciidoctor
76
+ # works.
12
77
  #
13
- # data - The Array of Strings holding the Asciidoc source document. The
14
- # original instance of this Array is not modified (default: nil)
15
- # document - The document with which this reader is associated. Used to access
16
- # document attributes (default: nil)
17
- # preprocess - A flag indicating whether to run the preprocessor on these lines.
18
- # Only enable for the outer-most Reader. If this argument is true,
19
- # a Document object must also be supplied.
20
- # (default: false)
21
- # block - A block that can be used to retrieve external Asciidoc
22
- # data to include in this document.
78
+ # Any leading or trailing blank lines are also removed.
23
79
  #
24
- # Examples
80
+ # The normalized lines are assigned to the @lines instance variable.
25
81
  #
26
- # data = File.readlines(filename)
27
- # reader = Asciidoctor::Reader.new data
28
- def initialize(data = nil, document = nil, preprocess = false, &block)
29
- data = [] if data.nil?
30
- # TODO use Struct to track file/lineno info; track as file changes; offset for sub-readers
31
- @lineno = 0
32
- if !preprocess
33
- @lines = data.is_a?(String) ? data.lines.entries : data.dup
34
- @preprocess_source = false
35
- elsif !data.empty?
36
- # NOTE we assume document is not nil!
37
- @document = document
38
- @preprocess_source = true
39
- @include_block = block_given? ? block : nil
40
- normalize_data(data.is_a?(String) ? data.lines.entries : data)
82
+ # data - A String Array of input data to be normalized
83
+ # opts - A Hash of options to control what cleansing is done
84
+ #
85
+ # Returns The String lines extracted from the data
86
+ def prepare_lines data, opts = {}
87
+ data.is_a?(String) ? data.each_line.to_a : data.dup
88
+ end
89
+
90
+ # Internal: Processes a previously unvisited line
91
+ #
92
+ # By default, this method marks the line as processed
93
+ # by incrementing the look_ahead counter and returns
94
+ # the line unmodified.
95
+ #
96
+ # Returns The String line the Reader should make available to the next
97
+ # invocation of Reader#read_line or nil if the Reader should drop the line,
98
+ # advance to the next line and process it.
99
+ def process_line line
100
+ @look_ahead += 1 if @process_lines
101
+ line
102
+ end
103
+
104
+ # Public: Check whether there are any lines left to read.
105
+ #
106
+ # If a previous call to this method resulted in a value of false,
107
+ # immediately returned the cached value. Otherwise, delegate to
108
+ # peek_line to determine if there is a next line available.
109
+ #
110
+ # Returns True if there are more lines, False if there are not.
111
+ def has_more_lines?
112
+ !(@eof || (@eof = peek_line.nil?))
113
+ end
114
+
115
+ # Public: Peek at the next line and check if it's empty (i.e., whitespace only)
116
+ #
117
+ # This method Does not consume the line from the stack.
118
+ #
119
+ # Returns True if the there are no more lines or if the next line is empty
120
+ def next_line_empty?
121
+ (line = peek_line).nil? || line.chomp.empty?
122
+ end
123
+
124
+ # Public: Peek at the next line of source data. Processes the line, if not
125
+ # already marked as processed, but does not consume it.
126
+ #
127
+ # This method will probe the reader for more lines. If there is a next line
128
+ # that has not previously been visited, the line is passed to the
129
+ # Reader#preprocess_line method to be initialized. This call gives
130
+ # sub-classess the opportunity to do preprocessing. If the return value of
131
+ # the Reader#process_line is nil, the data is assumed to be changed and
132
+ # Reader#peek_line is invoked again to perform further processing.
133
+ #
134
+ # direct - A Boolean flag to bypasses the check for more lines and immediately
135
+ # returns the first element of the internal @lines Array. (default: false)
136
+ #
137
+ # Returns the next line of the source data as a String if there are lines remaining.
138
+ # Returns nil if there is no more data.
139
+ def peek_line direct = false
140
+ if direct || @look_ahead > 0
141
+ @unescape_next_line ? @lines.first[1..-1] : @lines.first
142
+ elsif @eof || @lines.empty?
143
+ @eof = true
144
+ @look_ahead = 0
145
+ nil
41
146
  else
42
- @lines = []
43
- @preprocess_source = false
147
+ # FIXME the problem with this approach is that we aren't
148
+ # retaining the modified line (hence the @unescape_next_line tweak)
149
+ # perhaps we need a stack of proxy lines
150
+ if (line = process_line @lines.first).nil?
151
+ peek_line
152
+ else
153
+ line
154
+ end
44
155
  end
156
+ end
45
157
 
46
- @source = @lines.dup
158
+ # Public: Peek at the next multiple lines of source data. Processes the lines, if not
159
+ # already marked as processed, but does not consume them.
160
+ #
161
+ # This method delegates to Reader#read_line to process and collect the line, then
162
+ # restores the lines to the stack before returning them. This allows the lines to
163
+ # be processed and marked as such so that subsequent reads will not need to process
164
+ # the lines again.
165
+ #
166
+ # num - The Integer number of lines to peek.
167
+ # direct - A Boolean indicating whether processing should be disabled when reading lines
168
+ #
169
+ # Returns A String Array of the next multiple lines of source data, or an empty Array
170
+ # if there are no more lines in this Reader.
171
+ def peek_lines num = 1, direct = true
172
+ old_look_ahead = @look_ahead
173
+ result = []
174
+ (1..num).each do
175
+ if (line = read_line direct)
176
+ result << line
177
+ else
178
+ break
179
+ end
180
+ end
47
181
 
48
- @next_line_preprocessed = false
49
- @unescape_next_line = false
182
+ unless result.empty?
183
+ result.reverse_each {|line| unshift line }
184
+ @look_ahead = old_look_ahead if direct
185
+ end
50
186
 
51
- @conditionals_stack = []
52
- @skipping = false
53
- @eof = false
187
+ result
54
188
  end
55
189
 
56
- # Public: Get a copy of the remaining Array of String lines parsed from the source
57
- def lines
58
- @lines.nil? ? nil : @lines.dup
190
+ # Public: Get the next line of source data. Consumes the line returned.
191
+ #
192
+ # direct - A Boolean flag to bypasses the check for more lines and immediately
193
+ # returns the first element of the internal @lines Array. (default: false)
194
+ #
195
+ # Returns the String of the next line of the source data if data is present.
196
+ # Returns nil if there is no more data.
197
+ def read_line direct = false
198
+ if direct || @look_ahead > 0 || has_more_lines?
199
+ shift
200
+ else
201
+ nil
202
+ end
59
203
  end
60
204
 
61
- # Public: Check whether there are any lines left to read.
205
+ # Public: Get the remaining lines of source data.
62
206
  #
63
- # If preprocessing is enabled for this Reader, and there are lines remaining,
64
- # the next line is preprocessed before checking whether there are more lines.
207
+ # This method calls Reader#read_line repeatedly until all lines are consumed
208
+ # and returns the lines as a String Array. This method differs from
209
+ # Reader#lines in that it processes each line in turn, hence triggering
210
+ # any preprocessors implemented in sub-classes.
65
211
  #
66
- # Returns true if @lines is empty, or false otherwise.
67
- def has_more_lines?
68
- if @eof || (@eof = @lines.empty?)
69
- false
70
- elsif @preprocess_source && !@next_line_preprocessed
71
- preprocess_next_line.nil? ? false : !@lines.empty?
72
- else
73
- true
212
+ # Returns the lines read as a String Array
213
+ def read_lines
214
+ lines = []
215
+ while has_more_lines?
216
+ lines << read_line
74
217
  end
218
+ lines
75
219
  end
220
+ alias :readlines :read_lines
76
221
 
77
- # Public: Check whether this reader is empty (contains no lines)
222
+ # Public: Get the remaining lines of source data joined as a String.
78
223
  #
79
- # If preprocessing is enabled for this Reader, and there are lines remaining,
80
- # the next line is preprocessed before checking whether there are more lines.
224
+ # Delegates to Reader#read_lines, then joins the result.
81
225
  #
82
- # Returns true if @lines is empty, otherwise false.
83
- def empty?
84
- !has_more_lines?
226
+ # Returns the lines read joined as a String
227
+ def read
228
+ read_lines.join
229
+ end
230
+
231
+ # Public: Advance to the next line by discarding the line at the front of the stack
232
+ #
233
+ # direct - A Boolean flag to bypasses the check for more lines and immediately
234
+ # returns the first element of the internal @lines Array. (default: true)
235
+ #
236
+ # returns a Boolean indicating whether there was a line to discard.
237
+ def advance direct = true
238
+ !(read_line direct).nil?
239
+ end
240
+
241
+ # Public: Push the String line onto the beginning of the Array of source data.
242
+ #
243
+ # Since this line was (assumed to be) previously retrieved through the
244
+ # reader, it is marked as seen.
245
+ #
246
+ # returns nil
247
+ def unshift_line line_to_restore
248
+ unshift line_to_restore
249
+ nil
250
+ end
251
+ alias :restore_line :unshift_line
252
+
253
+ # Public: Push an Array of lines onto the front of the Array of source data.
254
+ #
255
+ # Since these lines were (assumed to be) previously retrieved through the
256
+ # reader, they are marked as seen.
257
+ #
258
+ # Returns nil
259
+ def unshift_lines lines_to_restore
260
+ # QUESTION is it faster to use unshift(*lines_to_restore)?
261
+ lines_to_restore.reverse_each {|line| unshift line }
262
+ nil
85
263
  end
264
+ alias :restore_lines :unshift_lines
86
265
 
87
- # Private: Strip off leading blank lines in the Array of lines.
266
+ # Public: Replace the current line with the specified line.
267
+ #
268
+ # Calls Reader#advance to consume the current line, then calls
269
+ # Reader#unshift to push the replacement onto the top of the
270
+ # line stack.
271
+ #
272
+ # replacement - The String line to put in place of the line at the cursor.
273
+ #
274
+ # Returns nothing.
275
+ def replace_line replacement
276
+ advance
277
+ unshift replacement
278
+ nil
279
+ end
280
+
281
+ # Public: Strip off leading blank lines in the Array of lines.
88
282
  #
89
283
  # Examples
90
284
  #
@@ -99,76 +293,66 @@ class Reader
99
293
  #
100
294
  # Returns an Integer of the number of lines skipped
101
295
  def skip_blank_lines
102
- skipped = 0
296
+ return 0 if eof?
297
+
298
+ num_skipped = 0
103
299
  # optimized code for shortest execution path
104
- while !(next_line = get_line).nil?
300
+ while (next_line = peek_line)
105
301
  if next_line.chomp.empty?
106
- skipped += 1
302
+ advance
303
+ num_skipped += 1
107
304
  else
108
- unshift_line next_line
109
- break
305
+ return num_skipped
110
306
  end
111
- end
307
+ end
112
308
 
113
- skipped
309
+ num_skipped
114
310
  end
115
311
 
116
- # Public: Consume consecutive lines containing line- or block-level comments.
117
- #
118
- # Returns the Array of lines that were consumed
312
+ # Public: Skip consecutive lines containing line comments and return them.
119
313
  #
120
314
  # Examples
121
315
  # @lines
122
- # => ["// foo\n", "////\n", "foo bar\n", "////\n", "actual text\n"]
316
+ # => ["// foo\n", "bar\n"]
123
317
  #
124
- # comment_lines = consume_comments
125
- # => ["// foo\n", "////\n", "foo bar\n", "////\n"]
318
+ # comment_lines = skip_comment_lines
319
+ # => ["// foo\n"]
126
320
  #
127
321
  # @lines
128
- # => ["actual text\n"]
129
- def consume_comments(options = {})
322
+ # => ["bar\n"]
323
+ #
324
+ # Returns the Array of lines that were skipped
325
+ def skip_comment_lines opts = {}
326
+ return [] if eof?
327
+
130
328
  comment_lines = []
131
- preprocess = options.fetch(:preprocess, true)
132
- while !(next_line = get_line(preprocess)).nil?
133
- if options[:include_blank_lines] && next_line.chomp.empty?
134
- comment_lines << next_line
329
+ include_blank_lines = opts[:include_blank_lines]
330
+ while (next_line = peek_line)
331
+ if include_blank_lines && next_line.chomp.empty?
332
+ comment_lines << read_line
135
333
  elsif (commentish = next_line.start_with?('//')) && (match = next_line.match(REGEXP[:comment_blk]))
136
- comment_lines << next_line
137
- comment_lines.push(*(grab_lines_until(:terminator => match[0], :grab_last_line => true, :preprocess => false)))
334
+ comment_lines << read_line
335
+ comment_lines.push(*(read_lines_until(:terminator => match[0], :read_last_line => true, :skip_processing => true)))
138
336
  elsif commentish && next_line.match(REGEXP[:comment])
139
- comment_lines << next_line
337
+ comment_lines << read_line
140
338
  else
141
- # throw it back
142
- unshift_line next_line
143
339
  break
144
340
  end
145
341
  end
146
342
 
147
343
  comment_lines
148
344
  end
149
- alias :skip_comment_lines :consume_comments
150
345
 
151
- # Public: Consume consecutive lines containing line comments.
152
- #
153
- # Returns the Array of lines that were consumed
154
- #
155
- # Examples
156
- # @lines
157
- # => ["// foo\n", "bar\n"]
158
- #
159
- # comment_lines = consume_comments
160
- # => ["// foo\n"]
161
- #
162
- # @lines
163
- # => ["bar\n"]
164
- def consume_line_comments
346
+ # Public: Skip consecutive lines that are line comments and return them.
347
+ def skip_line_comments
348
+ return [] if eof?
349
+
165
350
  comment_lines = []
166
351
  # optimized code for shortest execution path
167
- while !(next_line = get_line).nil?
352
+ while (next_line = peek_line)
168
353
  if next_line.match(REGEXP[:comment])
169
- comment_lines << next_line
354
+ comment_lines << read_line
170
355
  else
171
- unshift_line next_line
172
356
  break
173
357
  end
174
358
  end
@@ -176,122 +360,302 @@ class Reader
176
360
  comment_lines
177
361
  end
178
362
 
179
- # Public: Get the next line of source data. Consumes the line returned.
363
+ # Public: Advance to the end of the reader, consuming all remaining lines
180
364
  #
181
- # preprocess - A Boolean flag indicating whether to evaluate preprocessing
182
- # directives (macros) before reading line (default: true)
365
+ # Returns nothing.
366
+ def terminate
367
+ @lineno += @lines.size
368
+ @lines.clear
369
+ @eof = true
370
+ @look_ahead = 0
371
+ nil
372
+ end
373
+
374
+ # Public: Check whether this reader is empty (contains no lines)
183
375
  #
184
- # Returns the String of the next line of the source data if data is present.
185
- # Returns nil if there is no more data.
186
- def get_line(preprocess = true)
187
- if @eof || (@eof = @lines.empty?)
188
- @next_line_preprocessed = true
189
- nil
190
- elsif preprocess && @preprocess_source &&
191
- !@next_line_preprocessed && preprocess_next_line.nil?
192
- @next_line_preprocessed = true
193
- nil
376
+ # Returns true if there are no more lines to peek, otherwise false.
377
+ def eof?
378
+ !has_more_lines?
379
+ end
380
+ alias :empty? :eof?
381
+
382
+ # Public: Return all the lines from `@lines` until we (1) run out them,
383
+ # (2) find a blank line with :break_on_blank_lines => true, or (3) find
384
+ # a line for which the given block evals to true.
385
+ #
386
+ # options - an optional Hash of processing options:
387
+ # * :break_on_blank_lines may be used to specify to break on
388
+ # blank lines
389
+ # * :skip_first_line may be used to tell the reader to advance
390
+ # beyond the first line before beginning the scan
391
+ # * :preserve_last_line may be used to specify that the String
392
+ # causing the method to stop processing lines should be
393
+ # pushed back onto the `lines` Array.
394
+ # * :read_last_line may be used to specify that the String
395
+ # causing the method to stop processing lines should be
396
+ # included in the lines being returned
397
+ #
398
+ # Returns the Array of lines forming the next segment.
399
+ #
400
+ # Examples
401
+ #
402
+ # reader = Reader.new ["First paragraph\n", "Second paragraph\n",
403
+ # "Open block\n", "\n", "Can have blank lines\n",
404
+ # "--\n", "\n", "In a different segment\n"]
405
+ #
406
+ # reader.read_lines_until
407
+ # => ["First paragraph\n", "Second paragraph\n", "Open block\n"]
408
+ def read_lines_until options = {}
409
+ result = []
410
+ advance if options[:skip_first_line]
411
+ if @process_lines && options[:skip_processing]
412
+ @process_lines = false
413
+ restore_process_lines = true
194
414
  else
195
- @lineno += 1
196
- @next_line_preprocessed = false
197
- if @unescape_next_line
198
- @unescape_next_line = false
199
- @lines.shift[1..-1]
200
- else
201
- @lines.shift
415
+ restore_process_lines = false
416
+ end
417
+
418
+ has_block = block_given?
419
+ if (terminator = options[:terminator])
420
+ break_on_blank_lines = false
421
+ break_on_list_continuation = false
422
+ chomp_last_line = options.fetch :chomp_last_line, false
423
+ else
424
+ break_on_blank_lines = options[:break_on_blank_lines]
425
+ break_on_list_continuation = options[:break_on_list_continuation]
426
+ chomp_last_line = break_on_blank_lines
427
+ end
428
+ skip_line_comments = options[:skip_line_comments]
429
+ line_read = false
430
+ line_restored = false
431
+
432
+ while (line = read_line)
433
+ finish = while true
434
+ break true if terminator && line.chomp == terminator
435
+ # QUESTION: can we get away with line.chomp.empty? here?
436
+ break true if break_on_blank_lines && line.chomp.empty?
437
+ if break_on_list_continuation && line_read && line.chomp == LIST_CONTINUATION
438
+ options[:preserve_last_line] = true
439
+ break true
440
+ end
441
+ break true if has_block && (yield line)
442
+ break false
443
+ end
444
+
445
+ if finish
446
+ if options[:read_last_line]
447
+ result << line
448
+ line_read = true
449
+ end
450
+ if options[:preserve_last_line]
451
+ restore_line line
452
+ line_restored = true
453
+ end
454
+ break
455
+ end
456
+
457
+ unless skip_line_comments && line.start_with?('//') && line.match(REGEXP[:comment])
458
+ result << line
459
+ line_read = true
202
460
  end
203
461
  end
462
+
463
+ if chomp_last_line && line_read
464
+ result << result.pop.chomp
465
+ end
466
+
467
+ if restore_process_lines
468
+ @process_lines = true
469
+ @look_ahead -= 1 if line_restored && terminator.nil?
470
+ end
471
+ result
204
472
  end
205
473
 
206
- # Public: Advance to the next line by discarding the line at the front of the stack
474
+ # Internal: Shift the line off the stack and increment the lineno
475
+ def shift
476
+ @lineno += 1
477
+ @look_ahead -= 1 unless @look_ahead == 0
478
+ @lines.shift
479
+ end
480
+
481
+ # Internal: Restore the line to the stack and decrement the lineno
482
+ def unshift line
483
+ @lineno -= 1
484
+ @look_ahead += 1
485
+ @eof = false
486
+ @lines.unshift line
487
+ end
488
+
489
+ def cursor
490
+ Cursor.new @file, @dir, @path, @lineno
491
+ end
492
+
493
+ # Public: Get information about the last line read, including file name and line number.
207
494
  #
208
- # Removes the line at the front of the stack without any processing.
495
+ # Returns A String summary of the last line read
496
+ def line_info
497
+ %(#{@path}: line #{@lineno})
498
+ end
499
+ alias :next_line_info :line_info
500
+
501
+ def prev_line_info
502
+ %(#{@path}: line #{@lineno - 1})
503
+ end
504
+
505
+ # Public: Get a copy of the remaining Array of String lines managed by this Reader
209
506
  #
210
- # returns a boolean indicating whether there was a line to discard
211
- def advance
212
- @next_line_preprocessed = false
213
- # we assume that we're advancing over a line of known content
214
- if @eof || (@eof = @lines.empty?)
215
- false
216
- else
217
- @lineno += 1
218
- @lines.shift
219
- true
220
- end
507
+ # Returns A copy of the String Array of lines remaining in this Reader
508
+ def lines
509
+ @lines.dup
221
510
  end
222
511
 
223
- # Public: Get the next line of source data. Does not consume the line returned.
512
+ # Public: Get a copy of the remaining lines managed by this Reader joined as a String
513
+ def string
514
+ @lines.join
515
+ end
516
+
517
+ # Public: Get the source lines for this Reader joined as a String
518
+ def source
519
+ @source_lines.join
520
+ end
521
+
522
+ # Public: Get a summary of this Reader.
224
523
  #
225
- # preprocess - A Boolean flag indicating whether to evaluate preprocessing
226
- # directives (macros) before reading line (default: true)
227
524
  #
228
- # Returns a String dup of the next line of the source data if data is present.
229
- # Returns nil if there is no more data.
230
- def peek_line(preprocess = true)
231
- if !preprocess
232
- # QUESTION do we need to dup?
233
- @eof || (@eof = @lines.empty?) ? nil : @lines.first.dup
234
- elsif has_more_lines?
235
- # QUESTION do we need to dup?
236
- @lines.first.dup
525
+ # Returns A string summary of this reader, which contains the path and line information
526
+ def to_s
527
+ line_info
528
+ end
529
+ end
530
+
531
+ # Public: Methods for retrieving lines from AsciiDoc source files, evaluating preprocessor
532
+ # directives as each line is read off the Array of lines.
533
+ class PreprocessorReader < Reader
534
+ attr_reader :include_stack
535
+ attr_reader :includes
536
+
537
+ # Public: Initialize the PreprocessorReader object
538
+ def initialize document, data = nil, cursor = nil
539
+ @document = document
540
+ super data, cursor
541
+ include_depth_default = document.attributes.fetch('max-include-depth', 64).to_i
542
+ include_depth_default = 0 if include_depth_default < 0
543
+ # track both absolute depth for comparing to size of include stack and relative depth for reporting
544
+ @maxdepth = {:abs => include_depth_default, :rel => include_depth_default}
545
+ @include_stack = []
546
+ @includes = (document.references[:includes] ||= [])
547
+ @skipping = false
548
+ @conditional_stack = []
549
+ @include_processors = nil
550
+ end
551
+
552
+ def prepare_lines data, opts = {}
553
+ if data.is_a?(String)
554
+ if ::Asciidoctor::FORCE_ENCODING
555
+ result = data.each_line.map {|line| "#{line.rstrip.force_encoding ::Encoding::UTF_8}#{::Asciidoctor::EOL}" }
556
+ else
557
+ result = data.each_line.map {|line| "#{line.rstrip}#{::Asciidoctor::EOL}" }
558
+ end
237
559
  else
238
- nil
560
+ if ::Asciidoctor::FORCE_ENCODING
561
+ result = data.map {|line| "#{line.rstrip.force_encoding ::Encoding::UTF_8}#{::Asciidoctor::EOL}" }
562
+ else
563
+ result = data.map {|line| "#{line.rstrip}#{::Asciidoctor::EOL}" }
564
+ end
239
565
  end
240
- end
241
566
 
242
- # TODO document & test me!
243
- def peek_lines(number = 1)
244
- lines = []
245
- idx = 0
246
- (1..number).each do
247
- if @preprocess_source && !@next_line_preprocessed
248
- advanced = preprocess_next_line
249
- break if advanced.nil? || @eof || (@eof = @lines.empty?)
250
- idx = 0 if advanced
567
+ # QUESTION should this work for AsciiDoc table cell content? Currently it does not.
568
+ unless @document.nil? || !(@document.attributes.has_key? 'skip-front-matter')
569
+ if (front_matter = skip_front_matter! result)
570
+ @document.attributes['front-matter'] = front_matter.join.chomp
251
571
  end
252
- break if idx >= @lines.size
253
- # QUESTION do we need to dup?
254
- lines << @lines[idx].dup
255
- idx += 1
256
572
  end
257
- lines
573
+
574
+ # QUESTION should we chomp last line? (with or without the condense flag?)
575
+ if opts.fetch(:condense, true)
576
+ result.shift && @lineno += 1 while !(first = result.first).nil? && first == ::Asciidoctor::EOL
577
+ result.pop while !(last = result.last).nil? && last == ::Asciidoctor::EOL
578
+ end
579
+
580
+ if (indent = opts.fetch(:indent, nil))
581
+ Lexer.reset_block_indent! result, indent.to_i
582
+ end
583
+
584
+ result
258
585
  end
259
586
 
260
- # Internal: Preprocess the next line until the cursor is at a line of content
261
- #
262
- # Evaluate preprocessor macros on the next line, continuing to do so until
263
- # the cursor arrives at a line of included content. That line is marked as
264
- # preprocessed so that preprocessing is not performed multiple times.
265
- #
266
- # returns a Boolean indicating whether the cursor advanced, or nil if there
267
- # are no more lines available.
268
- def preprocess_next_line
269
- # this return could be happening from a recursive call
270
- return nil if @eof || (next_line = @lines.first).nil?
271
- if next_line.include?('::') && (next_line.include?('if') || next_line.include?('endif')) && (match = next_line.match(REGEXP[:ifdef_macro]))
272
- if next_line.start_with? '\\'
273
- @next_line_preprocessed = true
587
+ def process_line line
588
+ return line unless @process_lines
589
+
590
+ if line.chomp.empty?
591
+ @look_ahead += 1
592
+ return ''
593
+ end
594
+
595
+ macroish = line.include?('::') && line.include?('[')
596
+ if macroish && line.include?('if') && (match = line.match(REGEXP[:ifdef_macro]))
597
+ # if escaped, mark as processed and return line unescaped
598
+ if line.start_with? '\\'
274
599
  @unescape_next_line = true
275
- false
600
+ @look_ahead += 1
601
+ line[1..-1]
276
602
  else
277
- preprocess_conditional_inclusion(*match.captures)
603
+ if preprocess_conditional_inclusion(*match.captures)
604
+ # move the pointer past the conditional line
605
+ advance
606
+ # treat next line as uncharted territory
607
+ nil
608
+ else
609
+ # the line was not a valid conditional line
610
+ # mark it as visited and return it
611
+ @look_ahead += 1
612
+ line
613
+ end
278
614
  end
279
615
  elsif @skipping
280
616
  advance
281
- # skip over comment blocks, we don't want to process directives in them
282
- skip_comment_lines :include_blank_lines => true, :preprocess => false
283
- preprocess_next_line.nil? ? nil : true
284
- elsif next_line.include?('include::') && match = next_line.match(REGEXP[:include_macro])
285
- if next_line.start_with? '\\'
286
- @next_line_preprocessed = true
617
+ nil
618
+ elsif macroish && line.include?('include::') && (match = line.match(REGEXP[:include_macro]))
619
+ # if escaped, mark as processed and return line unescaped
620
+ if line.start_with? '\\'
287
621
  @unescape_next_line = true
288
- false
622
+ @look_ahead += 1
623
+ line[1..-1]
289
624
  else
290
- preprocess_include(match[1], match[2].strip)
625
+ # QUESTION should we strip whitespace from raw attributes in Substituters#parse_attributes? (check perf)
626
+ if preprocess_include match[1], match[2].strip
627
+ # peek again since the content has changed
628
+ nil
629
+ else
630
+ # the line was not a valid include line and is unchanged
631
+ # mark it as visited and return it
632
+ @look_ahead += 1
633
+ line
634
+ end
291
635
  end
292
636
  else
293
- @next_line_preprocessed = true
294
- false
637
+ # optimization to inline super
638
+ #super
639
+ @look_ahead += 1
640
+ line
641
+ end
642
+ end
643
+
644
+ # Public: Override the Reader#peek_line method to pop the include
645
+ # stack if the last line has been reached and there's at least
646
+ # one include on the stack.
647
+ #
648
+ # Returns the next line of the source data as a String if there are lines remaining
649
+ # in the current include context or a parent include context.
650
+ # Returns nil if there are no more lines remaining and the include stack is empty.
651
+ def peek_line direct = false
652
+ if (line = super)
653
+ line
654
+ elsif @include_stack.empty?
655
+ nil
656
+ else
657
+ pop_include
658
+ peek_line direct
295
659
  end
296
660
  end
297
661
 
@@ -314,37 +678,36 @@ class Reader
314
678
  # Used for a single-line conditional block in the case of the ifdef or
315
679
  # ifndef directives, and for the conditional expression for the ifeval directive.
316
680
  #
317
- # returns a Boolean indicating whether the cursor advanced, or nil if there
318
- # are no more lines available.
319
- def preprocess_conditional_inclusion(directive, target, delimiter, text)
681
+ # returns a Boolean indicating whether the cursor should be advanced
682
+ def preprocess_conditional_inclusion directive, target, delimiter, text
320
683
  # must have a target before brackets if ifdef or ifndef
321
684
  # must not have text between brackets if endif
322
685
  # don't honor match if it doesn't meet this criteria
686
+ # QUESTION should we warn for these bogus declarations?
323
687
  if ((directive == 'ifdef' || directive == 'ifndef') && target.empty?) ||
324
688
  (directive == 'endif' && !text.nil?)
325
- @next_line_preprocessed = true
326
689
  return false
327
690
  end
328
691
 
329
692
  if directive == 'endif'
330
- stack_size = @conditionals_stack.size
693
+ stack_size = @conditional_stack.size
331
694
  if stack_size > 0
332
- pair = @conditionals_stack.last
695
+ pair = @conditional_stack.last
333
696
  if target.empty? || target == pair[:target]
334
- @conditionals_stack.pop
335
- @skipping = @conditionals_stack.empty? ? false : @conditionals_stack.last[:skipping]
697
+ @conditional_stack.pop
698
+ @skipping = @conditional_stack.empty? ? false : @conditional_stack.last[:skipping]
336
699
  else
337
- puts "asciidoctor: ERROR: line #{@lineno + 1}: mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[]"
700
+ warn "asciidoctor: ERROR: #{line_info}: mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[]"
338
701
  end
339
702
  else
340
- puts "asciidoctor: ERROR: line #{@lineno + 1}: unmatched macro: endif::#{target}[]"
703
+ warn "asciidoctor: ERROR: #{line_info}: unmatched macro: endif::#{target}[]"
341
704
  end
342
- advance
343
- return preprocess_next_line.nil? ? nil : true
705
+ return true
344
706
  end
345
707
 
346
708
  skip = false
347
- if !@skipping
709
+ unless @skipping
710
+ # QUESTION any way to wrap ifdef & ifndef logic up together?
348
711
  case directive
349
712
  when 'ifdef'
350
713
  case delimiter
@@ -374,32 +737,35 @@ class Reader
374
737
  # the text in brackets must match an expression
375
738
  # don't honor match if it doesn't meet this criteria
376
739
  if !target.empty? || !(expr_match = text.strip.match(REGEXP[:eval_expr]))
377
- @next_line_preprocessed = true
378
740
  return false
379
741
  end
380
742
 
381
- lhs = resolve_expr_val(expr_match[1])
743
+ lhs = resolve_expr_val expr_match[1]
744
+ # regex enforces a restrict set of math-related operations
382
745
  op = expr_match[2]
383
- rhs = resolve_expr_val(expr_match[3])
746
+ rhs = resolve_expr_val expr_match[3]
384
747
 
385
- skip = !lhs.send(op.to_sym, rhs)
748
+ skip = !(lhs.send op.to_sym, rhs)
386
749
  end
387
750
  end
388
- advance
389
- # single line conditional inclusion
390
- if directive != 'ifeval' && !text.nil?
391
- if !@skipping && !skip
392
- unshift_line "#{text.rstrip}\n"
393
- return true
394
- end
751
+
395
752
  # conditional inclusion block
753
+ if directive == 'ifeval' || text.nil?
754
+ @skipping = true if skip
755
+ @conditional_stack << {:target => target, :skip => skip, :skipping => @skipping}
756
+ # single line conditional inclusion
396
757
  else
397
- if !@skipping && skip
398
- @skipping = true
758
+ unless @skipping || skip
759
+ # FIXME slight hack to skip past conditional line
760
+ # but keep our synthetic line marked as processed
761
+ conditional_line = peek_line true
762
+ replace_line "#{text.rstrip}#{::Asciidoctor::EOL}"
763
+ unshift conditional_line
764
+ return true
399
765
  end
400
- @conditionals_stack << {:target => target, :skip => skip, :skipping => @skipping}
401
766
  end
402
- return preprocess_next_line.nil? ? nil : true
767
+
768
+ true
403
769
  end
404
770
 
405
771
  # Internal: Preprocess the directive (macro) to include the target document.
@@ -410,13 +776,12 @@ class Reader
410
776
  # If SafeMode is SECURE or greater, the directive is ignore and the include
411
777
  # directive line is emitted verbatim.
412
778
  #
413
- # Otherwise, if an include handler is specified (currently controlled by a
414
- # closure block), pass the target to that block and expect an Array of String
415
- # lines in return.
779
+ # Otherwise, if an include processor is specified pass the target and
780
+ # attributes to that processor and expect an Array of String lines in return.
416
781
  #
417
- # Otherwise, if the include-depth attribute is greater than 0, normalize the
418
- # target path and read the lines onto the beginning of the Array of source
419
- # data.
782
+ # Otherwise, if the max depth is greater than 0, and is not exceeded by the
783
+ # stack size, normalize the target path and read the lines onto the beginning
784
+ # of the Array of source data.
420
785
  #
421
786
  # If none of the above apply, emit the include directive line verbatim.
422
787
  #
@@ -424,39 +789,68 @@ class Reader
424
789
  # target slot of the include::[] macro
425
790
  #
426
791
  # returns a Boolean indicating whether the line under the cursor has changed.
427
- def preprocess_include(target, raw_attributes)
428
- target = @document.sub_attributes target
792
+ def preprocess_include target, raw_attributes
793
+ target = @document.sub_attributes target, :attribute_missing => 'drop-line'
429
794
  if target.empty?
795
+ if @document.attributes.fetch('attribute-missing', COMPLIANCE[:attribute_missing]) == 'skip'
796
+ false
797
+ else
798
+ advance
799
+ true
800
+ end
801
+ # assume that if an include processor is given, the developer wants
802
+ # to handle when and how to process the include
803
+ elsif include_processors? &&
804
+ (processor = @include_processors.find {|candidate| candidate.handles? target })
430
805
  advance
431
- @next_line_preprocessed = false
432
- false
806
+ # QUESTION should we use @document.parse_attribues?
807
+ processor.process self, target, AttributeList.new(raw_attributes).parse
808
+ true
433
809
  # if running in SafeMode::SECURE or greater, don't process this directive
434
810
  # however, be friendly and at least make it a link to the source document
435
811
  elsif @document.safe >= SafeMode::SECURE
436
- @lines[0] = "link:#{target}[#{target}]\n"
437
- @next_line_preprocessed = true
812
+ replace_line "link:#{target}[]#{::Asciidoctor::EOL}"
813
+ # TODO make creating the output target a helper method
814
+ #output_target = %(#{File.join(File.dirname(target), File.basename(target, File.extname(target)))}#{@document.attributes['outfilesuffix']})
815
+ #unshift "link:#{output_target}[]#{::Asciidoctor::EOL}"
816
+ true
817
+ elsif (abs_maxdepth = @maxdepth[:abs]) > 0 && @include_stack.size >= abs_maxdepth
818
+ warn %(asciidoctor: ERROR: #{line_info}: maximum include depth of #{@maxdepth[:rel]} exceeded)
438
819
  false
439
- # assume that if a block is given, the developer wants
440
- # to handle when and how to process the include, even
441
- # if the include-depth attribute is 0
442
- elsif @include_block
443
- advance
444
- # FIXME this borks line numbers
445
- @lines.unshift(*normalize_include_data(@include_block.call(target)))
446
- # FIXME currently we're not checking the upper bound of the include depth
447
- elsif @document.attributes.fetch('include-depth', 0).to_i > 0
448
- advance
449
- # FIXME this borks line numbers
450
- include_file = @document.normalize_system_path(target, nil, nil, :target_name => 'include file')
451
- if !File.file?(include_file)
452
- puts "asciidoctor: WARNING: line #{@lineno}: include file not found: #{include_file}"
453
- return true
820
+ elsif abs_maxdepth > 0
821
+ if target.include?(':') && target.match(REGEXP[:uri_sniff])
822
+ unless @document.attributes.has_key? 'allow-uri-read'
823
+ replace_line "link:#{target}[]#{::Asciidoctor::EOL}"
824
+ return true
825
+ end
826
+
827
+ target_type = :uri
828
+ include_file = path = target
829
+ if @document.attributes.has_key? 'cache-uri'
830
+ # caching requires the open-uri-cached gem to be installed
831
+ # processing will be automatically aborted if these libraries can't be opened
832
+ Helpers.require_library 'open-uri/cached', 'open-uri-cached'
833
+ else
834
+ Helpers.require_library 'open-uri'
835
+ end
836
+ else
837
+ target_type = :file
838
+ # include file is resolved relative to dir of current include, or base_dir if within original docfile
839
+ include_file = @document.normalize_system_path(target, @dir, nil, :target_name => 'include file')
840
+ if !File.file?(include_file)
841
+ warn "asciidoctor: WARNING: #{line_info}: include file not found: #{include_file}"
842
+ advance
843
+ return true
844
+ end
845
+ #path = @document.relative_path include_file
846
+ path = PathResolver.new.relative_path include_file, @document.base_dir
454
847
  end
455
848
 
456
849
  inc_lines = nil
457
850
  tags = nil
458
851
  attributes = {}
459
852
  if !raw_attributes.empty?
853
+ # QUESTION should we use @document.parse_attribues?
460
854
  attributes = AttributeList.new(raw_attributes).parse
461
855
  if attributes.has_key? 'lines'
462
856
  inc_lines = []
@@ -474,6 +868,8 @@ class Reader
474
868
  end
475
869
  end
476
870
  inc_lines = inc_lines.sort.uniq
871
+ elsif attributes.has_key? 'tag'
872
+ tags = [attributes['tag']]
477
873
  elsif attributes.has_key? 'tags'
478
874
  tags = attributes['tags'].split(REGEXP[:ssv_or_csv_delim]).uniq
479
875
  end
@@ -481,213 +877,227 @@ class Reader
481
877
  if !inc_lines.nil?
482
878
  if !inc_lines.empty?
483
879
  selected = []
484
- f = File.new(include_file)
485
- f.each_line do |l|
486
- take = inc_lines.first
487
- if take.is_a?(Float) && take.infinite?
488
- selected.push l
489
- else
490
- if f.lineno == take
491
- selected.push l
492
- inc_lines.shift
880
+ inc_line_offset = 0
881
+ inc_lineno = 0
882
+ begin
883
+ open(include_file) do |f|
884
+ f.each_line do |l|
885
+ inc_lineno += 1
886
+ take = inc_lines.first
887
+ if take.is_a?(Float) && take.infinite?
888
+ selected.push l
889
+ inc_line_offset = inc_lineno if inc_line_offset == 0
890
+ else
891
+ if f.lineno == take
892
+ selected.push l
893
+ inc_line_offset = inc_lineno if inc_line_offset == 0
894
+ inc_lines.shift
895
+ end
896
+ break if inc_lines.empty?
897
+ end
493
898
  end
494
- break if inc_lines.empty?
495
899
  end
900
+ rescue
901
+ warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}"
902
+ advance
903
+ return true
496
904
  end
497
- @lines.unshift(*normalize_include_data(selected, attributes['indent'])) unless selected.empty?
905
+ advance
906
+ # FIXME not accounting for skipped lines in reader line numbering
907
+ push_include selected, include_file, path, inc_line_offset, attributes
498
908
  end
499
909
  elsif !tags.nil?
500
910
  if !tags.empty?
501
911
  selected = []
912
+ inc_line_offset = 0
913
+ inc_lineno = 0
502
914
  active_tag = nil
503
- f = File.new(include_file)
504
- f.each_line do |l|
505
- l.force_encoding(::Encoding::UTF_8) if ::Asciidoctor::FORCE_ENCODING
506
- if !active_tag.nil?
507
- if l.include?("end::#{active_tag}[]")
508
- active_tag = nil
509
- else
510
- selected.push "#{l.rstrip}\n"
511
- end
512
- else
513
- tags.each do |tag|
514
- if l.include?("tag::#{tag}[]")
515
- active_tag = tag
516
- break
915
+ begin
916
+ open(include_file) do |f|
917
+ f.each_line do |l|
918
+ inc_lineno += 1
919
+ # must force encoding here since we're performing String operations on line
920
+ l.force_encoding(::Encoding::UTF_8) if ::Asciidoctor::FORCE_ENCODING
921
+ if !active_tag.nil?
922
+ if l.include?("end::#{active_tag}[]")
923
+ active_tag = nil
924
+ else
925
+ selected.push l
926
+ inc_line_offset = inc_lineno if inc_line_offset == 0
927
+ end
928
+ else
929
+ tags.each do |tag|
930
+ if l.include?("tag::#{tag}[]")
931
+ active_tag = tag
932
+ break
933
+ end
934
+ end
517
935
  end
518
936
  end
519
937
  end
938
+ rescue
939
+ warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}"
940
+ advance
941
+ return true
520
942
  end
521
- #@lines.unshift(*selected) unless selected.empty?
522
- @lines.unshift(*normalize_include_data(selected, attributes['indent'])) unless selected.empty?
943
+ advance
944
+ # FIXME not accounting for skipped lines in reader line numbering
945
+ push_include selected, include_file, path, inc_line_offset, attributes
523
946
  end
524
947
  else
525
- @lines.unshift(*normalize_include_data(File.readlines(include_file), attributes['indent']))
948
+ begin
949
+ advance
950
+ push_include open(include_file) {|f| f.read }, include_file, path, 1, attributes
951
+ rescue
952
+ warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}"
953
+ advance
954
+ return true
955
+ end
526
956
  end
527
957
  true
528
958
  else
529
- @next_line_preprocessed = true
530
959
  false
531
960
  end
532
961
  end
533
962
 
534
- # Public: Push the String line onto the beginning of the Array of source data.
535
- #
536
- # Since this line was (assumed to be) previously retrieved through the
537
- # reader, it is marked as preprocessed.
538
- #
539
- # returns nil
540
- def unshift_line(line)
541
- @lines.unshift line
542
- @next_line_preprocessed = true
543
- @eof = false
544
- @lineno -= 1
545
- nil
963
+ def push_include data, file = nil, path = nil, lineno = 1, attributes = {}
964
+ @include_stack << [@lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines]
965
+ @includes << Helpers.rootname(path)
966
+ @file = file
967
+ @dir = File.dirname file
968
+ @path = path
969
+ @lineno = lineno
970
+ # NOTE only process lines in AsciiDoc files
971
+ @process_lines = ASCIIDOC_EXTENSIONS[File.extname(@file)]
972
+ if attributes.has_key? 'depth'
973
+ depth = attributes['depth'].to_i
974
+ depth = 1 if depth <= 0
975
+ @maxdepth = {:abs => (@include_stack.size - 1) + depth, :rel => depth}
976
+ end
977
+ # effectively fill the buffer
978
+ @lines = prepare_lines data, :condense => false, :indent => attributes['indent']
979
+ # FIXME kind of a hack
980
+ #Document::AttributeEntry.new('infile', @file).save_to_next_block @document
981
+ #Document::AttributeEntry.new('indir', File.dirname(@file)).save_to_next_block @document
982
+ if @lines.empty?
983
+ pop_include
984
+ else
985
+ @eof = false
986
+ @look_ahead = 0
987
+ end
546
988
  end
547
989
 
548
- # Public: Push Array of lines onto the front of the Array of source data, unless `lines` has no non-nil values.
549
- #
550
- # Returns nil
551
- def unshift(*new_lines)
552
- size = new_lines.size
553
- if size > 0
554
- @lines.unshift(*new_lines)
555
- # assume that what we are putting back on is already processed for directives
556
- @next_line_preprocessed = true
557
- @eof = false
558
- @lineno -= size
990
+ def pop_include
991
+ if @include_stack.size > 0
992
+ @lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines = @include_stack.pop
993
+ # FIXME kind of a hack
994
+ #Document::AttributeEntry.new('infile', @file).save_to_next_block @document
995
+ #Document::AttributeEntry.new('indir', File.dirname(@file)).save_to_next_block @document
996
+ @eof = @lines.empty?
997
+ @look_ahead = 0
559
998
  end
560
- nil
561
999
  end
562
1000
 
563
- # Public: Chomp the String on the last line if this reader contains at least one line
564
- #
565
- # Delegates to chomp!
566
- #
567
- # Returns nil
568
- def chomp_last!
569
- @lines.last.chomp! unless @eof || (@eof = @lines.empty?)
570
- nil
1001
+ def include_depth
1002
+ @include_stack.size
571
1003
  end
572
1004
 
573
- # Public: Return all the lines from `@lines` until we (1) run out them,
574
- # (2) find a blank line with :break_on_blank_lines => true, or (3) find
575
- # a line for which the given block evals to true.
576
- #
577
- # options - an optional Hash of processing options:
578
- # * :break_on_blank_lines may be used to specify to break on
579
- # blank lines
580
- # * :skip_first_line may be used to tell the reader to advance
581
- # beyond the first line before beginning the scan
582
- # * :preserve_last_line may be used to specify that the String
583
- # causing the method to stop processing lines should be
584
- # pushed back onto the `lines` Array.
585
- # * :grab_last_line may be used to specify that the String
586
- # causing the method to stop processing lines should be
587
- # included in the lines being returned
588
- #
589
- # Returns the Array of lines forming the next segment.
590
- #
591
- # Examples
592
- #
593
- # reader = Reader.new ["First paragraph\n", "Second paragraph\n",
594
- # "Open block\n", "\n", "Can have blank lines\n",
595
- # "--\n", "\n", "In a different segment\n"]
596
- #
597
- # reader.grab_lines_until
598
- # => ["First paragraph\n", "Second paragraph\n", "Open block\n"]
599
- def grab_lines_until(options = {}, &block)
600
- buffer = []
1005
+ def exceeded_max_depth?
1006
+ if (abs_maxdepth = @maxdepth[:abs]) > 0 && @include_stack.size >= abs_maxdepth
1007
+ @maxdepth[:rel]
1008
+ else
1009
+ false
1010
+ end
1011
+ end
601
1012
 
602
- advance if options[:skip_first_line]
603
- # very hot code
604
- # save options to locals for minor optimizations
605
- if options.has_key? :terminator
606
- terminator = options[:terminator]
607
- break_on_blank_lines = false
608
- break_on_list_continuation = false
609
- chomp_last_line = options[:chomp_last_line] || false
1013
+ # TODO Document this override
1014
+ # also, we now have the field in the super class, so perhaps
1015
+ # just implement the logic there?
1016
+ def shift
1017
+ if @unescape_next_line
1018
+ @unescape_next_line = false
1019
+ super[1..-1]
610
1020
  else
611
- terminator = nil
612
- break_on_blank_lines = options[:break_on_blank_lines]
613
- break_on_list_continuation = options[:break_on_list_continuation]
614
- chomp_last_line = break_on_blank_lines
1021
+ super
615
1022
  end
616
- skip_line_comments = options[:skip_line_comments]
617
- preprocess = options.fetch(:preprocess, true)
618
- buffer_empty = true
619
- while !(this_line = get_line(preprocess)).nil?
620
- # effectively a no-args lamba, but much faster
621
- finish = while true
622
- break true if terminator && this_line.chomp == terminator
623
- break true if break_on_blank_lines && this_line.strip.empty?
624
- if break_on_list_continuation && !buffer_empty && this_line.chomp == LIST_CONTINUATION
625
- options[:preserve_last_line] = true
626
- break true
627
- end
628
- break true if block && yield(this_line)
629
- break false
630
- end
1023
+ end
631
1024
 
632
- if finish
633
- if options[:grab_last_line]
634
- buffer << this_line
635
- buffer_empty = false
636
- end
637
- # QUESTION should we dup this_line when restoring??
638
- unshift_line this_line if options[:preserve_last_line]
639
- break
1025
+ # Private: Ignore front-matter, commonly used in static site generators
1026
+ def skip_front_matter! data, increment_linenos = true
1027
+ front_matter = nil
1028
+ if data.size > 0 && data.first.chomp == '---'
1029
+ original_data = data.dup
1030
+ front_matter = []
1031
+ data.shift
1032
+ @lineno += 1 if increment_linenos
1033
+ while !data.empty? && data.first.chomp != '---'
1034
+ front_matter.push data.shift
1035
+ @lineno += 1 if increment_linenos
640
1036
  end
641
1037
 
642
- unless skip_line_comments && this_line.match(REGEXP[:comment])
643
- buffer << this_line
644
- buffer_empty = false
1038
+ if data.empty?
1039
+ data.unshift(*original_data)
1040
+ @lineno = 0 if increment_linenos
1041
+ front_matter = nil
1042
+ else
1043
+ data.shift
1044
+ @lineno += 1 if increment_linenos
645
1045
  end
646
1046
  end
647
1047
 
648
- # should we dup the line before chopping?
649
- buffer.last.chomp! if chomp_last_line && !buffer_empty
650
- buffer
1048
+ front_matter
651
1049
  end
652
1050
 
653
- # Public: Convert a string to a legal attribute name.
1051
+ # Private: Resolve the value of one side of the expression
654
1052
  #
655
- # name - The String holding the Asciidoc attribute name.
1053
+ # Examples
656
1054
  #
657
- # Returns a String with the legal name.
1055
+ # expr = '"value"'
1056
+ # resolve_expr_val(expr)
1057
+ # # => "value"
658
1058
  #
659
- # Examples
1059
+ # expr = '"value'
1060
+ # resolve_expr_val(expr)
1061
+ # # => "\"value"
660
1062
  #
661
- # sanitize_attribute_name('Foo Bar')
662
- # => 'foobar'
1063
+ # expr = '"{undefined}"'
1064
+ # resolve_expr_val(expr)
1065
+ # # => ""
663
1066
  #
664
- # sanitize_attribute_name('foo')
665
- # => 'foo'
1067
+ # expr = '{undefined}'
1068
+ # resolve_expr_val(expr)
1069
+ # # => nil
666
1070
  #
667
- # sanitize_attribute_name('Foo 3 #-Billy')
668
- # => 'foo3-billy'
669
- def sanitize_attribute_name(name)
670
- Lexer.sanitize_attribute_name(name)
671
- end
672
-
673
- # Private: Resolve the value of one side of the expression
1071
+ # expr = '2'
1072
+ # resolve_expr_val(expr)
1073
+ # # => 2
1074
+ #
1075
+ # @document.attributes['name'] = 'value'
1076
+ # expr = '"{name}"'
1077
+ # resolve_expr_val(expr)
1078
+ # # => "value"
1079
+ #
1080
+ # Returns The value of the expression, coerced to the appropriate type
674
1081
  def resolve_expr_val(str)
675
1082
  val = str
676
1083
  type = nil
677
1084
 
678
1085
  if val.start_with?('"') && val.end_with?('"') ||
679
1086
  val.start_with?('\'') && val.end_with?('\'')
680
- type = :s
681
- val = val[1..-2]
1087
+ type = :string
1088
+ val = val[1...-1]
682
1089
  end
683
1090
 
1091
+ # QUESTION should we substitute first?
684
1092
  if val.include? '{'
685
1093
  val = @document.sub_attributes val
686
1094
  end
687
1095
 
688
- if type != :s
1096
+ unless type == :string
689
1097
  if val.empty?
690
1098
  val = nil
1099
+ elsif val.strip.empty?
1100
+ val = ' '
691
1101
  elsif val == 'true'
692
1102
  val = true
693
1103
  elsif val == 'false'
@@ -695,6 +1105,8 @@ class Reader
695
1105
  elsif val.include?('.')
696
1106
  val = val.to_f
697
1107
  else
1108
+ # fallback to coercing to integer, since we
1109
+ # require string values to be explicitly quoted
698
1110
  val = val.to_i
699
1111
  end
700
1112
  end
@@ -702,75 +1114,22 @@ class Reader
702
1114
  val
703
1115
  end
704
1116
 
705
- # Private: Normalize raw input read from an include directive
706
- #
707
- # This method strips whitespace from the end of every line of
708
- # the source data and appends a LF (i.e., Unix endline). This
709
- # whitespace substitution is very important to how Asciidoctor
710
- # works.
711
- #
712
- # Any leading or trailing blank lines are also removed. (DISABLED)
713
- #
714
- # data - A String Array of input data to be normalized
715
- #
716
- # returns the processed lines
717
- #-
718
- # FIXME this shares too much in common w/ normalize_data; combine
719
- # in a shared function
720
- def normalize_include_data(data, indent = nil)
721
- if ::Asciidoctor::FORCE_ENCODING
722
- result = data.map {|line| "#{line.rstrip.force_encoding(::Encoding::UTF_8)}\n" }
1117
+ def include_processors?
1118
+ if @include_processors.nil?
1119
+ if @document.extensions? && @document.extensions.include_processors?
1120
+ @include_processors = @document.extensions.load_include_processors(@document)
1121
+ true
1122
+ else
1123
+ @include_processors = false
1124
+ false
1125
+ end
723
1126
  else
724
- result = data.map {|line| "#{line.rstrip}\n" }
1127
+ @include_processors != false
725
1128
  end
726
-
727
- unless indent.nil?
728
- Lexer.reset_block_indent! result, indent.to_i
729
- end
730
-
731
- result
732
-
733
- #data.shift while !data.first.nil? && data.first.chomp.empty?
734
- #data.pop while !data.last.nil? && data.last.chomp.empty?
735
- #data
736
1129
  end
737
1130
 
738
- # Private: Normalize raw input, used for the outermost Reader.
739
- #
740
- # This method strips whitespace from the end of every line of
741
- # the source data and appends a LF (i.e., Unix endline). This
742
- # whitespace substitution is very important to how Asciidoctor
743
- # works.
744
- #
745
- # Any leading or trailing blank lines are also removed.
746
- #
747
- # The normalized lines are assigned to the @lines instance variable.
748
- #
749
- # data - A String Array of input data to be normalized
750
- #
751
- # returns nothing
752
- def normalize_data(data)
753
- # normalize line ending to LF (purging occurrences of CRLF)
754
- # this rstrip is *very* important to how Asciidoctor works
755
-
756
- if ::Asciidoctor::FORCE_ENCODING
757
- @lines = data.map {|line| "#{line.rstrip.force_encoding(::Encoding::UTF_8)}\n" }
758
- else
759
- @lines = data.map {|line| "#{line.rstrip}\n" }
760
- end
761
-
762
- @lines.shift && @lineno += 1 while !@lines.first.nil? && @lines.first.chomp.empty?
763
- @lines.pop while !@lines.last.nil? && @lines.last.chomp.empty?
764
-
765
- # Process bibliography references, so they're available when text
766
- # before the reference is being rendered.
767
- # FIXME reenable whereever it belongs
768
- #@lines.each do |line|
769
- # if biblio = line.match(REGEXP[:biblio])
770
- # @document.register(:ids, biblio[1])
771
- # end
772
- #end
773
- nil
1131
+ def to_s
1132
+ %(#{self.class.name} [path: #{@path}, line #: #{@lineno}, include depth: #{@include_stack.size}, include stack: [#{@include_stack.map {|inc| inc.to_s}.join ', '}]])
774
1133
  end
775
1134
  end
776
1135
  end