asciidoctor 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of asciidoctor might be problematic. Click here for more details.

Files changed (40) hide show
  1. data/README.asciidoc +11 -2
  2. data/asciidoctor.gemspec +3 -2
  3. data/lib/asciidoctor.rb +95 -62
  4. data/lib/asciidoctor/abstract_block.rb +7 -5
  5. data/lib/asciidoctor/abstract_node.rb +63 -15
  6. data/lib/asciidoctor/attribute_list.rb +3 -1
  7. data/lib/asciidoctor/backends/base_template.rb +17 -7
  8. data/lib/asciidoctor/backends/docbook45.rb +182 -150
  9. data/lib/asciidoctor/backends/html5.rb +138 -110
  10. data/lib/asciidoctor/block.rb +21 -18
  11. data/lib/asciidoctor/callouts.rb +3 -1
  12. data/lib/asciidoctor/cli/invoker.rb +3 -3
  13. data/lib/asciidoctor/cli/options.rb +6 -6
  14. data/lib/asciidoctor/debug.rb +7 -6
  15. data/lib/asciidoctor/document.rb +197 -25
  16. data/lib/asciidoctor/errors.rb +1 -1
  17. data/lib/asciidoctor/helpers.rb +29 -0
  18. data/lib/asciidoctor/inline.rb +11 -4
  19. data/lib/asciidoctor/lexer.rb +338 -182
  20. data/lib/asciidoctor/list_item.rb +14 -12
  21. data/lib/asciidoctor/reader.rb +423 -206
  22. data/lib/asciidoctor/renderer.rb +59 -15
  23. data/lib/asciidoctor/section.rb +7 -4
  24. data/lib/asciidoctor/substituters.rb +536 -511
  25. data/lib/asciidoctor/table.rb +473 -472
  26. data/lib/asciidoctor/version.rb +1 -1
  27. data/man/asciidoctor.1 +23 -14
  28. data/man/asciidoctor.ad +13 -7
  29. data/test/attributes_test.rb +42 -8
  30. data/test/blocks_test.rb +161 -1
  31. data/test/document_test.rb +134 -16
  32. data/test/invoker_test.rb +14 -6
  33. data/test/lexer_test.rb +45 -18
  34. data/test/lists_test.rb +79 -0
  35. data/test/paragraphs_test.rb +9 -1
  36. data/test/reader_test.rb +456 -19
  37. data/test/sections_test.rb +19 -0
  38. data/test/substitutions_test.rb +14 -12
  39. data/test/tables_test.rb +10 -10
  40. metadata +3 -5
@@ -1,5 +1,6 @@
1
+ module Asciidoctor
1
2
  # Public: Methods for managing items for AsciiDoc olists, ulist, and dlists.
2
- class Asciidoctor::ListItem < Asciidoctor::AbstractBlock
3
+ class ListItem < AbstractBlock
3
4
 
4
5
  # Public: Get/Set the String used to mark this list item
5
6
  attr_accessor :marker
@@ -20,7 +21,7 @@ class Asciidoctor::ListItem < Asciidoctor::AbstractBlock
20
21
 
21
22
  def text
22
23
  # this will allow the text to be processed
23
- ::Asciidoctor::Block.new(self, nil, [@text]).content
24
+ Block.new(self, nil, [@text]).content
24
25
  end
25
26
 
26
27
  def content
@@ -39,7 +40,7 @@ class Asciidoctor::ListItem < Asciidoctor::AbstractBlock
39
40
  #
40
41
  # Returns nothing
41
42
  def fold_first(continuation_connects_first_block = false, content_adjacent = false)
42
- if !blocks.empty? && blocks.first.is_a?(Asciidoctor::Block) &&
43
+ if !blocks.empty? && blocks.first.is_a?(Block) &&
43
44
  ((blocks.first.context == :paragraph && !continuation_connects_first_block) ||
44
45
  ((content_adjacent || !continuation_connects_first_block) && blocks.first.context == :literal &&
45
46
  blocks.first.attr('options', []).include?('listparagraph')))
@@ -56,20 +57,20 @@ class Asciidoctor::ListItem < Asciidoctor::AbstractBlock
56
57
 
