asciidoctor 0.1.1 → 0.1.2

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

Potentially problematic release.


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

Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +1 -1
  3. data/LICENSE +2 -2
  4. data/README.adoc +461 -0
  5. data/asciidoctor.gemspec +27 -16
  6. data/compat/asciidoc.conf +139 -0
  7. data/lib/asciidoctor.rb +212 -69
  8. data/lib/asciidoctor/abstract_block.rb +41 -0
  9. data/lib/asciidoctor/abstract_node.rb +128 -81
  10. data/lib/asciidoctor/attribute_list.rb +5 -2
  11. data/lib/asciidoctor/backends/base_template.rb +16 -4
  12. data/lib/asciidoctor/backends/docbook45.rb +112 -42
  13. data/lib/asciidoctor/backends/html5.rb +206 -90
  14. data/lib/asciidoctor/block.rb +5 -5
  15. data/lib/asciidoctor/cli/invoker.rb +38 -34
  16. data/lib/asciidoctor/cli/options.rb +3 -3
  17. data/lib/asciidoctor/document.rb +115 -13
  18. data/lib/asciidoctor/helpers.rb +16 -0
  19. data/lib/asciidoctor/lexer.rb +486 -359
  20. data/lib/asciidoctor/path_resolver.rb +360 -0
  21. data/lib/asciidoctor/reader.rb +122 -23
  22. data/lib/asciidoctor/renderer.rb +1 -33
  23. data/lib/asciidoctor/section.rb +1 -1
  24. data/lib/asciidoctor/substituters.rb +103 -19
  25. data/lib/asciidoctor/version.rb +1 -1
  26. data/man/asciidoctor.1 +6 -6
  27. data/man/asciidoctor.ad +5 -3
  28. data/stylesheets/asciidoctor.css +274 -0
  29. data/test/attributes_test.rb +133 -10
  30. data/test/blocks_test.rb +302 -17
  31. data/test/document_test.rb +269 -6
  32. data/test/fixtures/basic-docinfo.html +1 -0
  33. data/test/fixtures/basic-docinfo.xml +4 -0
  34. data/test/fixtures/basic.asciidoc +4 -0
  35. data/test/fixtures/docinfo.html +1 -0
  36. data/test/fixtures/docinfo.xml +2 -0
  37. data/test/fixtures/include-file.asciidoc +22 -1
  38. data/test/fixtures/stylesheets/custom.css +3 -0
  39. data/test/invoker_test.rb +38 -6
  40. data/test/lexer_test.rb +64 -21
  41. data/test/links_test.rb +4 -0
  42. data/test/lists_test.rb +251 -12
  43. data/test/paragraphs_test.rb +225 -30
  44. data/test/paths_test.rb +174 -0
  45. data/test/reader_test.rb +89 -2
  46. data/test/sections_test.rb +518 -16
  47. data/test/substitutions_test.rb +121 -10
  48. data/test/tables_test.rb +53 -13
  49. data/test/test_helper.rb +2 -2
  50. data/test/text_test.rb +5 -5
  51. metadata +46 -50
  52. data/README.asciidoc +0 -296
  53. data/lib/asciidoctor/errors.rb +0 -5
