asciidoctor 0.1.4 → 1.5.0

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +209 -25
  3. data/{LICENSE → LICENSE.adoc} +4 -3
  4. data/README.adoc +392 -395
  5. data/Rakefile +94 -137
  6. data/benchmark/benchmark.rb +127 -0
  7. data/benchmark/sample-data/mdbasics.adoc +334 -0
  8. data/bin/asciidoctor +5 -8
  9. data/bin/asciidoctor-safe +4 -8
  10. data/compat/asciidoc.conf +78 -11
  11. data/compat/font-awesome-3-compat.css +397 -0
  12. data/data/stylesheets/asciidoctor-default.css +399 -0
  13. data/data/stylesheets/coderay-asciidoctor.css +89 -0
  14. data/features/open_block.feature +92 -0
  15. data/features/pass_block.feature +66 -0
  16. data/features/step_definitions.rb +42 -0
  17. data/features/text_formatting.feature +55 -0
  18. data/features/xref.feature +116 -0
  19. data/lib/asciidoctor.rb +1155 -605
  20. data/lib/asciidoctor/abstract_block.rb +157 -71
  21. data/lib/asciidoctor/abstract_node.rb +150 -93
  22. data/lib/asciidoctor/attribute_list.rb +85 -90
  23. data/lib/asciidoctor/block.rb +51 -24
  24. data/lib/asciidoctor/callouts.rb +4 -7
  25. data/lib/asciidoctor/cli.rb +3 -0
  26. data/lib/asciidoctor/cli/invoker.rb +86 -76
  27. data/lib/asciidoctor/cli/options.rb +111 -61
  28. data/lib/asciidoctor/converter.rb +232 -0
  29. data/lib/asciidoctor/converter/base.rb +58 -0
  30. data/lib/asciidoctor/converter/composite.rb +66 -0
  31. data/lib/asciidoctor/converter/docbook45.rb +94 -0
  32. data/lib/asciidoctor/converter/docbook5.rb +684 -0
  33. data/lib/asciidoctor/converter/factory.rb +225 -0
  34. data/lib/asciidoctor/converter/html5.rb +1081 -0
  35. data/lib/asciidoctor/converter/template.rb +296 -0
  36. data/lib/asciidoctor/core_ext.rb +7 -0
  37. data/lib/asciidoctor/core_ext/object/nil_or_empty.rb +23 -0
  38. data/lib/asciidoctor/core_ext/string/chr.rb +6 -0
  39. data/lib/asciidoctor/core_ext/symbol/length.rb +6 -0
  40. data/lib/asciidoctor/document.rb +590 -304
  41. data/lib/asciidoctor/extensions.rb +1100 -308
  42. data/lib/asciidoctor/helpers.rb +109 -46
  43. data/lib/asciidoctor/inline.rb +16 -9
  44. data/lib/asciidoctor/list.rb +23 -15
  45. data/lib/asciidoctor/opal_ext.rb +4 -0
  46. data/lib/asciidoctor/opal_ext/comparable.rb +38 -0
  47. data/lib/asciidoctor/opal_ext/dir.rb +13 -0
  48. data/lib/asciidoctor/opal_ext/error.rb +2 -0
  49. data/lib/asciidoctor/opal_ext/file.rb +125 -0
  50. data/lib/asciidoctor/{lexer.rb → parser.rb} +646 -455
  51. data/lib/asciidoctor/path_resolver.rb +141 -77
  52. data/lib/asciidoctor/reader.rb +257 -187
  53. data/lib/asciidoctor/section.rb +12 -16
  54. data/lib/asciidoctor/stylesheets.rb +91 -0
  55. data/lib/asciidoctor/substitutors.rb +1548 -0
  56. data/lib/asciidoctor/table.rb +73 -57
  57. data/lib/asciidoctor/timings.rb +39 -0
  58. data/lib/asciidoctor/version.rb +1 -1
  59. data/man/asciidoctor.1 +22 -14
  60. data/man/asciidoctor.adoc +18 -10
  61. data/test/attributes_test.rb +314 -14
  62. data/test/blocks_test.rb +763 -118
  63. data/test/converter_test.rb +352 -0
  64. data/test/document_test.rb +518 -199
  65. data/test/extensions_test.rb +273 -103
  66. data/test/fixtures/asciidoc_index.txt +27 -13
  67. data/test/fixtures/basic-docinfo.xml +1 -1
  68. data/test/fixtures/chapter-a.adoc +3 -0
  69. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
  70. data/test/fixtures/docinfo.xml +1 -1
  71. data/test/fixtures/include-file.asciidoc +2 -0
  72. data/test/fixtures/master.adoc +5 -0
  73. data/test/invoker_test.rb +173 -61
  74. data/test/links_test.rb +97 -21
  75. data/test/lists_test.rb +181 -22
  76. data/test/options_test.rb +86 -2
  77. data/test/paragraphs_test.rb +47 -5
  78. data/test/{lexer_test.rb → parser_test.rb} +128 -57
  79. data/test/paths_test.rb +36 -1
  80. data/test/preamble_test.rb +25 -17
  81. data/test/reader_test.rb +404 -249
  82. data/test/sections_test.rb +623 -58
  83. data/test/substitutions_test.rb +609 -132
  84. data/test/tables_test.rb +198 -24
  85. data/test/test_helper.rb +101 -31
  86. data/test/text_test.rb +88 -31
  87. metadata +160 -64
  88. data/Gemfile +0 -12
  89. data/Guardfile +0 -18
  90. data/asciidoctor.gemspec +0 -143
  91. data/lib/asciidoctor/backends/_stylesheets.rb +0 -466
  92. data/lib/asciidoctor/backends/base_template.rb +0 -114
  93. data/lib/asciidoctor/backends/docbook45.rb +0 -774
  94. data/lib/asciidoctor/backends/docbook5.rb +0 -103
  95. data/lib/asciidoctor/backends/html5.rb +0 -1214
  96. data/lib/asciidoctor/renderer.rb +0 -259
  97. data/lib/asciidoctor/substituters.rb +0 -1083
  98. data/test/fixtures/asciidoc.txt +0 -105
  99. data/test/fixtures/ascshort.txt +0 -32
  100. data/test/fixtures/list_elements.asciidoc +0 -10
  101. data/test/renderer_test.rb +0 -162
@@ -102,9 +102,11 @@ module Asciidoctor
102
102
  class PathResolver
103
103
  DOT = '.'
104
104
  DOT_DOT = '..'
105
+ DOT_SLASH = './'
105
106
  SLASH = '/'
106
107
  BACKSLASH = '\\'
107
- WIN_ROOT_RE = /^[[:alpha:]]:(?:\\|\/)/
108
+ DOUBLE_SLASH = '//'
109
+ WindowsRootRx = /^[a-zA-Z]:(?:\\|\/)/
108
110
 
109
111
  attr_accessor :file_separator
110
112
  attr_accessor :working_dir
@@ -115,16 +117,18 @@ class PathResolver
115
117
  # expanded to an absolute path inside the constructor.