57
58
  def splain(parent_level = 0)
58
59
  parent_level += 1
59
- Asciidoctor.puts_indented(parent_level, "List Item anchor: #{@anchor}") unless @anchor.nil?
60
- Asciidoctor.puts_indented(parent_level, "Text: #{@text}") unless @text.nil?
60
+ Debug.puts_indented(parent_level, "List Item anchor: #{@anchor}") unless @anchor.nil?
61
+ Debug.puts_indented(parent_level, "Text: #{@text}") unless @text.nil?
61
62
 
62
- Asciidoctor.puts_indented(parent_level, "Blocks: #{@blocks.count}")
63
+ Debug.puts_indented(parent_level, "Blocks: #{@blocks.count}")
63
64
 
64
65
  if @blocks.any?
65
- Asciidoctor.puts_indented(parent_level, "Blocks content (#{@blocks.count}):")
66
+ Debug.puts_indented(parent_level, "Blocks content (#{@blocks.count}):")
66
67
  @blocks.each_with_index do |block, i|
67
- Asciidoctor.puts_indented(parent_level, "v" * (60 - parent_level*2))
68
- Asciidoctor.puts_indented(parent_level, "Block ##{i} is a #{block.class}")
69
- Asciidoctor.puts_indented(parent_level, "Name is #{block.title rescue 'n/a'}")
70
- Asciidoctor.puts_indented(parent_level, "=" * 40)
68
+ Debug.puts_indented(parent_level, "v" * (60 - parent_level*2))
69
+ Debug.puts_indented(parent_level, "Block ##{i} is a #{block.class}")
70
+ Debug.puts_indented(parent_level, "Name is #{block.title rescue 'n/a'}")
71
+ Debug.puts_indented(parent_level, "=" * 40)
71
72
  block.splain(parent_level) if block.respond_to? :splain
72
- Asciidoctor.puts_indented(parent_level, "^" * (60 - parent_level*2))
73
+ Debug.puts_indented(parent_level, "^" * (60 - parent_level*2))
73
74
  end
74
75
  end
75
76
  nil
@@ -79,3 +80,4 @@ class Asciidoctor::ListItem < Asciidoctor::AbstractBlock
79
80
  "#{super.to_s} - #@context [text:#@text, blocks:#{(@blocks || []).size}]"
80
81
  end
81
82
  end
83
+ end
@@ -1,23 +1,23 @@
1
- # Public: Methods for retrieving lines from Asciidoc documents
2
- class Asciidoctor::Reader
1
+ module Asciidoctor
2
+ # Public: Methods for retrieving lines from AsciiDoc source files
3
+ class Reader
3
4
 
4
- include Asciidoctor
5
-
6
- # Public: Get the String document source.
5
+ # Public: Get the document source as a String Array of lines.
7
6
  attr_reader :source
8
7
 
9
- # Public: Get the String Array of lines parsed from the source
10
- attr_reader :lines
8
+ # Public: Get the 1-based offset of the current line.
9
+ attr_reader :lineno
11
10
 
12
11
  # Public: Initialize the Reader object.
13
12
  #
14
13
  # data - The Array of Strings holding the Asciidoc source document. The
15
- # original instance of this Array is not modified
14
+ # original instance of this Array is not modified (default: nil)
16
15
  # document - The document with which this reader is associated. Used to access
17
- # document attributes
18
- # overrides - A Hash of attributes that were passed to the Document and should
19
- # prevent attribute assignments or removals of matching keys found in
20
- # the document
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
21
  # block - A block that can be used to retrieve external Asciidoc
22
22
  # data to include in this document.
23
23
  #
@@ -25,33 +25,63 @@ class Asciidoctor::Reader
25
25
  #
26
26
  # data = File.readlines(filename)
27
27
  # reader = Asciidoctor::Reader.new data
28
- def initialize(data = [], document = nil, overrides = nil, &block)
29
- # if document is nil, we assume this is a preprocessed string
30
- if document.nil?
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
31
33
  @lines = data.is_a?(String) ? data.lines.entries : data.dup