@@ -0,0 +1,360 @@
1
+ module Asciidoctor
2
+ # Public: Handles all operations for resolving, cleaning and joining paths.
3
+ # This class includes operations for handling both web paths (request URIs) and
4
+ # system paths.
5
+ #
6
+ # The main emphasis of the class is on creating clean and secure paths. Clean
7
+ # paths are void of duplicate parent and current directory references in the
8
+ # path name. Secure paths are paths which are restricted from accessing
9
+ # directories outside of a jail root, if specified.
10
+ #
11
+ # Since joining two paths can result in an insecure path, this class also
12
+ # handles the task of joining a parent (start) and child (target) path.
13
+ #
14
+ # This class makes no use of path utilities from the Ruby libraries. Instead,
15
+ # it handles all aspects of path manipulation. The main benefit of
16
+ # internalizing these operations is that the class is able to handle both posix
17
+ # and windows paths independent of the operating system on which it runs. This
18
+ # makes the class both deterministic and easier to test.
19
+ #
20
+ # Examples
21
+ #
22
+ # resolver = PathResolver.new
23
+ #
24
+ # # Web Paths
25
+ #
26
+ # resolver.web_path('images')
27
+ # => 'images'
28
+ #
29
+ # resolver.web_path('./images')
30
+ # => './images'
31
+ #
32
+ # resolver.web_path('/images')
33
+ # => '/images'
34
+ #
35
+ # resolver.web_path('./images/../assets/images')
36
+ # => './assets/images'
37
+ #
38
+ # resolver.web_path('/../images')
39
+ # => '/images'
40
+ #
41
+ # resolver.web_path('images', 'assets')
42
+ # => 'assets/images'
43
+ #
44
+ # resolver.web_path('tiger.png', '../assets/images')
45
+ # => '../assets/images/tiger.png'
46
+ #
47
+ # # System Paths
48
+ #
49
+ # resolver.working_dir
50
+ # => '/path/to/docs'
51
+ #
52
+ # resolver.system_path('images')
53
+ # => '/path/to/docs/images'
54
+ #
55
+ # resolver.system_path('../images')
56
+ # => '/path/to/images'
57
+ #
58
+ # resolver.system_path('/etc/images')
59
+ # => '/etc/images'
60
+ #
61
+ # resolver.system_path('images', '/etc')
62
+ # => '/etc/images'
63
+ #
64
+ # resolver.system_path('', '/etc/images')
65
+ # => '/etc/images'
66
+ #
67
+ # resolver.system_path(nil, nil, '/path/to/docs')
68
+ # => '/path/to/docs'
69
+ #
70
+ # resolver.system_path('..', nil, '/path/to/docs')
71
+ # => '/path/to/docs'
72
+ #
73
+ # resolver.system_path('../../../css', nil, '/path/to/docs')
74
+ # => '/path/to/docs/css'
75
+ #
76
+ # resolver.system_path('../../../css', '../../..', '/path/to/docs')
77
+ # => '/path/to/docs/css'
78
+ #
79
+ # begin
80
+ # resolver.system_path('../../../css', '../../..', '/path/to/docs', :recover => false)
81
+ # rescue SecurityError => e
82
+ # puts e.message
83
+ # end
84
+ # => 'path ../../../../../../css refers to location outside jail: /path/to/docs (disallowed in safe mode)'
85
+ #
86
+ # resolver.system_path('/path/to/docs/images', nil, '/path/to/docs')
87
+ # => '/path/to/docs/images'
88
+ #
89
+ # begin
90
+ # resolver.system_path('images', '/etc', '/path/to/docs')
91
+ # rescue SecurityError => e
92
+ # puts e.message
93
+ # end
94
+ # => Start path /etc is outside of jail: /path/to/docs'
95
+ #
96
+ class PathResolver
97
+ DOT = '.'
98
+ DOT_DOT = '..'
99
+ SLASH = '/'
100
+ BACKSLASH = '\\'
101
+ PARTITION_RE = /\/+/
102
+ WIN_ROOT_RE = /^[[:alpha:]]:(?:\\|\/)/
103
+
104
+ attr_accessor :file_separator
105
+ attr_accessor :working_dir
106
+
107
+ # Public: Construct a new instance of PathResolver, optionally specifying the
108
+ # path separator (to override the system default) and the working directory
109
+ # (to override the present working directory). The working directory will be
110
+ # expanded to an absolute path inside the constructor.
111
+ #
112
+ # file_separator - the String file separator to use for path operations
113
+ # (optional, default: File::FILE_SEPARATOR)
114
+ # working_dir - the String working directory (optional, default: Dir.pwd)
115
+ #
116
+ def initialize(file_separator = nil, working_dir = nil)
117
+ @file_separator = file_separator.nil? ? File::SEPARATOR : file_separator
118
+ if working_dir.nil?
119
+ @working_dir = File.expand_path(Dir.pwd)
120
+ else
121
+ @working_dir = is_root?(working_dir) ? working_dir : File.expand_path(working_dir)
122
+ end
123
+ end
124
+
125
+ # Public: Check if the specified path is an absolute root path
126
+ # This operation correctly handles both posix and windows paths.
127
+ #
128
+ # path - the String path to check
129
+ #
130
+ # returns a Boolean indicating whether the path is an absolute root path
131
+ def is_root?(path)
132
+ if @file_separator == BACKSLASH && path.match(WIN_ROOT_RE)
133
+ true
134
+ elsif path.start_with? SLASH
135
+ true
136
+ else
137
+ false
138
+ end
139
+ end
140
+
141
+ # Public: Determine if the path is an absolute (root) web path
142
+ #
143
+ # path - the String path to check
144
+ #
145
+ # returns a Boolean indicating whether the path is an absolute (root) web path
146
+ def is_web_root?(path)
147
+ path.start_with? SLASH
148
+ end
149
+
150
+ # Public: Normalize path by converting any backslashes to forward slashes
151
+ #
152
+ # path - the String path to normalize
153
+ #
154
+ # returns a String path with any backslashes replaced with forward slashes
155
+ def posixfy(path)
156
+ return '' if path.to_s.empty?
157
+ path.include?(BACKSLASH) ? path.tr(BACKSLASH, SLASH) : path
158
+ end
159
+
160
+ # Public: Expand the path by resolving any parent references (..)
161
+ # and cleaning self references (.).
162
+ #
163
+ # The result will be relative if the path is relative and
164
+ # absolute if the path is absolute. The file separator used
165
+ # in the expanded path is the one specified when the class
166
+ # was constructed.
167
+ #
168
+ # path - the String path to expand
169
+ #
170
+ # returns a String path with any parent or self references resolved.
171
+ def expand_path(path)
172
+ path_segments, path_root, _ = partition_path(path)
173
+ join_path(path_segments, path_root)
174
+ end
175
+
176
+ # Public: Partition the path into path segments and remove any empty segments
177
+ # or segments that are self references (.). The path is split on either posix
178
+ # or windows file separators.
179
+ #
180
+ # path - the String path to partition
181
+ # web_path - a Boolean indicating whether the path should be handled
182
+ # as a web path (optional, default: false)
183
+ #
184
+ # returns a 3-item Array containing the Array of String path segments, the
185
+ # path root, if the path is absolute, and the posix version of the path.
186
+ def partition_path(path, web_path = false)
187
+ posix_path = posixfy path
188
+ is_root = web_path ? is_web_root?(posix_path) : is_root?(posix_path)
189
+ path_segments = posix_path.split(PARTITION_RE)
190
+ # capture relative root
191
+ root = path_segments.first == DOT ? DOT : nil
192
+ path_segments.delete(DOT)
193
+ # capture absolute root, preserving relative root if set
194
+ root = is_root ? path_segments.shift : root
195
+
196
+ [path_segments, root, posix_path]
197
+ end
198
+
199
+ # Public: Join the segments using the file separator specified in the
200
+ # constructor. Use the root, if specified, to construct an absolute path.
201
+ # Otherwise join the segments as a relative path.
202
+ #
203
+ # segments - a String Array of path segments
204
+ # root - a String path root (optional, default: nil)
205
+ #
206
+ # returns a String path formed by joining the segments and prepending
207
+ # the root, if specified
208
+ def join_path(segments, root = nil)
209
+ if root
210
+ "#{root}#{@file_separator}#{segments * @file_separator}"
211
+ else
212
+ segments * @file_separator
213
+ end
214
+ end
215
+
216
+ # Public: Resolve a system path from the target and start paths. If a jail
217
+ # path is specified, enforce that the resolved directory is contained within
218
+ # the jail path. If a jail path is not provided, the resolved path may be
219
+ # any location on the system. If the resolved path is absolute, use it as is.
220
+ # If the resolved path is relative, resolve it relative to the working_dir
221
+ # specified in the constructor.
222
+ #
223
+ # target - the String target path
224
+ # start - the String start (i.e., parent) path
225
+ # jail - the String jail path to confine the resolved path
226
+ # opts - an optional Hash of options to control processing (default: {}):
227
+ # * :recover is used to control whether the processor should auto-recover
228
+ # when an illegal path is encountered
229
+ # * :target_name is used in messages to refer to the path being resolved
230
+ #
231
+ # returns a String path that joins the target path with the start path with
232
+ # any parent references resolved and self references removed and enforces
233
+ # that the resolved path be contained within the jail, if provided
234
+ def system_path(target, start, jail = nil, opts = {})
235
+ recover = opts.fetch(:recover, true)
236
+ unless jail.nil?
237
+ unless is_root? jail
238
+ raise SecurityError, "Jail is not an absolute path: #{jail}"
239
+ end
240
+ jail = posixfy jail
241
+ end
242
+
243
+ if target.to_s.empty?
244
+ target_segments = []
245
+ else
246
+ target_segments, target_root, _ = partition_path(target)
247
+ end
248
+
249
+ if target_segments.empty?
250
+ if start.to_s.empty?
251
+ return jail.nil? ? @working_dir : jail
252
+ elsif is_root? start
253
+ if jail.nil?
254
+ return expand_path start
255
+ end
256
+ else
257
+ return system_path(start, jail, jail)
258
+ end
259
+ end
260
+
261
+ if target_root && target_root != DOT
262
+ resolved_target = join_path target_segments, target_root
263
+ # if target is absolute and a sub-directory of jail, or
264
+ # a jail is not in place, let it slide
265
+ if jail.nil? || resolved_target.start_with?(jail)
266
+ return resolved_target
267
+ end
268
+ end
269
+
270
+ if start.to_s.empty?
271
+ start = jail.nil? ? @working_dir : jail
272
+ elsif is_root? start
273
+ start = posixfy start
274
+ else
275
+ start = system_path(start, jail, jail)
276
+ end
277
+
278
+ # both jail and start have been posixfied at this point
279
+ if jail == start
280
+ jail_segments, jail_root, _ = partition_path(jail)
281
+ start_segments = jail_segments.dup
282
+ elsif !jail.nil?
283
+ if !start.start_with?(jail)
284
+ raise SecurityError, "#{opts[:target_name] || 'Start path'} #{start} is outside of jail: #{jail} (disallowed in safe mode)"
285
+ end
286
+
287
+ start_segments, start_root, _ = partition_path(start)
288
+ jail_segments, jail_root, _ = partition_path(jail)
289
+
290
+ # Already checked for this condition
291
+ #if start_root != jail_root
292
+ # raise SecurityError, "Jail root #{jail_root} does not match root of #{opts[:target_name] || 'start path'}: #{start_root}"
293
+ #end
294
+ else
295
+ start_segments, start_root, _ = partition_path(start)
296
+ jail_root = start_root
297
+ end
298
+
299
+ resolved_segments = start_segments.dup
300
+ warned = false
301
+ target_segments.each do |segment|
302
+ if segment == DOT_DOT
303
+ if !jail.nil?
304
+ if resolved_segments.length > jail_segments.length
305
+ resolved_segments.pop
306
+ elsif !recover
307
+ raise SecurityError, "#{opts[:target_name] || 'path'} #{target} refers to location outside jail: #{jail} (disallowed in safe mode)"
308
+ elsif !warned
309
+ puts "asciidoctor: WARNING: #{opts[:target_name] || 'path'} has illegal reference to ancestor of jail, auto-recovering"
310
+ warned = true
311
+ end
312
+ else
313
+ resolved_segments.pop
314
+ end
315
+ else
316
+ resolved_segments.push segment
317
+ end
318
+ end
319
+
320
+ join_path resolved_segments, jail_root
321
+ end
322
+
323
+ # Public: Resolve a web path from the target and start paths.
324
+ # The main function of this operation is to resolve any parent
325
+ # references and remove any self references.
326
+ #
327
+ # target - the String target path
328
+ # start - the String start (i.e., parent) path
329
+ #
330
+ # returns a String path that joins the target path with the
331
+ # start path with any parent references resolved and self
332
+ # references removed
333
+ def web_path(target, start = nil)
334
+ target = posixfy(target)
335
+ start = posixfy(start)
336
+
337
+ unless is_web_root?(target) || start.empty?
338
+ target = "#{start}#{SLASH}#{target}"
339
+ end
340
+
341
+ target_segments, target_root, _ = partition_path(target, true)
342
+ resolved_segments = target_segments.inject([]) do |accum, segment|
343
+ if segment == DOT_DOT
344
+ if accum.empty?
345
+ accum.push segment unless target_root && target_root != DOT
346
+ elsif accum[-1] == DOT_DOT
347
+ accum.push segment
348
+ else
349
+ accum.pop
350
+ end
351
+ else
352
+ accum.push segment
353
+ end
354
+ accum
355
+ end
356
+
357
+ join_path resolved_segments, target_root
358
+ end
359
+ end
360
+ end
@@ -268,7 +268,7 @@ class Reader
268
268
  def preprocess_next_line