116
118
  #
117
119
  # file_separator - the String file separator to use for path operations
118
- # (optional, default: File::FILE_SEPARATOR)
120
+ # (optional, default: File::SEPARATOR)
119
121
  # working_dir - the String working directory (optional, default: Dir.pwd)
120
122
  #
121
- def initialize(file_separator = nil, working_dir = nil)
122
- @file_separator = file_separator.nil? ? (File::ALT_SEPARATOR || File::SEPARATOR) : file_separator
123
- if working_dir.nil?
124
- @working_dir = File.expand_path(Dir.pwd)
123
+ def initialize file_separator = nil, working_dir = nil
124
+ @file_separator = file_separator ? file_separator : (::File::ALT_SEPARATOR || ::File::SEPARATOR)
125
+ if working_dir
126
+ @working_dir = (is_root? working_dir) ? working_dir : (::File.expand_path working_dir)
125
127
  else
126
- @working_dir = is_root?(working_dir) ? working_dir : File.expand_path(working_dir)
128
+ @working_dir = ::File.expand_path ::Dir.pwd
127
129
  end
130
+ @_partition_path_sys = {}
131
+ @_partition_path_web = {}
128
132
  end
129
133
 
130
134
  # Public: Check if the specified path is an absolute root path
@@ -133,22 +137,33 @@ class PathResolver
133
137
  # path - the String path to check
134
138
  #
135
139
  # returns a Boolean indicating whether the path is an absolute root path
136
- def is_root?(path)
137
- if @file_separator == BACKSLASH && path.match(WIN_ROOT_RE)
140
+ def is_root? path
141
+ # Unix absolute paths and UNC paths start with slash
142
+ if path.start_with? SLASH
138
143
  true
139
- elsif path.start_with? SLASH
144
+ # Windows roots can begin with drive letter
145
+ elsif @file_separator == BACKSLASH && WindowsRootRx =~ path
140
146
  true
141
147
  else
142
148
  false
143
149
  end
144
150
  end
145
151
 
152
+ # Public: Determine if the path is a UNC (root) path
153
+ #
154
+ # path - the String path to check
155
+ #
156
+ # returns a Boolean indicating whether the path is a UNC path
157
+ def is_unc? path
158
+ path.start_with? DOUBLE_SLASH
159
+ end
160
+
146
161
  # Public: Determine if the path is an absolute (root) web path
147
162
  #
148
163
  # path - the String path to check
149
164
  #
150
165
  # returns a Boolean indicating whether the path is an absolute (root) web path
151
- def is_web_root?(path)
166
+ def is_web_root? path
152
167
  path.start_with? SLASH
153
168
  end
154
169
 
@@ -157,9 +172,14 @@ class PathResolver
157
172
  # path - the String path to normalize
158
173
  #
159
174
  # returns a String path with any backslashes replaced with forward slashes
160
- def posixfy(path)
161
- return '' if path.to_s.empty?
162
- path.include?(BACKSLASH) ? path.tr(BACKSLASH, SLASH) : path
175
+ def posixfy path
176
+ if path.nil_or_empty?
177
+ ''
178
+ elsif path.include? BACKSLASH
179
+ path.tr BACKSLASH, SLASH
180
+ else
181
+ path
182
+ end
163
183
  end
164
184
 
165
185
  # Public: Expand the path by resolving any parent references (..)
@@ -173,32 +193,76 @@ class PathResolver
173
193
  # path - the String path to expand
174
194
  #
175
195
  # returns a String path with any parent or self references resolved.
176
- def expand_path(path)
177
- path_segments, path_root, _ = partition_path(path)
196
+ def expand_path path
197
+ path_segments, path_root, _ = partition_path path
178
198
  join_path path_segments, path_root
179
199
  end
180
200
 
181
201
  # Public: Partition the path into path segments and remove any empty segments
182
- # or segments that are self references (.). The path is split on either posix
183
- # or windows file separators.
202
+ # or segments that are self references (.). The path is converted to a posix
203
+ # path before being partitioned.
184
204
  #
185
205
  # path - the String path to partition
186
206
  # web_path - a Boolean indicating whether the path should be handled
187
207
  # as a web path (optional, default: false)
188
208
  #
189
- # returns a 3-item Array containing the Array of String path segments, the
190
- # path root, if the path is absolute, and the posix version of the path.
191
- def partition_path(path, web_path = false)
209
+ # Returns a 3-item Array containing the Array of String path segments, the
210
+ # path root (e.g., '/', './', 'c:/') if the path is absolute and the posix
211
+ # version of the path.
212
+ #--
213
+ # QUESTION is it worth it to normalize slashes? it doubles the time elapsed
214
+ def partition_path path, web_path = false
215
+ if (result = web_path ? @_partition_path_web[path] : @_partition_path_sys[path])
216
+ return result
217
+ end
218
+
192
219
  posix_path = posixfy path
193
- is_root = web_path ? is_web_root?(posix_path) : is_root?(posix_path)
194
- path_segments = posix_path.tr_s(SLASH, SLASH).split(SLASH)
195
- # capture relative root
196
- root = path_segments.first == DOT ? DOT : nil
197
- path_segments.delete(DOT)
198
- # capture absolute root, preserving relative root if set
199
- root = is_root ? path_segments.shift : root
200
-
201
- [path_segments, root, posix_path]
220
+
221
+ root = if web_path
222
+ # ex. /sample/path
223
+ if is_web_root? posix_path
224
+ SLASH
225
+ # ex. ./sample/path
226
+ elsif posix_path.start_with? DOT_SLASH
227
+ DOT_SLASH
228
+ # ex. sample/path
229
+ else
230
+ nil
231
+ end
232
+ else
233
+ if is_root? posix_path
234
+ # ex. //sample/path
235
+ if is_unc? posix_path
236
+ DOUBLE_SLASH
237
+ # ex. /sample/path
238
+ elsif posix_path.start_with? SLASH
239
+ SLASH
240
+ # ex. c:/sample/path
241
+ else
242
+ posix_path[0..(posix_path.index SLASH)]
243
+ end
244
+ # ex. ./sample/path
245
+ elsif posix_path.start_with? DOT_SLASH
246
+ DOT_SLASH
247
+ # ex. sample/path
248
+ else
249
+ nil
250
+ end
251
+ end
252
+
253
+ path_segments = posix_path.split SLASH
254
+ # shift twice for a UNC path
255
+ if root == DOUBLE_SLASH
256
+ path_segments = path_segments[2..-1]
257
+ # shift once for any other root
258
+ elsif root
259
+ path_segments.shift
260
+ end
261
+ # strip out all dot entries
262
+ path_segments.delete DOT
263
+ # QUESTION should we chomp trailing /? (we pay a small fraction)
264
+ #posix_path = posix_path.chomp '/'
265
+ (web_path ? @_partition_path_web : @_partition_path_sys)[path] = [path_segments, root, posix_path]
202
266
  end
203
267
 
204
268
  # Public: Join the segments using the posix file separator (since Ruby knows