34
+ @preprocess_source = false
32
35
  elsif !data.empty?
33
- @overrides = overrides || {}
36
+ # NOTE we assume document is not nil!
34
37
  @document = document
35
- process(data.is_a?(String) ? data.lines.entries : data, &block)
38
+ @preprocess_source = true
39
+ @include_block = block_given? ? block : nil
40
+ normalize_data(data.is_a?(String) ? data.lines.entries : data)
36
41
  else
37
42
  @lines = []
43
+ @preprocess_source = false
38
44
  end
39
45
 
40
- @source = @lines.join
46
+ @source = @lines.dup
47
+
48
+ @next_line_preprocessed = false
49
+ @unescape_next_line = false
50
+
51
+ @conditionals_stack = []
52
+ @skipping = false
53
+ @eof = false
54
+ end
55
+
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
41
59
  end
42
60
 
43
61
  # Public: Check whether there are any lines left to read.
44
62
  #
45
- # Returns true if !@lines.empty? is true, or false otherwise.
46
- def has_lines?
47
- !@lines.empty?
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.
65
+ #
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
74
+ end
48
75
  end
49
76
 
50
77
  # Public: Check whether this reader is empty (contains no lines)
51
78
  #
52
- # Returns true if @lines.empty? is true, otherwise false.
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.
81
+ #
82
+ # Returns true if @lines is empty, otherwise false.
53
83
  def empty?
54
- @lines.empty?
84
+ !has_more_lines?
55
85
  end
56
86
 
57
87
  # Private: Strip off leading blank lines in the Array of lines.
@@ -61,25 +91,27 @@ class Asciidoctor::Reader
61
91
  # @lines
62
92
  # => ["\n", "\t\n", "Foo\n", "Bar\n", "\n"]
63
93
  #
64
- # skip_blank
94
+ # skip_blank_lines
65
95
  # => 2
66
96
  #
67
97
  # @lines
68
98
  # => ["Foo\n", "Bar\n"]
69
99
  #
70
100
  # Returns an Integer of the number of lines skipped
71
- def skip_blank
101
+ def skip_blank_lines
72
102
  skipped = 0
73
- while has_lines? && @lines.first.strip.empty?
74
- @lines.shift
75
- skipped += 1
76
- end
103
+ # optimized code for shortest execution path
104
+ while !(next_line = get_line).nil?
105
+ if next_line.chomp.empty?
106
+ skipped += 1
107
+ else
108
+ unshift_line next_line
109
+ break
110
+ end
111
+ end
77
112
 
78
113
  skipped
79
114
  end
80
- # Create alias of skip_blank named skip_blank_lines for readability
81
- # TODO likely want to drop the original method name
82
- alias :skip_blank_lines :skip_blank
83
115
 
84
116
  # Public: Consume consecutive lines containing line- or block-level comments.
85
117
  #
@@ -94,25 +126,27 @@ class Asciidoctor::Reader
94
126
  #
95
127
  # @lines
96
128
  # => ["actual text\n"]
97
- def consume_comments(opts = {})
129
+ def consume_comments(options = {})
98
130
  comment_lines = []
99
- while !@lines.empty?
100
- next_line = peek_line
101
- if opts[:include_blanks] && next_line.strip.empty?
102
- comment_lines << get_line
103
- elsif match = next_line.match(REGEXP[:comment_blk])
104
- comment_lines << get_line
105
- comment_lines.push(*(grab_lines_until(:terminator => match[0], :preserve_last_line => true)))
106
- comment_lines << get_line
107
- elsif next_line.match(REGEXP[:comment])
108
- comment_lines << get_line
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
135
+ 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)))
138
+ elsif commentish && next_line.match(REGEXP[:comment])
139
+ comment_lines << next_line
109
140
  else
141
+ # throw it back
142
+ unshift_line next_line
110
143
  break
111
144
  end
112
145
  end
113
146
 
114
147
  comment_lines
115
148
  end
149
+ alias :skip_comment_lines :consume_comments
116
150
 
117
151
  # Public: Consume consecutive lines containing line comments.
118
152
  #
