asciidoctor 1.5.6.2 → 1.5.7

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +330 -143
  3. data/README-fr.adoc +441 -0
  4. data/README-jp.adoc +418 -0
  5. data/README-zh_CN.adoc +430 -0
  6. data/README.adoc +454 -0
  7. data/Rakefile +57 -0
  8. data/asciidoctor.gemspec +7 -1
  9. data/data/locale/attributes-ar.adoc +22 -0
  10. data/data/locale/attributes-bg.adoc +22 -0
  11. data/data/locale/attributes-ca.adoc +22 -0
  12. data/data/locale/attributes-cs.adoc +22 -0
  13. data/data/locale/attributes-da.adoc +22 -0
  14. data/data/locale/attributes-de.adoc +22 -0
  15. data/data/locale/attributes-en.adoc +23 -0
  16. data/data/locale/attributes-es.adoc +22 -0
  17. data/data/locale/attributes-fa.adoc +22 -0
  18. data/data/locale/attributes-fi.adoc +22 -0
  19. data/data/locale/attributes-fr.adoc +22 -0
  20. data/data/locale/attributes-hu.adoc +22 -0
  21. data/data/locale/attributes-id.adoc +22 -0
  22. data/data/locale/attributes-it.adoc +22 -0
  23. data/data/locale/attributes-ja.adoc +22 -0
  24. data/data/locale/attributes-kr.adoc +22 -0
  25. data/data/locale/attributes-nb.adoc +22 -0
  26. data/data/locale/attributes-nl.adoc +22 -0
  27. data/data/locale/attributes-nn.adoc +22 -0
  28. data/data/locale/attributes-pl.adoc +22 -0
  29. data/data/locale/attributes-pt.adoc +22 -0
  30. data/data/locale/attributes-pt_BR.adoc +22 -0
  31. data/data/locale/attributes-ro.adoc +22 -0
  32. data/data/locale/attributes-ru.adoc +22 -0
  33. data/data/locale/attributes-sr.adoc +22 -0
  34. data/data/locale/attributes-sr_Latn.adoc +22 -0
  35. data/data/locale/attributes-tr.adoc +22 -0
  36. data/data/locale/attributes-uk.adoc +22 -0
  37. data/data/locale/attributes-zh_CN.adoc +22 -0
  38. data/data/locale/attributes-zh_TW.adoc +22 -0
  39. data/data/locale/attributes.adoc +8 -649
  40. data/data/stylesheets/asciidoctor-default.css +77 -72
  41. data/features/xref.feature +366 -7
  42. data/lib/asciidoctor.rb +107 -93
  43. data/lib/asciidoctor/abstract_block.rb +247 -239
  44. data/lib/asciidoctor/abstract_node.rb +56 -58
  45. data/lib/asciidoctor/block.rb +3 -3
  46. data/lib/asciidoctor/callouts.rb +1 -1
  47. data/lib/asciidoctor/cli/invoker.rb +36 -9
  48. data/lib/asciidoctor/cli/options.rb +63 -25
  49. data/lib/asciidoctor/converter.rb +23 -13
  50. data/lib/asciidoctor/converter/base.rb +4 -0
  51. data/lib/asciidoctor/converter/docbook45.rb +16 -9
  52. data/lib/asciidoctor/converter/docbook5.rb +115 -97
  53. data/lib/asciidoctor/converter/factory.rb +29 -31
  54. data/lib/asciidoctor/converter/html5.rb +229 -192
  55. data/lib/asciidoctor/converter/manpage.rb +72 -50
  56. data/lib/asciidoctor/converter/template.rb +12 -12
  57. data/lib/asciidoctor/core_ext.rb +5 -1
  58. data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +6 -0
  59. data/lib/asciidoctor/document.rb +168 -77
  60. data/lib/asciidoctor/extensions.rb +79 -47
  61. data/lib/asciidoctor/helpers.rb +33 -11
  62. data/lib/asciidoctor/inline.rb +3 -2
  63. data/lib/asciidoctor/list.rb +2 -1
  64. data/lib/asciidoctor/logging.rb +122 -0
  65. data/lib/asciidoctor/parser.rb +406 -382
  66. data/lib/asciidoctor/path_resolver.rb +169 -162
  67. data/lib/asciidoctor/reader.rb +166 -121
  68. data/lib/asciidoctor/section.rb +45 -28
  69. data/lib/asciidoctor/stylesheets.rb +13 -5
  70. data/lib/asciidoctor/substitutors.rb +328 -254
  71. data/lib/asciidoctor/table.rb +105 -48
  72. data/lib/asciidoctor/timings.rb +34 -6
  73. data/lib/asciidoctor/version.rb +1 -1
  74. data/man/asciidoctor.1 +41 -23
  75. data/man/asciidoctor.adoc +14 -8
  76. data/test/api_test.rb +1004 -0
  77. data/test/attributes_test.rb +241 -50
  78. data/test/blocks_test.rb +549 -124
  79. data/test/converter_test.rb +170 -78
  80. data/test/document_test.rb +208 -767
  81. data/test/extensions_test.rb +188 -53
  82. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +1 -1
  83. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +1 -1
  84. data/test/fixtures/file-with-missing-include.adoc +1 -0
  85. data/test/fixtures/include-file.jsx +8 -0
  86. data/test/fixtures/lists.adoc +96 -0
  87. data/test/fixtures/other-chapters.adoc +11 -0
  88. data/test/fixtures/outer-include.adoc +5 -0
  89. data/test/fixtures/sample.asciidoc +5 -1
  90. data/test/fixtures/subdir/index.adoc +3 -0
  91. data/test/fixtures/subdir/inner-include.adoc +3 -0
  92. data/test/fixtures/subdir/middle-include.adoc +5 -0
  93. data/test/fixtures/tagged-class-enclosed.rb +0 -1
  94. data/test/fixtures/unclosed-tag.adoc +3 -0
  95. data/test/fixtures/unexpected-end-tag.adoc +4 -0
  96. data/test/invoker_test.rb +101 -40
  97. data/test/links_test.rb +266 -72
  98. data/test/lists_test.rb +243 -45
  99. data/test/logger_test.rb +211 -0
  100. data/test/manpage_test.rb +124 -6
  101. data/test/options_test.rb +46 -1
  102. data/test/paragraphs_test.rb +23 -10
  103. data/test/parser_test.rb +30 -1
  104. data/test/paths_test.rb +115 -33
  105. data/test/preamble_test.rb +1 -1
  106. data/test/reader_test.rb +337 -81
  107. data/test/sections_test.rb +656 -72
  108. data/test/substitutions_test.rb +182 -57
  109. data/test/tables_test.rb +324 -57
  110. data/test/test_helper.rb +77 -32
  111. data/test/text_test.rb +7 -7
  112. metadata +67 -3