@@ -211,9 +275,9 @@ class PathResolver
211
275
  #
212
276
  # returns a String path formed by joining the segments using the posix file
213
277
  # separator and prepending the root, if specified
214
- def join_path(segments, root = nil)
278
+ def join_path segments, root = nil
215
279
  if root
216
- "#{root}#{SLASH}#{segments * SLASH}"
280
+ %(#{root}#{segments * SLASH})
217
281
  else
218
282
  segments * SLASH
219
283
  end
@@ -237,68 +301,68 @@ class PathResolver
237
301
  # returns a String path that joins the target path with the start path with
238
302
  # any parent references resolved and self references removed and enforces
239
303
  # that the resolved path be contained within the jail, if provided
240
- def system_path(target, start, jail = nil, opts = {})
241
- recover = opts.fetch(:recover, true)
242
- unless jail.nil?
304
+ def system_path target, start, jail = nil, opts = {}
305
+ recover = opts.fetch :recover, true
306
+ if jail
243
307
  unless is_root? jail
244
- raise SecurityError, "Jail is not an absolute path: #{jail}"
308
+ raise ::SecurityError, %(Jail is not an absolute path: #{jail})
245
309
  end
246
310
  jail = posixfy jail
247
311
  end
248
312
 
249
- if target.to_s.empty?
313
+ if target.nil_or_empty?
250
314
  target_segments = []
251
315
  else
252
- target_segments, target_root, _ = partition_path(target)
316
+ target_segments, target_root, _ = partition_path target
253
317
  end
254
318
 
255
319
  if target_segments.empty?
256
- if start.to_s.empty?
257
- return jail.nil? ? @working_dir : jail
320
+ if start.nil_or_empty?
321
+ return jail ? jail : @working_dir
258
322
  elsif is_root? start
259
- if jail.nil?
323
+ unless jail
260
324
  return expand_path start
261
325
  end
262
326
  else
263
- return system_path(start, jail, jail)
327
+ return system_path start, jail, jail
264
328
  end
265
329
  end
266
330
 
267
- if target_root && target_root != DOT
331
+ if target_root && target_root != DOT_SLASH
268
332
  resolved_target = join_path target_segments, target_root
269
333
  # if target is absolute and a sub-directory of jail, or
270
334
  # a jail is not in place, let it slide
271
- if jail.nil? || resolved_target.start_with?(jail)
335
+ if !jail || (resolved_target.start_with? jail)
272
336
  return resolved_target
273
337
  end
274
338
  end
275
339
 
276
- if start.to_s.empty?
277
- start = jail.nil? ? @working_dir : jail
340
+ if start.nil_or_empty?
341
+ start = jail ? jail : @working_dir
278
342
  elsif is_root? start
279
343
  start = posixfy start
280
344
  else
281
- start = system_path(start, jail, jail)
345
+ start = system_path start, jail, jail
282
346
  end
283
347
 
284
348
  # both jail and start have been posixfied at this point
285
349
  if jail == start
286
- jail_segments, jail_root, _ = partition_path(jail)
350
+ jail_segments, jail_root, _ = partition_path jail
287
351
  start_segments = jail_segments.dup
288
- elsif !jail.nil?
289
- if !start.start_with?(jail)
290
- raise SecurityError, "#{opts[:target_name] || 'Start path'} #{start} is outside of jail: #{jail} (disallowed in safe mode)"
352
+ elsif jail
353
+ unless start.start_with? jail
354
+ raise ::SecurityError, %(#{opts[:target_name] || 'Start path'} #{start} is outside of jail: #{jail} (disallowed in safe mode))
291
355
  end
292
356
 
293
- start_segments, start_root, _ = partition_path(start)
294
- jail_segments, jail_root, _ = partition_path(jail)
357
+ start_segments, start_root, _ = partition_path start
358
+ jail_segments, jail_root, _ = partition_path jail
295
359
 
296
360
  # Already checked for this condition
297
361
  #if start_root != jail_root
298
- # raise SecurityError, "Jail root #{jail_root} does not match root of #{opts[:target_name] || 'start path'}: #{start_root}"
362
+ # raise ::SecurityError, %(Jail root #{jail_root} does not match root of #{opts[:target_name] || 'start path'}: #{start_root})
299
363
  #end
300
364
  else
301
- start_segments, start_root, _ = partition_path(start)
365
+ start_segments, start_root, _ = partition_path start
302
366
  jail_root = start_root
303
367
  end
304
368
 
@@ -306,13 +370,13 @@ class PathResolver
306
370
  warned = false
307
371
  target_segments.each do |segment|
308
372
  if segment == DOT_DOT
309
- if !jail.nil?
373
+ if jail
310
374
  if resolved_segments.length > jail_segments.length
311
375
  resolved_segments.pop
312
376
  elsif !recover
313
- raise SecurityError, "#{opts[:target_name] || 'path'} #{target} refers to location outside jail: #{jail} (disallowed in safe mode)"
377
+ raise ::SecurityError, %(#{opts[:target_name] || 'path'} #{target} refers to location outside jail: #{jail} (disallowed in safe mode))
314
378
  elsif !warned
315
- warn "asciidoctor: WARNING: #{opts[:target_name] || 'path'} has illegal reference to ancestor of jail, auto-recovering"
379
+ warn %(asciidoctor: WARNING: #{opts[:target_name] || 'path'} has illegal reference to ancestor of jail, auto-recovering)
316
380
  warned = true
317
381
  end
318
382
  else
@@ -336,39 +400,39 @@ class PathResolver
336
400
  # returns a String path that joins the target path with the
337
401
  # start path with any parent references resolved and self
338
402
  # references removed
339
- def web_path(target, start = nil)
340
- target = posixfy(target)
341
- start = posixfy(start)
403
+ def web_path target, start = nil
404
+ target = posixfy target
405
+ start = posixfy start
342
406
  uri_prefix = nil
343
407
 
344
- unless is_web_root?(target) || start.empty?
345
- target = "#{start}#{SLASH}#{target}"
346
- if target.include?(':') && target.match(Asciidoctor::REGEXP[:uri_sniff])
408
+ unless start.nil_or_empty? || (is_web_root? target)
409
+ target = %(#{start}#{SLASH}#{target})
410
+ if (target.include? ':') && UriSniffRx =~ target
347
411
  uri_prefix = $~[0]
348
412
  target = target[uri_prefix.length..-1]
349
413
  end
350
414
  end
351
415
 
352
- target_segments, target_root, _ = partition_path(target, true)
353
- resolved_segments = target_segments.inject([]) do |accum, segment|
416
+ target_segments, target_root, _ = partition_path target, true
417
+ resolved_segments = []
418
+ target_segments.each do |segment|
354
419
  if segment == DOT_DOT
355
- if accum.empty?
356
- accum.push segment unless target_root && target_root != DOT
357
- elsif accum[-1] == DOT_DOT
358
- accum.push segment
420
+ if resolved_segments.empty?
421
+ resolved_segments << segment unless target_root && target_root != DOT_SLASH
422
+ elsif resolved_segments[-1] == DOT_DOT
423
+ resolved_segments << segment
359
424
  else
360
- accum.pop
425
+ resolved_segments.pop
361
426
  end
362
427
  else
363
- accum.push segment
428
+ resolved_segments << segment
364
429
  end
365
- accum
366
430
  end
367
431
 
368
- if uri_prefix.nil?
369
- join_path resolved_segments, target_root
432
+ if uri_prefix
433
+ %(#{uri_prefix}#{join_path resolved_segments, target_root})
370
434
  else
371
- "#{uri_prefix}#{join_path resolved_segments, target_root}"
435
+ join_path resolved_segments, target_root
372
436
  end
373
437
  end
374
438
 
@@ -380,7 +444,7 @@ class PathResolver
380
444
  # base_directory - An absolute base directory as a String
381
445
  #
382
446
  # Return the relative path String of the filename calculated from the base directory
383
- def relative_path(filename, base_directory)
447
+ def relative_path filename, base_directory
384
448
  if (is_root? filename) && (is_root? base_directory)
385
449
  offset = base_directory.chomp(@file_separator).length + 1
386
450
  filename[offset..-1]
@@ -17,6 +17,8 @@ class Reader
17
17
  def line_info
18
18
  %(#{path}: line #{lineno})
19
19
  end
20
+
21
+ alias :to_s :line_info
20
22
  end
21
23
 
22
24
  attr_reader :file
@@ -33,34 +35,33 @@ class Reader
33
35
  attr_accessor :process_lines
34
36
 
35
37
  # Public: Initialize the Reader object
36
- def initialize data = nil, cursor = nil
37
- if cursor.nil?
38
+ def initialize data = nil, cursor = nil, opts = {:normalize => false}
39
+ if !cursor
38
40
  @file = @dir = nil
39
41
  @path = '<stdin>'
40
42
  @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
41
- elsif cursor.is_a? String
43
+ elsif cursor.is_a? ::String
42
44
  @file = cursor
43
- @dir = File.dirname @file
44
- @path = File.basename @file
45
+ @dir, @path = ::File.split @file
45
46
  @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
46
47
  else
47
48
  @file = cursor.file
48
49
  @dir = cursor.dir
49
50
  @path = cursor.path || '<stdin>'
50
- unless @file.nil?
51
- if @dir.nil?
51
+ if @file
52
+ unless @dir
52
53
  # REVIEW might to look at this assignment closer
53
- @dir = File.dirname @file
54
+ @dir = ::File.dirname @file
54
55
  @dir = nil if @dir == '.' # right?
55
56
  end
56
57
 
57
- if cursor.path.nil?
58
- @path = File.basename @file
58
+ unless cursor.path
59
+ @path = ::File.basename @file
59
60
  end
60
61
  end
61
62
  @lineno = cursor.lineno || 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
62
63
  end
63
- @lines = data.nil? ? [] : (prepare_lines data)
64
+ @lines = data ? (prepare_lines data, opts) : []
64
65
  @source_lines = @lines.dup
65
66
  @eof = @lines.empty?
66
67
  @look_ahead = 0
@@ -77,14 +78,24 @@ class Reader
77
78
  #
78
79
  # Any leading or trailing blank lines are also removed.
79
80
  #
80
- # The normalized lines are assigned to the @lines instance variable.
81
- #
82
81
  # data - A String Array of input data to be normalized
83
82
  # opts - A Hash of options to control what cleansing is done
84
83
  #
85
84
  # Returns The String lines extracted from the data
86
85
  def prepare_lines data, opts = {}
87
- data.is_a?(String) ? data.each_line.to_a : data.dup
86
+ if data.is_a? ::String
87
+ if opts[:normalize]
88
+ Helpers.normalize_lines_from_string data
89
+ else
90
+ data.split EOL
91
+ end
92
+ else
93
+ if opts[:normalize]
94
+ Helpers.normalize_lines_array data
95
+ else
96
+ data.dup
97
+ end
98
+ end
88
99
  end
89
100
 
90
101
  # Internal: Processes a previously unvisited line
@@ -118,7 +129,7 @@ class Reader
118
129
  #
119
130
  # Returns True if the there are no more lines or if the next line is empty
120
131
  def next_line_empty?
121
- (line = peek_line).nil? || line.chomp.empty?
132
+ peek_line.nil_or_empty?
122
133
  end
123
134
 
124
135
  # Public: Peek at the next line of source data. Processes the line, if not
@@ -126,7 +137,7 @@ class Reader
126
137
  #
127
138
  # This method will probe the reader for more lines. If there is a next line
128
139
  # that has not previously been visited, the line is passed to the
129
- # Reader#preprocess_line method to be initialized. This call gives
140
+ # Reader#process_line method to be initialized. This call gives
130
141
  # sub-classess the opportunity to do preprocessing. If the return value of
131
142
  # the Reader#process_line is nil, the data is assumed to be changed and
132
143
  # Reader#peek_line is invoked again to perform further processing.
@@ -138,7 +149,7 @@ class Reader
138
149
  # Returns nil if there is no more data.
139
150
  def peek_line direct = false
140
151
  if direct || @look_ahead > 0
141
- @unescape_next_line ? @lines.first[1..-1] : @lines.first
152
+ @unescape_next_line ? @lines[0][1..-1] : @lines[0]
142
153
  elsif @eof || @lines.empty?
143
154
  @eof = true
144
155
  @look_ahead = 0
@@ -147,7 +158,7 @@ class Reader
147
158
  # FIXME the problem with this approach is that we aren't
148
159
  # retaining the modified line (hence the @unescape_next_line tweak)
149
160
  # perhaps we need a stack of proxy lines
150
- if (line = process_line @lines.first).nil?
161
+ if !(line = process_line @lines[0])
151
162
  peek_line
152
163
  else
153
164
  line
@@ -171,7 +182,7 @@ class Reader
171
182
  def peek_lines num = 1, direct = true
172
183
  old_look_ahead = @look_ahead
173
184
  result = []
174
- (1..num).each do
185
+ num.times do
175
186
  if (line = read_line direct)
176
187
  result << line
177
188
  else
@@ -213,7 +224,7 @@ class Reader
213
224
  def read_lines
214
225
  lines = []
215
226
  while has_more_lines?
216
- lines << read_line
227
+ lines << shift
217
228
  end
218
229
  lines
219
230
  end
@@ -225,7 +236,7 @@ class Reader
225
236
  #
226
237
  # Returns the lines read joined as a String
227
238
  def read
228
- read_lines.join
239
+ read_lines * EOL
229
240
  end
230
241
 
231
242
  # Public: Advance to the next line by discarding the line at the front of the stack
@@ -235,7 +246,7 @@ class Reader
235
246
  #
236
247
  # returns a Boolean indicating whether there was a line to discard.
237
248
  def advance direct = true
238
- !(read_line direct).nil?
249
+ !!read_line(direct)
239
250
  end
240
251
 
241
252
  # Public: Push the String line onto the beginning of the Array of source data.
@@ -283,13 +294,13 @@ class Reader
283
294
  # Examples
284
295
  #
285
296
  # @lines
286
- # => ["\n", "\t\n", "Foo\n", "Bar\n", "\n"]
297
+ # => ["", "", "Foo", "Bar", ""]
287
298
  #
288
299
  # skip_blank_lines
289
300
  # => 2
290
301
  #
291
302
  # @lines
292
- # => ["Foo\n", "Bar\n"]
303
+ # => ["Foo", "Bar", ""]
293
304
  #
294
305
  # Returns an Integer of the number of lines skipped
295
306
  def skip_blank_lines
@@ -298,7 +309,7 @@ class Reader
298
309
  num_skipped = 0
299
310
  # optimized code for shortest execution path
300
311
  while (next_line = peek_line)
301
- if next_line.chomp.empty?
312
+ if next_line.empty?
302
313
  advance
303
314
  num_skipped += 1
304
315
  else
@@ -313,13 +324,13 @@ class Reader
313
324
  #
314
325
  # Examples
315
326
  # @lines
316
- # => ["// foo\n", "bar\n"]
327
+ # => ["// foo", "bar"]
317
328
  #
318
329
  # comment_lines = skip_comment_lines
319
- # => ["// foo\n"]
330
+ # => ["// foo"]
320
331
  #
321
332
  # @lines
322
- # => ["bar\n"]
333
+ # => ["bar"]
323
334
  #
324
335
  # Returns the Array of lines that were skipped
325
336
  def skip_comment_lines opts = {}
@@ -328,13 +339,13 @@ class Reader
328
339
  comment_lines = []
329
340
  include_blank_lines = opts[:include_blank_lines]
330
341
  while (next_line = peek_line)
331
- if include_blank_lines && next_line.chomp.empty?
332
- comment_lines << read_line
333
- elsif (commentish = next_line.start_with?('//')) && (match = next_line.match(REGEXP[:comment_blk]))
334
- comment_lines << read_line
342
+ if include_blank_lines && next_line.empty?
343
+ comment_lines << shift
344
+ elsif (commentish = next_line.start_with?('//')) && (match = CommentBlockRx.match(next_line))
345
+ comment_lines << shift
335
346
  comment_lines.push(*(read_lines_until(:terminator => match[0], :read_last_line => true, :skip_processing => true)))
336
- elsif commentish && next_line.match(REGEXP[:comment])
337
- comment_lines << read_line
347
+ elsif commentish && CommentLineRx =~ next_line
348
+ comment_lines << shift
338
349
  else
339
350
  break
340
351
  end
@@ -350,8 +361,8 @@ class Reader
350
361
  comment_lines = []
351
362
  # optimized code for shortest execution path
352
363
  while (next_line = peek_line)
353
- if next_line.match(REGEXP[:comment])
354
- comment_lines << read_line
364
+ if CommentLineRx =~ next_line
365
+ comment_lines << shift
355
366
  else
356
367
  break
357
368
  end
@@ -399,12 +410,16 @@ class Reader
399
410
  #
400
411
  # Examples
401
412
  #
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"]
413
+ # data = [
414
+ # "First line\n",
415
+ # "Second line\n",
416
+ # "\n",
417
+ # "Third line\n",
418
+ # ]
419
+ # reader = Reader.new data, nil, :normalize => true
405
420
  #
406
421
  # reader.read_lines_until
407
- # => ["First paragraph\n", "Second paragraph\n", "Open block\n"]
422
+ # => ["First line", "Second line"]
408
423
  def read_lines_until options = {}
409
424
  result = []
410
425
  advance if options[:skip_first_line]
@@ -415,34 +430,32 @@ class Reader
415
430
  restore_process_lines = false
416
431
  end
417
432
 
418
- has_block = block_given?
419
433
  if (terminator = options[:terminator])
420
434
  break_on_blank_lines = false
421
435
  break_on_list_continuation = false
422
- chomp_last_line = options.fetch :chomp_last_line, false
423
436
  else
424
437
  break_on_blank_lines = options[:break_on_blank_lines]
425
438
  break_on_list_continuation = options[:break_on_list_continuation]
426
- chomp_last_line = break_on_blank_lines
427
439
  end
428
440
  skip_line_comments = options[:skip_line_comments]
429
441
  line_read = false
430
442
  line_restored = false
431
443
 
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
444
+ complete = false
445
+ while !complete && (line = read_line)
446
+ complete = while true
447
+ break true if terminator && line == terminator
448
+ # QUESTION: can we get away with line.empty? here?
449
+ break true if break_on_blank_lines && line.empty?
450
+ if break_on_list_continuation && line_read && line == LIST_CONTINUATION
438
451
  options[:preserve_last_line] = true
439
452
  break true
440
453
  end
441
- break true if has_block && (yield line)
454
+ break true if block_given? && (yield line)
442
455
  break false
443
456
  end
444
457
 
445
- if finish
458
+ if complete
446
459
  if options[:read_last_line]
447
460
  result << line
448
461
  line_read = true
@@ -451,27 +464,27 @@ class Reader
451
464
  restore_line line
452
465
  line_restored = true
453
466
  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
467
+ else
468
+ unless skip_line_comments && line.start_with?('//') && CommentLineRx =~ line
469
+ result << line
470
+ line_read = true
471
+ end
460
472
  end
461
473
  end
462
474
 
463
- if chomp_last_line && line_read
464
- result << result.pop.chomp
465
- end
466
-
467
475
  if restore_process_lines
468
476
  @process_lines = true
469
- @look_ahead -= 1 if line_restored && terminator.nil?
477
+ @look_ahead -= 1 if line_restored && !terminator
470
478
  end
471
479
  result
472
480
  end
473
481
 
474
482
  # Internal: Shift the line off the stack and increment the lineno
483
+ #
484
+ # This method can be used directly when you've already called peek_line
485
+ # and determined that you do, in fact, want to pluck that line off the stack.
486
+ #
487
+ # Returns The String line at the top of the stack
475
488
  def shift
476
489
  @lineno += 1
477
490
  @look_ahead -= 1 unless @look_ahead == 0
@@ -511,12 +524,12 @@ class Reader
511
524
 
512
525
  # Public: Get a copy of the remaining lines managed by this Reader joined as a String
513
526
  def string
514
- @lines.join
527
+ @lines * EOL
515
528
  end
516
529
 
517
530
  # Public: Get the source lines for this Reader joined as a String
518
531
  def source
519
- @source_lines.join
532
+ @source_lines * EOL
520
533
  end
521
534
 
522
535
  # Public: Get a summary of this Reader.
@@ -537,7 +550,7 @@ class PreprocessorReader < Reader
537
550
  # Public: Initialize the PreprocessorReader object
538
551
  def initialize document, data = nil, cursor = nil
539
552
  @document = document
540
- super data, cursor
553
+ super data, cursor, :normalize => true
541
554
  include_depth_default = document.attributes.fetch('max-include-depth', 64).to_i
542
555
  include_depth_default = 0 if include_depth_default < 0
543
556
  # track both absolute depth for comparing to size of include stack and relative depth for reporting
@@ -546,39 +559,26 @@ class PreprocessorReader < Reader
546
559
  @includes = (document.references[:includes] ||= [])
547
560
  @skipping = false
548
561
  @conditional_stack = []
549
- @include_processors = nil
562
+ @include_processor_extensions = nil
550
563
  end
551
564
 
552
565
  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
559
- else
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
565
- end
566
+ result = super
566
567
 
567
568
  # 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 @document && (@document.attributes.has_key? 'skip-front-matter')
569
570
  if (front_matter = skip_front_matter! result)
570
- @document.attributes['front-matter'] = front_matter.join.chomp
571
+ @document.attributes['front-matter'] = front_matter * EOL
571
572
  end
572
573
  end
573
574
 
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
575
+ if opts.fetch :condense, true
576
+ result.shift && @lineno += 1 while (first = result[0]) && first.empty?
577
+ result.pop while (last = result[-1]) && last.empty?
578
578
  end
579
579
 
580
580
  if (indent = opts.fetch(:indent, nil))
581
- Lexer.reset_block_indent! result, indent.to_i
581
+ Parser.reset_block_indent! result, indent.to_i
582
582
  end
583
583
 
584
584
  result
@@ -587,55 +587,63 @@ class PreprocessorReader < Reader
587
587
  def process_line line
588
588
  return line unless @process_lines
589
589
 
590
- if line.chomp.empty?
590
+ if line.empty?
591
591
  @look_ahead += 1
592
592
  return ''
593
593
  end
594
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? '\\'
599
- @unescape_next_line = true
600
- @look_ahead += 1
601
- line[1..-1]
602
- else
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
595
+ # NOTE highly optimized
596
+ if line.end_with?(']') && !line.start_with?('[') && line.include?('::')
597
+ if line.include?('if') && (match = ConditionalDirectiveRx.match(line))
598
+ # if escaped, mark as processed and return line unescaped
599
+ if line.start_with?('\\')
600
+ @unescape_next_line = true
601
+ @look_ahead += 1
602
+ line[1..-1]
608
603
  else
609
- # the line was not a valid conditional line
610
- # mark it as visited and return it
604
+ if preprocess_conditional_inclusion(*match.captures)
605
+ # move the pointer past the conditional line
606
+ advance
607
+ # treat next line as uncharted territory
608
+ nil
609
+ else
610
+ # the line was not a valid conditional line
611
+ # mark it as visited and return it
612
+ @look_ahead += 1
613
+ line
614
+ end
615
+ end
616
+ elsif @skipping
617
+ advance
618
+ nil
619
+ elsif ((escaped = line.start_with?('\\include::')) || line.start_with?('include::')) && (match = IncludeDirectiveRx.match(line))
620
+ # if escaped, mark as processed and return line unescaped
621
+ if escaped
622
+ @unescape_next_line = true
611
623
  @look_ahead += 1
612
- line
624
+ line[1..-1]
625
+ else
626
+ # QUESTION should we strip whitespace from raw attributes in Substitutors#parse_attributes? (check perf)
627
+ if preprocess_include match[1], match[2].strip
628
+ # peek again since the content has changed
629
+ nil
630
+ else
631
+ # the line was not a valid include line and is unchanged
632
+ # mark it as visited and return it
633
+ @look_ahead += 1
634
+ line
635
+ end
613
636
  end
637
+ else
638
+ # NOTE optimization to inline super
639
+ @look_ahead += 1
640
+ line
614
641
  end
615
642
  elsif @skipping
616
643
  advance
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? '\\'
621
- @unescape_next_line = true
622
- @look_ahead += 1
623
- line[1..-1]
624
- else
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
635
- end
644
+ nil
636
645
  else
637
- # optimization to inline super
638
- #super
646
+ # NOTE optimization to inline super
639
647
  @look_ahead += 1
640
648
  line
641
649
  end
@@ -685,17 +693,20 @@ class PreprocessorReader < Reader
685
693
  # don't honor match if it doesn't meet this criteria
686
694
  # QUESTION should we warn for these bogus declarations?
687
695
  if ((directive == 'ifdef' || directive == 'ifndef') && target.empty?) ||
688
- (directive == 'endif' && !text.nil?)
696
+ (directive == 'endif' && text)
689
697
  return false
690
698
  end
691
699
 
700
+ # attributes are case insensitive
701
+ target = target.downcase
702
+
692
703
  if directive == 'endif'
693
704
  stack_size = @conditional_stack.size
694
705
  if stack_size > 0
695
- pair = @conditional_stack.last
706
+ pair = @conditional_stack[-1]
696
707
  if target.empty? || target == pair[:target]
697
708
  @conditional_stack.pop
698
- @skipping = @conditional_stack.empty? ? false : @conditional_stack.last[:skipping]
709
+ @skipping = @conditional_stack.empty? ? false : @conditional_stack[-1][:skipping]
699
710
  else
700
711
  warn "asciidoctor: ERROR: #{line_info}: mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[]"
701
712
  end
@@ -736,7 +747,7 @@ class PreprocessorReader < Reader
736
747
  when 'ifeval'
737
748
  # the text in brackets must match an expression
738
749
  # don't honor match if it doesn't meet this criteria
739
- if !target.empty? || !(expr_match = text.strip.match(REGEXP[:eval_expr]))
750
+ if !target.empty? || !(expr_match = EvalExpressionRx.match(text.strip))
740
751
  return false
741
752
  end
742
753
 
@@ -750,7 +761,7 @@ class PreprocessorReader < Reader
750
761
  end
751
762
 
752
763
  # conditional inclusion block
753
- if directive == 'ifeval' || text.nil?
764
+ if directive == 'ifeval' || !text
754
765
  @skipping = true if skip
755
766
  @conditional_stack << {:target => target, :skip => skip, :skipping => @skipping}
756
767
  # single line conditional inclusion
@@ -759,7 +770,7 @@ class PreprocessorReader < Reader
759
770
  # FIXME slight hack to skip past conditional line
760
771
  # but keep our synthetic line marked as processed
761
772
  conditional_line = peek_line true
762
- replace_line "#{text.rstrip}#{::Asciidoctor::EOL}"
773
+ replace_line text.rstrip
763
774
  unshift conditional_line
764
775
  return true
765
776
  end
@@ -789,11 +800,11 @@ class PreprocessorReader < Reader
789
800
  # target slot of the include::[] macro
790
801
  #
791
802
  # returns a Boolean indicating whether the line under the cursor has changed.
792
- def preprocess_include target, raw_attributes
793
- target = @document.sub_attributes target, :attribute_missing => 'drop-line'
794
- if target.empty?
795
- if @document.attributes.fetch('attribute-missing', COMPLIANCE[:attribute_missing]) == 'skip'
796
- false
803
+ def preprocess_include raw_target, raw_attributes
804
+ if (target = @document.sub_attributes raw_target, :attribute_missing => 'drop-line').empty?
805
+ if @document.attributes.fetch('attribute-missing', Compliance.attribute_missing) == 'skip'
806
+ replace_line %(Unresolved directive in #{@path} - include::#{raw_target}[#{raw_attributes}])
807
+ true
797
808
  else
798
809
  advance
799
810
  true
@@ -801,26 +812,34 @@ class PreprocessorReader < Reader
801
812
  # assume that if an include processor is given, the developer wants
802
813
  # to handle when and how to process the include
803
814
  elsif include_processors? &&
804
- (processor = @include_processors.find {|candidate| candidate.handles? target })
815
+ (extension = @include_processor_extensions.find {|candidate| candidate.instance.handles? target })
805
816
  advance
806
- # QUESTION should we use @document.parse_attribues?
807
- processor.process self, target, AttributeList.new(raw_attributes).parse
817
+ # FIXME parse attributes if requested by extension
818
+ extension.process_method[@document, self, target, AttributeList.new(raw_attributes).parse]
808
819
  true
809
820
  # if running in SafeMode::SECURE or greater, don't process this directive
810
821
  # however, be friendly and at least make it a link to the source document
811
822
  elsif @document.safe >= SafeMode::SECURE
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}"
823
+ # FIXME we don't want to use a link macro if we are in a verbatim context
824
+ replace_line %(link:#{target}[])
816
825
  true
817
826
  elsif (abs_maxdepth = @maxdepth[:abs]) > 0 && @include_stack.size >= abs_maxdepth
818
827
  warn %(asciidoctor: ERROR: #{line_info}: maximum include depth of #{@maxdepth[:rel]} exceeded)
819
828
  false
820
829
  elsif abs_maxdepth > 0
821
- if target.include?(':') && target.match(REGEXP[:uri_sniff])
830
+ if ::RUBY_ENGINE_OPAL
831
+ # NOTE resolves uri relative to currently loaded document
832
+ # NOTE we defer checking if file exists and catch the 404 error if it does not
833
+ # TODO only use this logic if env-browser is set
834
+ target_type = :file
835
+ include_file = path = if @include_stack.empty?
836
+ ::Dir.pwd == @document.base_dir ? target : (::File.join @dir, target)
837
+ else
838
+ ::File.join @dir, target
839
+ end
840
+ elsif target.include?(':') && UriSniffRx =~ target
822
841
  unless @document.attributes.has_key? 'allow-uri-read'
823
- replace_line "link:#{target}[]#{::Asciidoctor::EOL}"
842
+ replace_line %(link:#{target}[])
824
843
  return true
825
844
  end
826
845
 
@@ -830,16 +849,17 @@ class PreprocessorReader < Reader
830
849
  # caching requires the open-uri-cached gem to be installed
831
850
  # processing will be automatically aborted if these libraries can't be opened
832
851
  Helpers.require_library 'open-uri/cached', 'open-uri-cached'
833
- else
834
- Helpers.require_library 'open-uri'
852
+ elsif !::RUBY_ENGINE_OPAL
853
+ # autoload open-uri
854
+ ::OpenURI
835
855
  end
836
856
  else
837
857
  target_type = :file
838
858
  # include file is resolved relative to dir of current include, or base_dir if within original docfile
839
859
  include_file = @document.normalize_system_path(target, @dir, nil, :target_name => 'include file')
840
- if !File.file?(include_file)
860
+ unless ::File.file? include_file
841
861
  warn "asciidoctor: WARNING: #{line_info}: include file not found: #{include_file}"
842
- advance
862
+ replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}])
843
863
  return true
844
864
  end
845
865
  #path = @document.relative_path include_file
@@ -854,14 +874,14 @@ class PreprocessorReader < Reader
854
874
  attributes = AttributeList.new(raw_attributes).parse
855
875
  if attributes.has_key? 'lines'
856
876
  inc_lines = []
857
- attributes['lines'].split(REGEXP[:ssv_or_csv_delim]).each do |linedef|
877
+ attributes['lines'].split(DataDelimiterRx).each do |linedef|
858
878
  if linedef.include?('..')
859
879
  from, to = linedef.split('..').map(&:to_i)
860
880
  if to == -1
861
881
  inc_lines << from
862
882
  inc_lines << 1.0/0.0
863
883
  else
864
- inc_lines.concat Range.new(from, to).to_a
884
+ inc_lines.concat ::Range.new(from, to).to_a
865
885
  end
866
886
  else
867
887
  inc_lines << linedef.to_i
@@ -869,9 +889,9 @@ class PreprocessorReader < Reader
869
889
  end
870
890
  inc_lines = inc_lines.sort.uniq
871
891
  elsif attributes.has_key? 'tag'
872
- tags = [attributes['tag']]
892
+ tags = [attributes['tag']].to_set
873
893
  elsif attributes.has_key? 'tags'
874
- tags = attributes['tags'].split(REGEXP[:ssv_or_csv_delim]).uniq
894
+ tags = attributes['tags'].split(DataDelimiterRx).uniq.to_set
875
895
  end
876
896
  end
877
897
  if !inc_lines.nil?
@@ -880,11 +900,11 @@ class PreprocessorReader < Reader
880
900
  inc_line_offset = 0
881
901
  inc_lineno = 0
882
902
  begin
883
- open(include_file) do |f|
903
+ open(include_file, 'r') do |f|
884
904
  f.each_line do |l|
885
905
  inc_lineno += 1
886
- take = inc_lines.first
887
- if take.is_a?(Float) && take.infinite?
906
+ take = inc_lines[0]
907
+ if take.is_a?(::Float) && take.infinite?
888
908
  selected.push l
889
909
  inc_line_offset = inc_lineno if inc_line_offset == 0
890
910
  else
@@ -898,8 +918,8 @@ class PreprocessorReader < Reader
898
918
  end
899
919
  end
900
920
  rescue
901
- warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}"
902
- advance
921
+ warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file})
922
+ replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}])
903
923
  return true
904
924
  end
905
925
  advance
@@ -912,23 +932,26 @@ class PreprocessorReader < Reader
912
932
  inc_line_offset = 0
913
933
  inc_lineno = 0
914
934
  active_tag = nil
935
+ tags_found = ::Set.new
915
936
  begin
916
- open(include_file) do |f|
937
+ open(include_file, 'r') do |f|
917
938
  f.each_line do |l|
918
939
  inc_lineno += 1
919
940
  # 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}[]")
941
+ l.force_encoding(::Encoding::UTF_8) if FORCE_ENCODING
942
+ l = l.rstrip
943
+ if active_tag
944
+ if l.end_with?(%(end::#{active_tag}[])) && TagDirectiveRx =~ l
923
945
  active_tag = nil
924
946
  else
925
- selected.push l
947
+ selected.push l unless l.end_with?('[]') && TagDirectiveRx =~ l
926
948
  inc_line_offset = inc_lineno if inc_line_offset == 0
927
949
  end
928
950
  else
929
951
  tags.each do |tag|
930
- if l.include?("tag::#{tag}[]")
952
+ if l.end_with?(%(tag::#{tag}[])) && TagDirectiveRx =~ l
931
953
  active_tag = tag
954
+ tags_found << tag
932
955
  break
933
956
  end
934
957
  end
@@ -936,10 +959,13 @@ class PreprocessorReader < Reader
936
959
  end
937
960
  end
938
961
  rescue
939
- warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}"
940
- advance
962
+ warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file})
963
+ replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}])
941
964
  return true