@@ -129,10 +163,12 @@ class Asciidoctor::Reader
129
163
  # => ["bar\n"]
130
164
  def consume_line_comments
131
165
  comment_lines = []
132
- while !@lines.empty?
133
- if peek_line.match(REGEXP[:comment])
134
- comment_lines << get_line
166
+ # optimized code for shortest execution path
167
+ while !(next_line = get_line).nil?
168
+ if next_line.match(REGEXP[:comment])
169
+ comment_lines << next_line
135
170
  else
171
+ unshift_line next_line
136
172
  break
137
173
  end
138
174
  end
@@ -140,41 +176,302 @@ class Asciidoctor::Reader
140
176
  comment_lines
141
177
  end
142
178
 
143
- # Skip the next line if it's a list continuation character
144
- #
145
- # Returns nil
146
- def skip_list_continuation
147
- if has_lines? && @lines.first.chomp == '+'
148
- @lines.shift
149
- end
150
-
151
- nil
152
- end
153
-
154
179
  # Public: Get the next line of source data. Consumes the line returned.
155
180
  #
181
+ # preprocess - A Boolean flag indicating whether to evaluate preprocessing
182
+ # directives (macros) before reading line (default: true)
183
+ #
156
184
  # Returns the String of the next line of the source data if data is present.
157
185
  # Returns nil if there is no more data.
158
- def get_line
159
- @lines.shift
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
194
+ 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
202
+ end
203
+ end
204
+ end
205
+
206
+ # Public: Advance to the next line by discarding the line at the front of the stack
207
+ #
208
+ # Removes the line at the front of the stack without any processing.
209
+ #
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
160
221
  end
161
- # QUESTION what about advance?
162
- # Create an alias of get_line named next_line for readability
163
- alias :next_line :get_line
164
222
 
165
223
  # Public: Get the next line of source data. Does not consume the line returned.
166
224
  #
225
+ # preprocess - A Boolean flag indicating whether to evaluate preprocessing
226
+ # directives (macros) before reading line (default: true)
227
+ #
167
228
  # Returns a String dup of the next line of the source data if data is present.
168
229
  # Returns nil if there is no more data.
169
- def peek_line
170
- @lines.first.dup if @lines.first
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
237
+ else
238
+ nil
239
+ end
240
+ end
241
+
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
251
+ end
252
+ break if idx >= @lines.size
253
+ # QUESTION do we need to dup?
254
+ lines << @lines[idx].dup
255
+ idx += 1
256
+ end
257
+ lines
258
+ end
259
+
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?('if') && (match = next_line.match(REGEXP[:ifdef_macro]))
272
+ if next_line.start_with? '\\'
273
+ @next_line_preprocessed = true
274
+ @unescape_next_line = true
275
+ false
276
+ else
277
+ preprocess_conditional_inclusion(*match.captures)
278
+ end
279
+ elsif @skipping
280
+ 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
287
+ @unescape_next_line = true
288
+ false
289
+ else
290
+ preprocess_include(match[1])
291
+ end
292
+ else
293
+ @next_line_preprocessed = true
294
+ false
295
+ end
171
296
  end
172
297
 