269
269
  # this return could be happening from a recursive call
270
270
  return nil if @eof || (next_line = @lines.first).nil?
271
- if next_line.include?('if') && (match = next_line.match(REGEXP[:ifdef_macro]))
271
+ if next_line.include?('::') && (next_line.include?('if') || next_line.include?('endif')) && (match = next_line.match(REGEXP[:ifdef_macro]))
272
272
  if next_line.start_with? '\\'
273
273
  @next_line_preprocessed = true
274
274
  @unescape_next_line = true
@@ -287,7 +287,7 @@ class Reader
287
287
  @unescape_next_line = true
288
288
  false
289
289
  else
290
- preprocess_include(match[1])
290
+ preprocess_include(match[1], match[2].strip)
291
291
  end
292
292
  else
293
293
  @next_line_preprocessed = true
@@ -343,7 +343,7 @@ class Reader
343
343
  return preprocess_next_line.nil? ? nil : true
344
344
  end
345
345
 
346
- skip = nil
346
+ skip = false
347
347
  if !@skipping
348
348
  case directive
349
349
  when 'ifdef'
@@ -384,17 +384,19 @@ class Reader
384
384
 
385
385
  skip = !lhs.send(op.to_sym, rhs)
386
386
  end
387
- @skipping = skip
388
387
  end