942
965
  end
966
+ unless (missing_tags = tags.to_a - tags_found.to_a).empty?
967
+ warn "asciidoctor: WARNING: #{line_info}: tag#{missing_tags.size > 1 ? 's' : nil} '#{missing_tags * ','}' not found in include #{target_type}: #{include_file}"
968
+ end
943
969
  advance
944
970
  # FIXME not accounting for skipped lines in reader line numbering
945
971
  push_include selected, include_file, path, inc_line_offset, attributes
@@ -947,10 +973,10 @@ class PreprocessorReader < Reader
947
973
  else
948
974
  begin
949
975
  advance
950
- push_include open(include_file) {|f| f.read }, include_file, path, 1, attributes
976
+ push_include open(include_file, 'r') {|f| f.read }, include_file, path, 1, attributes
951
977
  rescue
952
- warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}"
953
- advance
978
+ warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file})
979
+ replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}])
954
980
  return true
955
981
  end
956
982
  end
@@ -960,31 +986,74 @@ class PreprocessorReader < Reader
960
986
  end
961
987
  end
962
988
 
989
+ # Public: Push source onto the front of the reader and switch the context
990
+ # based on the file, document-relative path and line information given.
991
+ #
992
+ # This method is typically used in an IncludeProcessor to add source
993
+ # read from the target specified.
994
+ #
995
+ # Examples
996
+ #
997
+ # path = 'partial.adoc'
998
+ # file = File.expand_path path
999
+ # data = IO.read file
1000
+ # reader.push_include data, file, path
1001
+ #
1002
+ # Returns nothing
963
1003
  def push_include data, file = nil, path = nil, lineno = 1, attributes = {}