173
- # Public: Push Array of string `lines` onto queue of source data lines, unless `lines` has no non-nil values.
298
+ # Internal: Preprocess the directive (macro) to conditionally include content.
299
+ #
300
+ # Preprocess the conditional inclusion directive (ifdef, ifndef, ifeval,
301
+ # endif) under the cursor. If the Reader is currently skipping content, then
302
+ # simply track the open and close delimiters of any nested conditional
303
+ # blocks. If the Reader is not skipping, mark whether the condition is
304
+ # satisfied and continue preprocessing recursively until the next line of
305
+ # available content is found.
306
+ #
307
+ # directive - The conditional inclusion directive (ifdef, ifndef, ifeval, endif)
308
+ # target - The target, which is the name of one or more attributes that are
309
+ # used in the condition (blank in the case of the ifeval directive)
310
+ # delimiter - The conditional delimiter for multiple attributes ('+' means all
311
+ # attributes must be defined or undefined, ',' means any of the attributes
312
+ # can be defined or undefined.
313
+ # text - The text associated with this directive (occurring between the square brackets)
314
+ # Used for a single-line conditional block in the case of the ifdef or
315
+ # ifndef directives, and for the conditional expression for the ifeval directive.
316
+ #
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)
320
+ # must have a target before brackets if ifdef or ifndef
321
+ # must not have text between brackets if endif
322
+ # don't honor match if it doesn't meet this criteria
323
+ if ((directive == 'ifdef' || directive == 'ifndef') && target.empty?) ||
324
+ (directive == 'endif' && !text.nil?)
325
+ @next_line_preprocessed = true
326
+ return false
327
+ end
328
+
329
+ if directive == 'endif'
330
+ stack_size = @conditionals_stack.size
331
+ if stack_size > 0
332
+ pair = @conditionals_stack.last
333
+ if target.empty? || target == pair[:target]
334
+ @conditionals_stack.pop
335
+ @skipping = @conditionals_stack.empty? ? false : @conditionals_stack.last[:skipping]
336
+ else
337
+ puts "asciidoctor: ERROR: line #{@lineno + 1}: mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[]"
338
+ end
339
+ else
340
+ puts "asciidoctor: ERROR: line #{@lineno + 1}: unmatched macro: endif::#{target}[]"
341
+ end
342
+ advance
343
+ return preprocess_next_line.nil? ? nil : true
344
+ end
345
+
346
+ skip = nil
347
+ if !@skipping
348
+ case directive
349
+ when 'ifdef'
350
+ case delimiter
351
+ when nil
352
+ # if the attribute is undefined, then skip
353
+ skip = !@document.attributes.has_key?(target)
354
+ when ','
355
+ # if any attribute is defined, then don't skip
356
+ skip = !target.split(',').detect {|name| @document.attributes.has_key? name }
357
+ when '+'
358
+ # if any attribute is undefined, then skip
359
+ skip = target.split('+').detect {|name| !@document.attributes.has_key? name }
360
+ end
361
+ when 'ifndef'
362
+ case delimiter
363
+ when nil
364
+ # if the attribute is defined, then skip
365
+ skip = @document.attributes.has_key?(target)
366
+ when ','
367
+ # if any attribute is undefined, then don't skip
368
+ skip = !target.split(',').detect {|name| !@document.attributes.has_key? name }
369
+ when '+'
370
+ # if any attribute is defined, then skip
371
+ skip = target.split('+').detect {|name| @document.attributes.has_key? name }
372
+ end
373
+ when 'ifeval'
374
+ # the text in brackets must match an expression
375
+ # don't honor match if it doesn't meet this criteria
376
+ if !target.empty? || !(expr_match = text.strip.match(REGEXP[:eval_expr]))
377
+ @next_line_preprocessed = true
378
+ return false
379
+ end
380
+
381
+ lhs = resolve_expr_val(expr_match[1])
382
+ op = expr_match[2]
383
+ rhs = resolve_expr_val(expr_match[3])
384
+
385
+ skip = !lhs.send(op.to_sym, rhs)
386
+ end
387
+ @skipping = skip
388
+ end
389
+ advance
390
+ # single line conditional inclusion
391
+ if directive != 'ifeval' && !text.nil?
392
+ if !@skipping
393
+ unshift_line "#{text.rstrip}\n"
394
+ return true
395
+ end
396
+ # conditional inclusion block
397
+ else
398
+ @conditionals_stack << {:target => target, :skip => skip, :skipping => @skipping}
399
+ end
400
+ return preprocess_next_line.nil? ? nil : true
401
+ end
402
+
403
+ # Internal: Preprocess the directive (macro) to include the target document.
404
+ #
405
+ # Preprocess the directive to include the target document. The scenarios
406
+ # are as follows:
407
+ #
408
+ # If SafeMode is SECURE or greater, the directive is ignore and the include
409
+ # directive line is emitted verbatim.
410
+ #
411
+ # Otherwise, if an include handler is specified (currently controlled by a
412
+ # closure block), pass the target to that block and expect an Array of String
413
+ # lines in return.
414
+ #
415
+ # Otherwise, if the include-depth attribute is greater than 0, normalize the
416
+ # target path and read the lines onto the beginning of the Array of source
417
+ # data.
418
+ #
419
+ # If none of the above apply, emit the include directive line verbatim.
420
+ #
421
+ # target - The name of the source document to include as specified in the
422
+ # target slot of the include::[] macro
423
+ #
424
+ # returns a Boolean indicating whether the line under the cursor has changed.
425
+ def preprocess_include(target)
426
+ # if running in SafeMode::SECURE or greater, don't process this directive
427
+ if @document.safe >= SafeMode::SECURE
428
+ @next_line_preprocessed = true
429
+ false
430
+ # assume that if a block is given, the developer wants
431
+ # to handle when and how to process the include, even
432
+ # if the include-depth attribute is 0
433
+ elsif @include_block
434
+ advance
435
+ # FIXME this borks line numbers
436
+ @lines.unshift(*@include_block.call(target).map {|l| "#{l.rstrip}\n"})
437
+ # FIXME currently we're not checking the upper bound of the include depth
438
+ elsif @document.attributes.fetch('include-depth', 0).to_i > 0
439
+ advance
440
+ # FIXME this borks line numbers
441
+ @lines.unshift(*File.readlines(@document.normalize_asset_path(target, 'include file')).map {|l| "#{l.rstrip}\n"})
442
+ true
443
+ else
444
+ @next_line_preprocessed = true
445
+ false
446
+ end
447
+ end
448
+
449
+ # Public: Push the String line onto the beginning of the Array of source data.
450
+ #
451
+ # Since this line was (assumed to be) previously retrieved through the
452
+ # reader, it is marked as preprocessed.
453
+ #
454
+ # returns nil
455
+ def unshift_line(line)
456
+ @lines.unshift line
457
+ @next_line_preprocessed = true
458
+ @eof = false
459
+ @lineno -= 1
460
+ nil
461
+ end
462
+
463
+ # Public: Push Array of lines onto the front of the Array of source data, unless `lines` has no non-nil values.
174
464
  #