@@ -2,21 +2,21 @@
2
2
  module Asciidoctor
3
3
  # Public: Methods for retrieving lines from AsciiDoc source files
4
4
  class Reader
5
+ include Logging
6
+
5
7
  class Cursor
6
- attr_accessor :file
7
- attr_accessor :dir
8
- attr_accessor :path
9
- attr_accessor :lineno
10
-
11
- def initialize file, dir = nil, path = nil, lineno = nil
12
- @file = file
13
- @dir = dir
14
- @path = path
15
- @lineno = lineno
8
+ attr_reader :file, :dir, :path, :lineno
9
+
10
+ def initialize file, dir = nil, path = nil, lineno = 1
11
+ @file, @dir, @path, @lineno = file, dir, path, lineno
12
+ end
13
+
14
+ def advance num
15
+ @lineno += num
16
16
  end
17
17
 
18
18
  def line_info
19
- %(#{path}: line #{lineno})
19
+ %(#{@path}: line #{@lineno})
20
20
  end
21
21
 
22
22
  alias to_s line_info
@@ -35,10 +35,14 @@ class Reader
35
35
  # Public: Control whether lines are processed using Reader#process_line on first visit (default: true)
36
36
  attr_accessor :process_lines
37
37
 
38
+ # Public: Indicates that the end of the reader was reached with a delimited block still open.
39
+ attr_accessor :unterminated
40
+
38
41
  # Public: Initialize the Reader object
39
42
  def initialize data = nil, cursor = nil, opts = {}
40
43
  if !cursor
41
- @file = @dir = nil
44
+ @file = nil
45
+ @dir = '.'
42
46
  @path = '<stdin>'
43
47
  @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
44
48
  elsif ::String === cursor
@@ -46,27 +50,22 @@ class Reader
46
50
  @dir, @path = ::File.split @file
47
51
  @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
48
52
  else
49
- @file = cursor.file
50
- @dir = cursor.dir
51
- @path = cursor.path || '<stdin>'
52
- if @file
53
- unless @dir
54
- # REVIEW might to look at this assignment closer
55
- @dir = ::File.dirname @file
56
- @dir = nil if @dir == '.' # right?
57
- end
58
-
59
- unless cursor.path
60
- @path = ::File.basename @file
61
- end
53
+ if (@file = cursor.file)
54
+ @dir = cursor.dir || (::File.dirname @file)
55
+ @path = cursor.path || (::File.basename @file)
56
+ else
57
+ @dir = cursor.dir || '.'
58
+ @path = cursor.path || '<stdin>'
62
59
  end
63
60
  @lineno = cursor.lineno || 1 # IMPORTANT lineno assignment must proceed prepare_lines call!
64
61
  end
65
62
  @lines = data ? (prepare_lines data, opts) : []
66
63
  @source_lines = @lines.dup
64
+ @mark = nil
67
65
  @look_ahead = 0
68
66
  @process_lines = true
69
67
  @unescape_next_line = false
68
+ @unterminated = nil
70
69
  end
71
70
 
72
71
  # Internal: Prepare the lines from the provided data
@@ -188,15 +187,15 @@ class Reader
188
187
  # be processed and marked as such so that subsequent reads will not need to process
189
188
  # the lines again.
190
189
  #
191
- # num - The positive Integer number of lines to peek (must be greater than 0).
192
- # direct - A Boolean indicating whether processing should be disabled when reading lines
190
+ # num - The positive Integer number of lines to peek or nil to peek all lines (default: nil).
191
+ # direct - A Boolean indicating whether processing should be disabled when reading lines (default: false).
193
192
  #
194
193
  # Returns A String Array of the next multiple lines of source data, or an empty Array
195
194
  # if there are no more lines in this Reader.
196
- def peek_lines num, direct = false
195
+ def peek_lines num = nil, direct = false
197
196
  old_look_ahead = @look_ahead
198
197
  result = []
199
- num.times do
198
+ (num || MAX_INT).times do
200
199
  if (line = direct ? shift : read_line)
201
200
  result << line
202
201
  else
@@ -331,44 +330,42 @@ class Reader
331
330
  end
332
331
  end
333
332
 
334
- # Public: Skip consecutive lines containing line comments and return them.
333
+ # Public: Skip consecutive comment lines and block comments.
335
334
  #
336
335
  # Examples
337
336
  # @lines
338
337
  # => ["// foo", "bar"]
339
338
  #
340
339
  # comment_lines = skip_comment_lines
341
- # => ["// foo"]
340
+ # => nil
342
341
  #
343
342
  # @lines
344
343
  # => ["bar"]
345
344
  #
346
- # Returns the Array of lines that were skipped
345
+ # Returns nothing
347
346
  def skip_comment_lines
348
- return [] if empty?
347
+ return if empty?
349
348
 
350
- comment_lines = []
351
349
  while (next_line = peek_line) && !next_line.empty?
352
350
  if next_line.start_with? '//'
353
351
  if next_line.start_with? '///'
354
352
  if (ll = next_line.length) > 3 && next_line == '/' * ll
355
- comment_lines << shift
356
- comment_lines.push(*(read_lines_until(:terminator => next_line, :read_last_line => true, :skip_processing => true)))
353
+ read_lines_until :terminator => next_line, :skip_first_line => true, :read_last_line => true, :skip_processing => true, :context => :comment
357
354
  else
358
355
  break
359
356
  end
360
357
  else
361
- comment_lines << shift
358
+ shift
362
359
  end
363
360
  else
364
361
  break
365
362
  end
366
363
  end
367
364
 
368
- comment_lines
365
+ nil
369
366
  end
370
367
 
371
- # Public: Skip consecutive lines that are line comments and return them.
368
+ # Public: Skip consecutive comment lines and return them.
372
369
  #
373
370
  # This method assumes the reader only contains simple lines (no blocks).
374
371
  def skip_line_comments
@@ -402,8 +399,12 @@ class Reader
402
399
  # a line for which the given block evals to true.
403
400
  #
404
401
  # options - an optional Hash of processing options:
402
+ # * :terminator may be used to specify the contents of the line
403
+ # at which the reader should stop
405
404
  # * :break_on_blank_lines may be used to specify to break on
406
405
  # blank lines
406
+ # * :break_on_list_continuation may be used to specify to break
407
+ # on a list continuation line
407
408
  # * :skip_first_line may be used to tell the reader to advance
408
409
  # beyond the first line before beginning the scan
409
410
  # * :preserve_last_line may be used to specify that the String
@@ -412,6 +413,10 @@ class Reader
412
413
  # * :read_last_line may be used to specify that the String
413
414
  # causing the method to stop processing lines should be
414
415
  # included in the lines being returned
416
+ # * :skip_line_comments may be used to look for and skip
417
+ # line comments
418
+ # * :skip_processing is used to disable line (pre)processing
419
+ # for the duration of this method
415
420
  #
416
421
  # Returns the Array of lines forming the next segment.
417
422
  #
@@ -429,15 +434,12 @@ class Reader
429
434
  # => ["First line", "Second line"]
430
435
  def read_lines_until options = {}
431
436
  result = []
432
- shift if options[:skip_first_line]
433
437
  if @process_lines && options[:skip_processing]
434
438
  @process_lines = false
435
439
  restore_process_lines = true
436
- else
437
- restore_process_lines = false
438
440
  end
439
-
440
441
  if (terminator = options[:terminator])
442
+ start_cursor = options[:cursor] || cursor
441
443
  break_on_blank_lines = false
442
444
  break_on_list_continuation = false
443
445
  else
@@ -445,10 +447,8 @@ class Reader
445
447
  break_on_list_continuation = options[:break_on_list_continuation]
446
448
  end
447
449
  skip_comments = options[:skip_line_comments]
448
- line_read = false
449
- line_restored = false
450
-
451
- complete = false
450
+ complete = line_read = line_restored = nil
451
+ shift if options[:skip_first_line]
452
452
  while !complete && (line = read_line)
453
453
  complete = while true
454
454
  break true if terminator && line == terminator
@@ -461,7 +461,6 @@ class Reader
461
461
  break true if block_given? && (yield line)
462
462
  break false
463
463
  end
464
-
465
464
  if complete
466
465
  if options[:read_last_line]
467
466
  result << line
@@ -478,11 +477,15 @@ class Reader
478
477
  end
479
478
  end
480
479
  end
481
-
482
480
  if restore_process_lines
483
481
  @process_lines = true
484
482
  @look_ahead -= 1 if line_restored && !terminator
485
483
  end
484
+ if terminator && terminator != line && (context = options.fetch :context, terminator)
485
+ start_cursor = cursor_at_mark if start_cursor == :at_mark
486
+ logger.warn message_with_context %(unterminated #{context} block), :source_location => start_cursor
487
+ @unterminated = true
488
+ end
486
489
  result
487
490
  end
488
491
 
@@ -517,17 +520,37 @@ class Reader
517
520
  Cursor.new @file, @dir, @path, @lineno
518
521
  end
519
522
 
523
+ def cursor_at_line lineno
524
+ Cursor.new @file, @dir, @path, lineno
525
+ end
526
+
527
+ def cursor_at_mark
528
+ @mark ? Cursor.new(*@mark) : cursor
529
+ end
530
+
531
+ def cursor_before_mark
532
+ if @mark
533
+ m_file, m_dir, m_path, m_lineno = @mark
534
+ Cursor.new m_file, m_dir, m_path, m_lineno - 1
535
+ else
536
+ Cursor.new @file, @dir, @path, @lineno - 1
537
+ end
538
+ end
539
+
540
+ def cursor_at_prev_line
541
+ Cursor.new @file, @dir, @path, @lineno - 1
542
+ end
543
+
544
+ def mark
545
+ @mark = @file, @dir, @path, @lineno
546
+ end
547
+
520
548
  # Public: Get information about the last line read, including file name and line number.
521
549
  #
522
550
  # Returns A String summary of the last line read
523
551
  def line_info
524
552
  %(#{@path}: line #{@lineno})
525
553
  end
526
- alias next_line_info line_info
527
-
528
- def prev_line_info
529
- %(#{@path}: line #{@lineno - 1})
530
- end
531
554
 
532
555
  # Public: Get a copy of the remaining Array of String lines managed by this Reader
533
556
  #
@@ -550,16 +573,13 @@ class Reader
550
573
  #
551
574
  #
552
575
  # Returns A string summary of this reader, which contains the path and line information
553
- def to_s
554
- line_info
555
- end
576
+ alias to_s line_info
556
577
  end
557
578
 
558
579
  # Public: Methods for retrieving lines from AsciiDoc source files, evaluating preprocessor
559
580
  # directives as each line is read off the Array of lines.
560
581
  class PreprocessorReader < Reader
561
582
  attr_reader :include_stack
562
- attr_reader :includes
563
583
 
564
584
  # Public: Initialize the PreprocessorReader object
565
585
  def initialize document, data = nil, cursor = nil, opts = {}
@@ -580,7 +600,7 @@ class PreprocessorReader < Reader
580
600
  result = super
581
601
 
582
602
  # QUESTION should this work for AsciiDoc table cell content? Currently it does not.
583
- if @document && (@document.attributes.key? 'skip-front-matter')
603
+ if @document && @document.attributes['skip-front-matter']
584
604
  if (front_matter = skip_front_matter! result)
585
605
  @document.attributes['front-matter'] = front_matter * LF
586
606
  end
@@ -635,7 +655,7 @@ class PreprocessorReader < Reader
635
655
  @look_ahead += 1
636
656
  line[1..-1]
637
657
  # QUESTION should we strip whitespace from raw attributes in Substitutors#parse_attributes? (check perf)
638
- elsif preprocess_include_directive $2, $3.strip
658
+ elsif preprocess_include_directive $2, $3
639
659
  # peek again since the content has changed
640
660
  nil
641
661
  else
@@ -720,12 +740,12 @@ class PreprocessorReader < Reader
720
740
 
721
741
  if keyword == 'endif'
722
742
  if @conditional_stack.empty?
723
- warn %(asciidoctor: ERROR: #{line_info}: unmatched macro: endif::#{target}[])
743
+ logger.error message_with_context %(unmatched macro: endif::#{target}[]), :source_location => cursor
724
744
  elsif no_target || target == (pair = @conditional_stack[-1])[:target]
725
745
  @conditional_stack.pop
726
746
  @skipping = @conditional_stack.empty? ? false : @conditional_stack[-1][:skipping]
727
747
  else
728
- warn %(asciidoctor: ERROR: #{line_info}: mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[])
748
+ logger.error message_with_context %(mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[]), :source_location => cursor
729
749
  end
730
750
  return true
731
751
  end
@@ -825,14 +845,14 @@ class PreprocessorReader < Reader
825
845
  if ((expanded_target = target).include? ATTR_REF_HEAD) &&
826
846
  (expanded_target = @document.sub_attributes target, :attribute_missing => 'drop-line').empty?
827
847
  shift
828
- if @document.attributes.fetch('attribute-missing', Compliance.attribute_missing) == 'skip'
848
+ if (@document.attributes['attribute-missing'] || Compliance.attribute_missing) == 'skip'
829
849
  unshift %(Unresolved directive in #{@path} - include::#{target}[#{attrlist}])
830
850
  end
831
851
  true
832
852
  elsif include_processors? && (ext = @include_processor_extensions.find {|candidate| candidate.instance.handles? expanded_target })
833
853
  shift
834
854
  # FIXME parse attributes only if requested by extension
835
- ext.process_method[@document, self, expanded_target, AttributeList.new(attrlist).parse]
855
+ ext.process_method[@document, self, expanded_target, attrlist ? (AttributeList.new attrlist).parse : {}]
836
856
  true
837
857
  # if running in SafeMode::SECURE or greater, don't process this directive
838
858
  # however, be friendly and at least make it a link to the source document
@@ -841,48 +861,39 @@ class PreprocessorReader < Reader
841
861
  replace_next_line %(link:#{expanded_target}[])
842
862
  elsif (abs_maxdepth = @maxdepth[:abs]) > 0
843
863
  if @include_stack.size >= abs_maxdepth
844
- warn %(asciidoctor: ERROR: #{line_info}: maximum include depth of #{@maxdepth[:rel]} exceeded)
864
+ logger.error message_with_context %(maximum include depth of #{@maxdepth[:rel]} exceeded), :source_location => cursor
845
865
  return
846
866
  end
847
867
 
848
- parsed_attributes = attrlist.empty? ? {} : (AttributeList.new attrlist).parse
868
+ parsed_attributes = attrlist ? (AttributeList.new attrlist).parse : {}
849
869
  inc_path, target_type, relpath = resolve_include_path expanded_target, attrlist, parsed_attributes
850
870
  return inc_path unless target_type
851
871
 
852
872
  inc_linenos = inc_tags = nil
853
- unless parsed_attributes.empty?
873
+ if attrlist
854
874
  if parsed_attributes.key? 'lines'
855
875
  inc_linenos = []
856
876
  parsed_attributes['lines'].split(DataDelimiterRx).each do |linedef|
857
877
  if linedef.include?('..')
858
878
  from, to = linedef.split('..', 2).map {|it| it.to_i }
859
- if to == -1
860
- inc_linenos << from
861
- inc_linenos << 1.0/0.0
862
- else
863
- inc_linenos.concat ::Range.new(from, to).to_a
864
- end
879
+ inc_linenos += to < 0 ? [from, 1.0/0.0] : ::Range.new(from, to).to_a
865
880
  else
866
881
  inc_linenos << linedef.to_i
867
882
  end
868
883
  end
869
884
  inc_linenos = inc_linenos.empty? ? nil : inc_linenos.sort.uniq
870
885
  elsif parsed_attributes.key? 'tag'
871
- unless (tag = parsed_attributes['tag']).empty?
872
- if tag.start_with? '!'
873
- inc_tags = { (tag.slice 1, tag.length) => false } unless tag == '!'
874
- else
875
- inc_tags = { tag => true }
876
- end
886
+ unless (tag = parsed_attributes['tag']).empty? || tag == '!'
887
+ inc_tags = (tag.start_with? '!') ? { (tag.slice 1, tag.length) => false } : { tag => true }
877
888
  end
878
889
  elsif parsed_attributes.key? 'tags'
879
890
  inc_tags = {}
880
891
  parsed_attributes['tags'].split(DataDelimiterRx).each do |tagdef|
881
892
  if tagdef.start_with? '!'
882
- inc_tags[tagdef.slice 1, tagdef.length] = false unless tagdef == '!'
893
+ inc_tags[tagdef.slice 1, tagdef.length] = false
883
894
  else
884
895
  inc_tags[tagdef] = true
885
- end unless tagdef.empty?
896
+ end unless tagdef.empty? || tagdef == '!'
886
897
  end
887
898
  inc_tags = nil if inc_tags.empty?
888
899
  end
@@ -891,16 +902,16 @@ class PreprocessorReader < Reader
891
902
  if inc_linenos
892
903
  inc_lines, inc_offset, inc_lineno = [], nil, 0
893
904
  begin
894
- open(inc_path, 'r') do |f|
905
+ open(inc_path, 'rb') do |f|
906
+ select_remaining = nil
895
907
  f.each_line do |l|
896
908
  inc_lineno += 1
897
- select = inc_linenos[0]
898
- if ::Float === select && select.infinite?
909
+ if select_remaining || (::Float === (select = inc_linenos[0]) && (select_remaining = select.infinite?))
899
910
  # NOTE record line where we started selecting
900
911
  inc_offset ||= inc_lineno
901
912
  inc_lines << l
902
913
  else
903
- if inc_lineno == select
914
+ if select == inc_lineno
904
915
  # NOTE record line where we started selecting
905
916
  inc_offset ||= inc_lineno
906
917
  inc_lines << l
@@ -911,12 +922,15 @@ class PreprocessorReader < Reader
911
922
  end
912
923
  end
913
924
  rescue
914
- warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{inc_path})
925
+ logger.error message_with_context %(include #{target_type} not readable: #{inc_path}), :source_location => cursor
915
926
  return replace_next_line %(Unresolved directive in #{@path} - include::#{expanded_target}[#{attrlist}])
916
927
  end
917
928
  shift
918
929
  # FIXME not accounting for skipped lines in reader line numbering
919
- push_include inc_lines, inc_path, relpath, inc_offset, parsed_attributes if inc_offset
930
+ if inc_offset
931
+ parsed_attributes['partial-option'] = true
932
+ push_include inc_lines, inc_path, relpath, inc_offset, parsed_attributes
933
+ end
920
934
  elsif inc_tags
921
935
  inc_lines, inc_offset, inc_lineno, tag_stack, tags_used, active_tag = [], nil, 0, [], ::Set.new, nil
922
936
  if inc_tags.key? '**'
@@ -930,37 +944,35 @@ class PreprocessorReader < Reader
930
944
  select = base_select = !(inc_tags.value? true)
931
945
  wildcard = inc_tags.delete '*'
932
946
  end
933
- if (ext_idx = inc_path.rindex '.') && (circ_cmt = CIRCUMFIX_COMMENTS[inc_path.slice ext_idx, inc_path.length])
934
- cmt_suffix_len = (tag_suffix = %([] #{circ_cmt[:suffix]})).length - 2
935
- end
936
947
  begin
937
- open(inc_path, 'r') do |f|
948
+ open(inc_path, 'rb') do |f|
949
+ dbl_co, dbl_sb = '::', '[]'
950
+ encoding = ::Encoding::UTF_8 if COERCE_ENCODING
938
951
  f.each_line do |l|
939
952
  inc_lineno += 1
940
953
  # must force encoding since we're performing String operations on line
941
- l.force_encoding ::Encoding::UTF_8 if FORCE_ENCODING
942
- if (((tl = l.chomp).end_with? '[]') ||
943
- (tag_suffix && (tl.end_with? tag_suffix) && (tl = tl.slice 0, tl.length - cmt_suffix_len))) &&
944
- TagDirectiveRx =~ tl
954
+ l.force_encoding encoding if encoding
955
+ if (l.include? dbl_co) && (l.include? dbl_sb) && TagDirectiveRx =~ l
945
956
  if $1 # end tag
946
957
  if (this_tag = $2) == active_tag
947
958
  tag_stack.pop
948
959
  active_tag, select = tag_stack.empty? ? [nil, base_select] : tag_stack[-1]
949
960
  elsif inc_tags.key? this_tag
961
+ include_cursor = create_include_cursor inc_path, expanded_target, inc_lineno
950
962
  if (idx = tag_stack.rindex {|key, _| key == this_tag })
951
963
  idx == 0 ? tag_stack.shift : (tag_stack.delete_at idx)
952
- warn %(asciidoctor: WARNING: #{expanded_target}: line #{inc_lineno}: mismatched end tag in include: expected #{active_tag}, found #{this_tag})
964
+ logger.warn message_with_context %(mismatched end tag (expected '#{active_tag}' but found '#{this_tag}') at line #{inc_lineno} of include #{target_type}: #{inc_path}), :source_location => cursor, :include_location => include_cursor
953
965
  else
954
- warn %(asciidoctor: WARNING: #{expanded_target}: line #{inc_lineno}: unexpected end tag in include: #{this_tag})
966
+ logger.warn message_with_context %(unexpected end tag '#{this_tag}' at line #{inc_lineno} of include #{target_type}: #{inc_path}), :source_location => cursor, :include_location => include_cursor
955
967
  end
956
968
  end
957
969
  elsif inc_tags.key?(this_tag = $2)
958
970
  tags_used << this_tag
959
971
  # QUESTION should we prevent tag from being selected when enclosing tag is excluded?
960
- tag_stack << [(active_tag = this_tag), (select = inc_tags[this_tag])]
972
+ tag_stack << [(active_tag = this_tag), (select = inc_tags[this_tag]), inc_lineno]
961
973
  elsif !wildcard.nil?
962
974
  select = active_tag && !select ? false : wildcard
963
- tag_stack << [(active_tag = this_tag), select]
975
+ tag_stack << [(active_tag = this_tag), select, inc_lineno]
964
976
  end
965
977
  elsif select
966
978
  # NOTE record the line where we started selecting
@@ -970,29 +982,35 @@ class PreprocessorReader < Reader
970
982
  end
971
983
  end
972
984
  rescue
973
- warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{inc_path})
985
+ logger.error message_with_context %(include #{target_type} not readable: #{inc_path}), :source_location => cursor
974
986
  return replace_next_line %(Unresolved directive in #{@path} - include::#{expanded_target}[#{attrlist}])
975
987
  end
988
+ unless tag_stack.empty?
989
+ tag_stack.each do |tag_name, _, tag_lineno|
990
+ logger.warn message_with_context %(detected unclosed tag '#{tag_name}' starting at line #{tag_lineno} of include #{target_type}: #{inc_path}), :source_location => cursor, :include_location => (create_include_cursor inc_path, expanded_target, tag_lineno)
991
+ end
992
+ end
976
993
  unless (missing_tags = inc_tags.keys.to_a - tags_used.to_a).empty?
977
- warn %(asciidoctor: WARNING: #{line_info}: tag#{missing_tags.size > 1 ? 's' : nil} '#{missing_tags * ','}' not found in include #{target_type}: #{inc_path})
994
+ logger.warn message_with_context %(tag#{missing_tags.size > 1 ? 's' : ''} '#{missing_tags.join ', '}' not found in include #{target_type}: #{inc_path}), :source_location => cursor
978
995
  end
979
996
  shift
980
- # FIXME not accounting for skipped lines in reader line numbering
981
- push_include inc_lines, inc_path, relpath, inc_offset, parsed_attributes if inc_offset
997
+ if inc_offset
998
+ parsed_attributes['partial-option'] = true unless base_select && wildcard && inc_tags.empty?
999
+ # FIXME not accounting for skipped lines in reader line numbering
1000
+ push_include inc_lines, inc_path, relpath, inc_offset, parsed_attributes
1001
+ end
982
1002
  else
983
1003
  begin
984
1004
  # NOTE read content first so that we only advance cursor if IO operation succeeds
985
- inc_content = target_type == :file ? (::IO.read inc_path) : open(inc_path, 'r') {|f| f.read }
1005
+ inc_content = target_type == :file ? (::IO.binread inc_path) : open(inc_path, 'rb') {|f| f.read }
986
1006
  shift
987
1007
  push_include inc_content, inc_path, relpath, 1, parsed_attributes
988
1008
  rescue
989
- warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{inc_path})
1009
+ logger.error message_with_context %(include #{target_type} not readable: #{inc_path}), :source_location => cursor
990
1010
  return replace_next_line %(Unresolved directive in #{@path} - include::#{expanded_target}[#{attrlist}])
991
1011
  end
992
1012
  end
993
1013
  true
994
- else
995
- false
996
1014
  end
997
1015
  end
998
1016
 
@@ -1015,9 +1033,10 @@ class PreprocessorReader < Reader
1015
1033
  # Returns An Array containing the resolved (absolute) include path, the target type, and the path
1016
1034
  # relative to the outermost document. May also return a boolean to halt processing of the include.
1017
1035
  def resolve_include_path target, attrlist, attributes
1018
- if Helpers.uriish? target
1019
- return replace_next_line %(link:#{target}[#{attrlist}]) unless @document.attributes.key? 'allow-uri-read'
1020
- if @document.attributes.key? 'cache-uri'
1036
+ doc = @document
1037
+ if (Helpers.uriish? target) || (::String === @dir ? nil : (target = %(#{@dir}/#{target})))
1038
+ return replace_next_line %(link:#{target}[#{attrlist}]) unless doc.attr? 'allow-uri-read'
1039
+ if doc.attr? 'cache-uri'
1021
1040
  # caching requires the open-uri-cached gem to be installed
1022
1041
  # processing will be automatically aborted if these libraries can't be opened
1023
1042
  Helpers.require_library 'open-uri/cached', 'open-uri-cached' unless defined? ::OpenURI::Cache
@@ -1025,17 +1044,22 @@ class PreprocessorReader < Reader
1025
1044
  # autoload open-uri
1026
1045
  ::OpenURI
1027
1046
  end
1028
- [target, :uri, target]
1047
+ [(::URI.parse target), :uri, target]
1029
1048
  else
1030
1049
  # include file is resolved relative to dir of current include, or base_dir if within original docfile
1031
- inc_path = @document.normalize_system_path target, @dir, nil, :target_name => 'include file'
1050
+ inc_path = doc.normalize_system_path target, @dir, nil, :target_name => 'include file'
1032
1051
  unless ::File.file? inc_path
1033
- warn %(asciidoctor: WARNING: #{line_info}: include file not found: #{inc_path})
1034
- return replace_next_line %(Unresolved directive in #{@path} - include::#{target}[#{attrlist}])
1052
+ if attributes.key? 'optional-option'
1053
+ shift
1054
+ return true
1055
+ else
1056
+ logger.error message_with_context %(include file not found: #{inc_path}), :source_location => cursor
1057
+ return replace_next_line %(Unresolved directive in #{@path} - include::#{target}[#{attrlist}])
1058
+ end
1035
1059
  end
1036
1060
  # NOTE relpath is the path relative to the root document (or base_dir, if set)
1037
1061
  # QUESTION should we move relative_path method to Document
1038
- relpath = (@path_resolver ||= PathResolver.new).relative_path inc_path, @document.base_dir
1062
+ relpath = doc.path_resolver.relative_path inc_path, doc.base_dir
1039
1063
  [inc_path, :file, relpath]
1040
1064
  end
1041
1065
  end
@@ -1056,20 +1080,29 @@ class PreprocessorReader < Reader
1056
1080
  # Returns this Reader object.
1057
1081
  def push_include data, file = nil, path = nil, lineno = 1, attributes = {}
1058
1082
  @include_stack << [@lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines]
1059
- if file
1060
- @file = file
1061
- @dir = File.dirname file
1083
+ if (@file = file)
1084
+ # NOTE if file is not a string, assume it's a URI
1085
+ if ::String === file
1086
+ @dir = ::File.dirname file
1087
+ elsif ::RUBY_ENGINE_OPAL
1088
+ @dir = ::URI.parse ::File.dirname(file = file.to_s)
1089
+ else
1090
+ # NOTE this intentionally throws an error if URI has no path
1091
+ (@dir = file.dup).path = (dir = ::File.dirname file.path) == '/' ? '' : dir
1092
+ file = file.to_s
1093
+ end
1094
+ path ||= ::File.basename file
1062
1095
  # only process lines in AsciiDoc files
1063
1096
  @process_lines = ASCIIDOC_EXTENSIONS[::File.extname file]
1064
1097
  else
1065
- @file = nil
1066
- @dir = '.' # right?
1098
+ @dir = '.'
1067
1099
  # we don't know what file type we have, so assume AsciiDoc
1068
1100
  @process_lines = true
1069
1101
  end
1070
1102
 
1071
1103
  if path
1072
- @includes << Helpers.rootname(@path = path)
1104
+ @path = path
1105
+ @includes[Helpers.rootname path] = attributes['partial-option'] ? nil : true if @process_lines
1073
1106
  else
1074
1107
  @path = '<stdin>'
1075
1108
  end
@@ -1108,6 +1141,18 @@ class PreprocessorReader < Reader
1108
1141
  self
1109
1142
  end
1110
1143
 
1144
+ def create_include_cursor file, path, lineno
1145
+ if ::String === file
1146
+ dir = ::File.dirname file
1147
+ elsif ::RUBY_ENGINE_OPAL
1148
+ dir = ::File.dirname(file = file.to_s)
1149
+ else
1150
+ dir = (dir = ::File.dirname file.path) == '' ? '/' : dir
1151
+ file = file.to_s
1152
+ end
1153
+ Cursor.new file, dir, path, lineno
1154
+ end
1155
+
1111
1156
  def pop_include
1112
1157
  if @include_stack.size > 0
1113
1158
  @lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines = @include_stack.pop