964
1004
  @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
1005
+ if file
1006
+ @file = file
1007
+ @dir = File.dirname file
1008
+ # only process lines in AsciiDoc files
1009
+ @process_lines = ASCIIDOC_EXTENSIONS[::File.extname(file)]
1010
+ else
1011
+ @file = nil
1012
+ @dir = '.' # right?
1013
+ # we don't know what file type we have, so assume AsciiDoc
1014
+ @process_lines = true
1015
+ end
1016
+
1017
+ @path = if path
1018
+ @includes << Helpers.rootname(path)
1019
+ path
1020
+ else
1021
+ '<stdin>'
1022
+ end
1023
+
969
1024
  @lineno = lineno
970
- # NOTE only process lines in AsciiDoc files
971
- @process_lines = ASCIIDOC_EXTENSIONS[File.extname(@file)]
1025
+
972
1026
  if attributes.has_key? 'depth'
973
1027
  depth = attributes['depth'].to_i
974
1028
  depth = 1 if depth <= 0
975
1029
  @maxdepth = {:abs => (@include_stack.size - 1) + depth, :rel => depth}
976
1030
  end
1031
+
977
1032
  # 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?
1033
+ if (@lines = prepare_lines data, :normalize => true, :condense => false, :indent => attributes['indent']).empty?
983
1034
  pop_include