175
465
  # Returns nil
176
466
  def unshift(*new_lines)
177
- @lines.unshift(*new_lines) if !new_lines.empty?
467
+ size = new_lines.size
468
+ if size > 0
469
+ @lines.unshift(*new_lines)
470
+ # assume that what we are putting back on is already processed for directives
471
+ @next_line_preprocessed = true
472
+ @eof = false
473
+ @lineno -= size
474
+ end
178
475
  nil
179
476
  end
180
477
 
@@ -184,7 +481,7 @@ class Asciidoctor::Reader
184
481
  #
185
482
  # Returns nil
186
483
  def chomp_last!
187
- @lines.last.chomp! unless @lines.empty?
484
+ @lines.last.chomp! unless @eof || (@eof = @lines.empty?)
188
485
  nil
189
486
  end
190
487
 
@@ -218,27 +515,27 @@ class Asciidoctor::Reader
218
515
  buffer = []
219
516
 
220
517
  finis = false
221
- get_line if options[:skip_first_line]
518
+ advance if options[:skip_first_line]
222
519
  # save options to locals for minor optimization
223
520
  terminator = options[:terminator]
224
521
  terminator.chomp! if terminator
225
522
  break_on_blank_lines = options[:break_on_blank_lines]
226
523
  break_on_list_continuation = options[:break_on_list_continuation]
227
- while (this_line = self.get_line)
228
- Asciidoctor.debug { "Reader processing line: '#{this_line}'" }
524
+ skip_line_comments = options[:skip_line_comments]
525
+ preprocess = options.fetch(:preprocess, true)
526
+ while !(this_line = get_line(preprocess)).nil?
527
+ Debug.debug { "Reader processing line: '#{this_line}'" }
229
528
  finis = true if terminator && this_line.chomp == terminator
230
529
  finis = true if !finis && break_on_blank_lines && this_line.strip.empty?
231
530
  finis = true if !finis && break_on_list_continuation && this_line.chomp == LIST_CONTINUATION
232
531
  finis = true if !finis && block && yield(this_line)
233
532
  if finis