389
388
  advance
390
389
  # single line conditional inclusion
391
390
  if directive != 'ifeval' && !text.nil?
392
- if !@skipping
391
+ if !@skipping && !skip
393
392
  unshift_line "#{text.rstrip}\n"
394
393
  return true
395
394
  end
396
395
  # conditional inclusion block
397
396
  else
397
+ if !@skipping && skip
398
+ @skipping = true
399
+ end
398
400
  @conditionals_stack << {:target => target, :skip => skip, :skipping => @skipping}
399
401
  end
400
402
  return preprocess_next_line.nil? ? nil : true
@@ -422,9 +424,11 @@ class Reader
422
424
  # target slot of the include::[] macro
423
425
  #
424
426
  # returns a Boolean indicating whether the line under the cursor has changed.
425
- def preprocess_include(target)
427
+ def preprocess_include(target, raw_attributes)
426
428
  # if running in SafeMode::SECURE or greater, don't process this directive
429
+ # however, be friendly and at least make it a link to the source document
427
430
  if @document.safe >= SafeMode::SECURE
431
+ @lines[0] = "link:#{target}[#{target}]"
428
432
  @next_line_preprocessed = true
429
433
  false
430
434
  # assume that if a block is given, the developer wants
@@ -438,7 +442,81 @@ class Reader
438
442
  elsif @document.attributes.fetch('include-depth', 0).to_i > 0