984
1035
  else
1036
+ # FIXME we eventually want to handle leveloffset without affecting the lines
1037
+ if attributes.has_key? 'leveloffset'
1038
+ @lines.unshift ''
1039
+ @lines.unshift %(:leveloffset: #{attributes['leveloffset']})
1040
+ @lines.push ''
1041
+ if (old_leveloffset = @document.attr 'leveloffset')
1042
+ @lines.push %(:leveloffset: #{old_leveloffset})
1043
+ else
1044
+ @lines.push ':leveloffset!:'
1045
+ end
1046
+ # compensate for these extra lines
1047
+ @lineno -= 2
1048
+ end
1049
+
1050
+ # FIXME kind of a hack
1051
+ #Document::AttributeEntry.new('infile', @file).save_to_next_block @document
1052
+ #Document::AttributeEntry.new('indir', @dir).save_to_next_block @document
985
1053
  @eof = false
986
1054
  @look_ahead = 0
987
1055
  end
1056
+ nil
988
1057
  end
989
1058
 
990
1059
  def pop_include
@@ -992,10 +1061,11 @@ class PreprocessorReader < Reader
992
1061
  @lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines = @include_stack.pop
993
1062
  # FIXME kind of a hack
994
1063
  #Document::AttributeEntry.new('infile', @file).save_to_next_block @document
995
- #Document::AttributeEntry.new('indir', File.dirname(@file)).save_to_next_block @document
1064
+ #Document::AttributeEntry.new('indir', ::File.dirname(@file)).save_to_next_block @document
996
1065
  @eof = @lines.empty?
997
1066
  @look_ahead = 0
998
1067
  end
1068
+ nil
999
1069
  end
1000
1070
 
1001
1071
  def include_depth
@@ -1025,12 +1095,12 @@ class PreprocessorReader < Reader
1025
1095
  # Private: Ignore front-matter, commonly used in static site generators
1026
1096
  def skip_front_matter! data, increment_linenos = true
1027
1097
  front_matter = nil
1028
- if data.size > 0 && data.first.chomp == '---'
1098
+ if data[0] == '---'
1029
1099
  original_data = data.dup
1030
1100
  front_matter = []
1031
1101
  data.shift
1032
1102
  @lineno += 1 if increment_linenos
1033
- while !data.empty? && data.first.chomp != '---'
1103
+ while !data.empty? && data[0] != '---'
1034
1104
  front_matter.push data.shift
1035
1105
  @lineno += 1 if increment_linenos
1036
1106
  end
@@ -1115,21 +1185,21 @@ class PreprocessorReader < Reader
1115
1185
  end
1116
1186
 
1117
1187
  def include_processors?
1118
- if @include_processors.nil?
1188
+ if !@include_processor_extensions
1119
1189
  if @document.extensions? && @document.extensions.include_processors?
1120
- @include_processors = @document.extensions.load_include_processors(@document)
1190
+ @include_processor_extensions = @document.extensions.include_processors
1121
1191
  true
1122
1192
  else
1123
- @include_processors = false
1193
+ @include_processor_extensions = false
1124
1194
  false
1125
1195
  end
1126
1196
  else
1127
- @include_processors != false
1197
+ @include_processor_extensions != false
1128
1198
  end
1129
1199
  end
1130
1200
 
1131
1201
  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 ', '}]])
1202
+ %(#<#{self.class}@#{object_id} {path: #{@path.inspect}, line #: #{@lineno}, include depth: #{@include_stack.size}, include stack: [#{@include_stack.map {|inc| inc.to_s}.join ', '}]}>)
1133
1203
  end
1134
1204
  end
1135
1205
  end