234
- self.unshift(this_line) if options[:preserve_last_line]
235
533
  buffer << this_line if options[:grab_last_line]
534
+ unshift_line(this_line) if options[:preserve_last_line]
236
535
  break
237
536
  end
238
537
 
239
- if options[:skip_line_comments] && this_line.match(REGEXP[:comment])
240
- # skip it
241
- else
538
+ unless skip_line_comments && this_line.match(REGEXP[:comment])
242
539
  buffer << this_line
243
540
  end
244
541
  end
@@ -263,109 +560,63 @@ class Asciidoctor::Reader
263
560
  # sanitize_attribute_name('Foo 3 #-Billy')
264
561
  # => 'foo3-billy'
265
562
  def sanitize_attribute_name(name)
266
- name.gsub(/[^\w\-]/, '').downcase
563
+ Lexer.sanitize_attribute_name(name)
267
564
  end
268
565
 
269
- # Private: Process raw input, used for the outermost reader.
270
- def process(data, &block)
271
- raw_source = []
272
- include_depth = @document.attr('include-depth', 0).to_i
273
-
274
- data.each do |line|
275
- if inc = line.match(REGEXP[:include_macro])
276
- if inc[0].start_with? '\\'
277
- raw_source << line[1..-1]
278
- # if running in SafeMode::SECURE or greater, don't process
279
- # this directive (or should we swallow it?)
280
- elsif @document.safe >= SafeMode::SECURE
281
- raw_source << line
282
- # assume that if a block is given, the developer wants
283
- # to handle when and how to process the include, even
284
- # if the include-depth attribute is 0
285
- elsif block_given?
286
- raw_source.concat yield(inc[1])
287
- elsif include_depth > 0
288
- raw_source.concat File.readlines(@document.normalize_asset_path(inc[1], 'include file'))
289
- else
290
- raw_source << line
291
- end
292
- else
293
- raw_source << line
294
- end
566
+ # Private: Resolve the value of one side of the expression
567
+ def resolve_expr_val(str)
568
+ val = str
569
+ type = nil
570
+
571
+ if val.start_with?('"') && val.end_with?('"') ||
572
+ val.start_with?('\'') && val.end_with?('\'')
573
+ type = :s
574
+ val = val[1..-2]
295
575
  end
296
576
 
297
- skip_to = nil
298
- continuing_value = nil
299
- continuing_key = nil
300
- @lines = []
301
-
302
- raw_source.each do |line|
303
- # normalize line ending to LF (purging occurrences of CRLF)
304
- line = "#{line.rstrip}\n"
305
- if skip_to
306
- skip_to = nil if line.match(skip_to)
307
- elsif continuing_value
308
- close_continue = false
309
- # Lines that start with whitespace and end with a '+' are
310
- # a continuation, so gobble them up into `value`
311
- if line.match(REGEXP[:attr_continue])
312
- continuing_value += ' ' + $1.rstrip
313
- # An empty line ends a continuation
314
- elsif line.strip.empty?
315
- raw_source.unshift(line)
316
- close_continue = true
317
- else
318
- # If this continued line isn't empty and doesn't end with a +, then
319
- # this is the end of the continuation, no matter what the next line
320
- # does.
321
- continuing_value += ' ' + line.strip
322
- close_continue = true
323
- end
324
- if close_continue
325
- unless attribute_overridden? continuing_key
326
- @document.attributes[continuing_key] = apply_attribute_value_subs(continuing_value)
327
- end
328
- continuing_key = nil
329
- continuing_value = nil
330
- end
331
- elsif line.match(REGEXP[:ifdef_macro])
332
- attr = $2
333
- skip = case $1
334
- when 'ifdef'; !@document.attributes.has_key?(attr)
335
- when 'ifndef'; @document.attributes.has_key?(attr)
336
- end
337
- skip_to = /^endif::#{attr}\[\]\s*\n/ if skip
338
- elsif line.match(REGEXP[:attr_assign])
339
- key = sanitize_attribute_name($1)
340
- value = $2
341
- if value.match(REGEXP[:attr_continue])
342
- # attribute value continuation line; grab lines until we run out
343
- # of continuation lines
344
- continuing_key = key
345
- continuing_value = $1.rstrip # strip off the spaces and +
346
- else
347
- unless attribute_overridden? key
348
- @document.attributes[key] = apply_attribute_value_subs(value)
349
- if key == 'backend'
350
- @document.update_backend_attributes()
351
- end
352
- end
353
- end
354
- elsif line.match(REGEXP[:attr_delete])
355
- key = sanitize_attribute_name($1)
356
- unless attribute_overridden? key
357
- @document.attributes.delete(key)
358
- end
359
- elsif !line.match(REGEXP[:endif_macro])
360
- while line.match(REGEXP[:attr_conditional])
361
- value = @document.attributes.has_key?($1) ? $2 : ''
362
- line.sub!(REGEXP[:attr_conditional], value)
363
- end
364
- # NOTE leave line comments in as they play a role in flow (such as a list divider)
365
- @lines << line
577
+ if val.include? '{'
578
+ val = @document.sub_attributes(val)
579
+ end
580
+
581
+ if type != :s
582
+ if val.empty?
583
+ val = nil
584
+ elsif val == 'true'
585
+ val = true
586
+ elsif val == 'false'
587
+ val = false
588
+ elsif val.include?('.')
589
+ val = val.to_f
590
+ else
591
+ val = val.to_i
366
592
  end