439
443
  advance
440
444
  # FIXME this borks line numbers
441
- @lines.unshift(*File.readlines(@document.normalize_asset_path(target, 'include file')).map {|l| "#{l.rstrip}\n"})
445
+ include_file = @document.normalize_system_path(target, nil, nil, :target_name => 'include file')
446
+ if !File.file?(include_file)
447
+ puts "asciidoctor: WARNING: line #{@lineno}: include file not found: #{include_file}"
448
+ return true
449
+ end
450
+
451
+ lines = nil
452
+ tags = nil
453
+ if !raw_attributes.empty?
454
+ attributes = AttributeList.new(raw_attributes).parse
455
+ if attributes.has_key? 'lines'
456
+ lines = []
457
+ attributes['lines'].split(REGEXP[:scsv_csv_delim]).each do |linedef|
458
+ if linedef.include?('..')
459
+ from, to = linedef.split('..').map(&:to_i)
460
+ if to == -1
461
+ lines << from
462
+ lines << 1.0/0.0
463
+ else
464
+ lines.concat Range.new(from, to).to_a
465
+ end
466
+ else
467
+ lines << linedef.to_i
468
+ end
469
+ end
470
+ lines = lines.sort.uniq
471
+ #lines.push lines.shift if lines.first == -1
472
+ elsif attributes.has_key? 'tags'
473
+ tags = attributes['tags'].split(REGEXP[:scsv_csv_delim]).uniq
474
+ end
475
+ end
476
+ if !lines.nil?
477
+ if !lines.empty?
478
+ selected = []
479
+ f = File.new(include_file)
480
+ f.each_line do |l|
481
+ take = lines.first
482
+ if take.is_a?(Float) && take.infinite?
483
+ selected.push("#{l.rstrip}\n")
484
+ else
485
+ if f.lineno == take
486
+ selected.push("#{l.rstrip}\n")
487
+ lines.shift
488
+ end
489
+ break if lines.empty?
490
+ end
491
+ end
492
+ @lines.unshift(*selected) unless selected.empty?
493
+ end
494
+ elsif !tags.nil?
495
+ if !tags.empty?
496
+ selected = []
497
+ active_tag = nil
498
+ f = File.new(include_file)
499
+ f.each_line do |l|
500
+ if !active_tag.nil?
501
+ if l.include?("end::#{active_tag}[]")
502
+ active_tag = nil
503
+ else
504
+ selected.push("#{l.rstrip}\n")
505
+ end
506
+ else
507
+ tags.each do |tag|
508
+ if l.include?("tag::#{tag}[]")
509
+ active_tag = tag
510
+ break
511
+ end
512
+ end
513
+ end
514
+ end
515
+ @lines.unshift(*selected) unless selected.empty?
516
+ end
517
+ else
518
+ @lines.unshift(*File.readlines(include_file).map {|l| "#{l.rstrip}\n"})
519
+ end
442
520
  true