367
593
  end
368
594
 
595
+ val
596
+ end
597
+
598
+ # Private: Normalize raw input, used for the outermost Reader.
599
+ #
600
+ # This method strips whitespace from the end of every line of
601
+ # the source data and appends a LF (i.e., Unix endline). This
602
+ # whitespace substitution is very important to how Asciidoctor
603
+ # works.
604
+ #
605
+ # Any leading or trailing blank lines are also removed.
606
+ #
607
+ # The normalized lines are assigned to the @lines instance variable.
608
+ #
609
+ # data - A String Array of input data to be normalized
610
+ #
611
+ # returns nothing
612
+ def normalize_data(data)
613
+ # normalize line ending to LF (purging occurrences of CRLF)
614
+ # this rstrip is *very* important to how Asciidoctor works
615
+ @lines = data.map {|line| "#{line.rstrip}\n" }
616
+
617
+ @lines.shift && @lineno += 1 while !@lines.first.nil? && @lines.first.chomp.empty?
618
+ @lines.pop while !@lines.last.nil? && @lines.last.chomp.empty?
619
+
369
620
  # Process bibliography references, so they're available when text
370
621
  # before the reference is being rendered.
371
622
  # FIXME we don't have support for bibliography lists yet, so disable for now
@@ -375,41 +626,7 @@ class Asciidoctor::Reader
375
626
  # @document.register(:ids, biblio[1])
376
627
  # end
377
628
  #end
629
+ nil
378
630
  end
379
-
380
- # Internal: Determine if the attribute has been overridden in the document options
381
- #
382
- # key - The attribute key to check
383
- #
384
- # Returns true if the attribute has been overridden, false otherwise
385
- def attribute_overridden?(key)
386
- @overrides.has_key?(key) || @overrides.has_key?(key + '!')
387
- end
388
-
389
- # Internal: Apply substitutions to the attribute value
390
- #
391
- # If the value is an inline passthrough macro (e.g., pass:[text]), then
392
- # apply the substitutions defined on the macro to the text. Otherwise,
393
- # apply the verbatim substitutions to the value.
394
- #
395
- # value - The String attribute value on which to perform substitutions
396
- #
397
- # Returns The String value with substitutions performed.
398
- def apply_attribute_value_subs(value)
399
- if value.match(REGEXP[:pass_macro_basic])
400
- # copy match for Ruby 1.8.7 compat
401
- m = $~
402
- subs = []
403
- if !m[1].empty?
404
- subs = @document.resolve_subs(m[1])
405
- end
406
- if !subs.empty?
407
- @document.apply_subs(m[2], subs)
408
- else
409
- m[2]
410
- end
411
- else
412
- @document.apply_header_subs(value)
413
- end
414
- end
631
+ end
415
632
  end