443
521
  else
444
522
  @next_line_preprocessed = true
@@ -514,32 +592,54 @@ class Reader
514
592
  def grab_lines_until(options = {}, &block)
515
593
  buffer = []
516
594
 
517
- finis = false
518
595
  advance if options[:skip_first_line]
519
- # save options to locals for minor optimization
520
- terminator = options[:terminator]
521
- terminator.chomp! if terminator
522
- break_on_blank_lines = options[:break_on_blank_lines]
523
- break_on_list_continuation = options[:break_on_list_continuation]
596
+ # very hot code
597
+ # save options to locals for minor optimizations
598
+ if options.has_key? :terminator
599
+ terminator = options[:terminator]
600
+ break_on_blank_lines = false
601
+ break_on_list_continuation = false
602
+ chomp_last_line = options[:chomp_last_line] || false
603
+ else
604
+ terminator = nil
605
+ break_on_blank_lines = options[:break_on_blank_lines]
606
+ break_on_list_continuation = options[:break_on_list_continuation]
607
+ chomp_last_line = break_on_blank_lines
608
+ end
524
609
  skip_line_comments = options[:skip_line_comments]
525
610
  preprocess = options.fetch(:preprocess, true)
611
+ buffer_empty = true
526
612
  while !(this_line = get_line(preprocess)).nil?
527
- Debug.debug { "Reader processing line: '#{this_line}'" }
528
- finis = true if terminator && this_line.chomp == terminator
529
- finis = true if !finis && break_on_blank_lines && this_line.strip.empty?
530
- finis = true if !finis && break_on_list_continuation && this_line.chomp == LIST_CONTINUATION
531
- finis = true if !finis && block && yield(this_line)
532
- if finis
533
- buffer << this_line if options[:grab_last_line]
534
- unshift_line(this_line) if options[:preserve_last_line]
613
+ # effectively a no-args lamba, but much faster
614
+ finish = while true
615
+ break true if terminator && this_line.chomp == terminator
616
+ break true if break_on_blank_lines && this_line.strip.empty?
617
+ if break_on_list_continuation && !buffer_empty && this_line.chomp == LIST_CONTINUATION
618
+ options[:preserve_last_line] = true
619
+ break true
620
+ end
621
+ break true if block && yield(this_line)
622
+ break false
623
+ end
624
+
625
+ if finish
626
+ if options[:grab_last_line]
627
+ buffer << this_line
628
+ buffer_empty = false
629
+ end
630
+ # QUESTION should we dup this_line when restoring??
631
+ unshift_line this_line if options[:preserve_last_line]
535
632
  break
536
633
  end
537
634
 
538
635
  unless skip_line_comments && this_line.match(REGEXP[:comment])
539
636
  buffer << this_line
637
+ buffer_empty = false
540
638
  end
541
639
  end
542
640
 
641
+ # should we dup the line before chopping?
642
+ buffer.last.chomp! if chomp_last_line && !buffer_empty
543
643
  buffer
544
644
  end
545
645
 
@@ -619,8 +719,7 @@ class Reader
619
719
 
620
720
  # Process bibliography references, so they're available when text
621
721
  # before the reference is being rendered.
622
- # FIXME we don't have support for bibliography lists yet, so disable for now
623
- # plus, this should be done while we are walking lines above
722
+ # FIXME reenable whereever it belongs
624
723
  #@lines.each do |line|
625
724
  # if biblio = line.match(REGEXP[:biblio])
626
725
  # @document.register(:ids, biblio[1])