asciidoctor 1.5.1 → 1.5.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +85 -0
  3. data/README.adoc +79 -6
  4. data/Rakefile +22 -1
  5. data/benchmark/benchmark.rb +3 -1
  6. data/compat/asciidoc.conf +2 -2
  7. data/data/stylesheets/asciidoctor-default.css +3 -3
  8. data/features/step_definitions.rb +12 -1
  9. data/lib/asciidoctor.rb +74 -42
  10. data/lib/asciidoctor/abstract_block.rb +5 -1
  11. data/lib/asciidoctor/abstract_node.rb +40 -23
  12. data/lib/asciidoctor/attribute_list.rb +14 -5
  13. data/lib/asciidoctor/block.rb +45 -12
  14. data/lib/asciidoctor/callouts.rb +1 -0
  15. data/lib/asciidoctor/cli/invoker.rb +6 -2
  16. data/lib/asciidoctor/cli/options.rb +23 -9
  17. data/lib/asciidoctor/converter.rb +9 -19
  18. data/lib/asciidoctor/converter/base.rb +11 -14
  19. data/lib/asciidoctor/converter/composite.rb +8 -19
  20. data/lib/asciidoctor/converter/docbook45.rb +1 -0
  21. data/lib/asciidoctor/converter/docbook5.rb +24 -2
  22. data/lib/asciidoctor/converter/factory.rb +1 -0
  23. data/lib/asciidoctor/converter/html5.rb +61 -25
  24. data/lib/asciidoctor/converter/template.rb +19 -26
  25. data/lib/asciidoctor/document.rb +73 -45
  26. data/lib/asciidoctor/extensions.rb +121 -9
  27. data/lib/asciidoctor/helpers.rb +1 -0
  28. data/lib/asciidoctor/inline.rb +1 -0
  29. data/lib/asciidoctor/list.rb +1 -0
  30. data/lib/asciidoctor/opal_ext.rb +22 -0
  31. data/lib/asciidoctor/opal_ext/file.rb +26 -13
  32. data/lib/asciidoctor/parser.rb +15 -18
  33. data/lib/asciidoctor/path_resolver.rb +18 -0
  34. data/lib/asciidoctor/reader.rb +8 -9
  35. data/lib/asciidoctor/section.rb +5 -8
  36. data/lib/asciidoctor/stylesheets.rb +1 -0
  37. data/lib/asciidoctor/substitutors.rb +18 -18
  38. data/lib/asciidoctor/table.rb +2 -1
  39. data/lib/asciidoctor/timings.rb +1 -0
  40. data/lib/asciidoctor/version.rb +1 -1
  41. data/man/asciidoctor.1 +10 -11
  42. data/man/asciidoctor.adoc +80 -99
  43. data/test/attributes_test.rb +42 -0
  44. data/test/blocks_test.rb +19 -7
  45. data/test/document_test.rb +114 -0
  46. data/test/extensions_test.rb +100 -0
  47. data/test/fixtures/custom-docinfodir/basic-docinfo.html +1 -0
  48. data/test/fixtures/custom-docinfodir/docinfo.html +1 -0
  49. data/test/fixtures/hello-asciidoctor.pdf +0 -0
  50. data/test/invoker_test.rb +4 -3
  51. data/test/lists_test.rb +31 -5
  52. data/test/options_test.rb +1 -1
  53. data/test/paths_test.rb +21 -0
  54. data/test/preamble_test.rb +33 -0
  55. data/test/reader_test.rb +13 -0
  56. data/test/sections_test.rb +22 -0
  57. data/test/substitutions_test.rb +49 -0
  58. data/test/tables_test.rb +76 -0
  59. data/test/test_helper.rb +4 -2
  60. metadata +7 -5
  61. data/lib/asciidoctor/debug.rb +0 -25
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  module Asciidoctor
2
3
  # A {Converter} implementation that delegates to the chain of {Converter}
3
4
  # objects passed to the constructor. Selects the first {Converter} that
@@ -15,20 +16,6 @@ module Asciidoctor
15
16
  @converter_map = {}
16
17
  end
17
18
 
18
- # Public: Delegates to the first converter that identifies itself as the
19
- # handler for the given transform.
20
- #
21
- # node - the AbstractNode to convert
22
- # transform - the optional String transform, or the name of the node if no
23
- # transform is specified. (default: nil)
24
- #
25
- # Returns the String result returned from the delegate's convert method
26
- def convert node, transform = nil
27
- transform ||= node.node_name
28
- # QUESTION is there a way we can control whether to use convert or send?
29
- (converter_for transform).convert node, transform
30
- end
31
-
32
19
  # Public: Delegates to the first converter that identifies itself as the
33
20
  # handler for the given transform. The optional Hash is passed as the last
34
21
  # option to the delegate's convert method.
@@ -36,20 +23,22 @@ module Asciidoctor
36
23
  # node - the AbstractNode to convert
37
24
  # transform - the optional String transform, or the name of the node if no
38
25
  # transform is specified. (default: nil)
39
- # opts - a optional Hash that is passed to the delegate's convert method. (default: {})
26
+ # opts - an optional Hash that is passed to the delegate's convert method. (default: {})
40
27
  #
41
28
  # Returns the String result returned from the delegate's convert method
42
- def convert_with_options node, transform = nil, opts = {}
29
+ def convert node, transform = nil, opts = {}
43
30
  transform ||= node.node_name
44
- # QUESTION should we check arity, or perhaps do a rescue ::ArgumentError?
45
- (converter_for transform).convert_with_options node, transform, opts
31
+ (converter_for transform).convert node, transform, opts
46
32
  end
47
33
 
34
+ # Alias for backward compatibility.
35
+ alias :convert_with_options :convert
36
+
48
37
  # Public: Retrieve the converter for the specified transform.
49
38
  #
50
39
  # Returns the matching [Converter] object
51
40
  def converter_for transform
52
- @converter_map[transform] ||= find_converter transform
41
+ @converter_map[transform] ||= (find_converter transform)
53
42
  end
54
43
 
55
44
  # Internal: Find the converter for the specified transform.
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  require 'asciidoctor/converter/docbook5'
2
3
 
3
4
  module Asciidoctor
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  module Asciidoctor
2
3
  # A built-in {Converter} implementation that generates DocBook 5 output
3
4
  # similar to the docbook45 backend from AsciiDoc Python, but migrated to the
@@ -5,7 +6,9 @@ module Asciidoctor
5
6
  class Converter::DocBook5Converter < Converter::BuiltIn
6
7
  def document node
7
8
  result = []
8
- root_tag_name = node.doctype
9
+ if (root_tag_name = node.doctype) == 'manpage'
10
+ root_tag_name = 'refentry'
11
+ end
9
12
  result << '<?xml version="1.0" encoding="UTF-8"?>'
10
13
  if (doctype_line = doctype_declaration root_tag_name)
11
14
  result << doctype_line
@@ -39,10 +42,18 @@ module Asciidoctor
39
42
  alias :embedded :content
40
43
 
41
44
  def section node
45
+ doctype = node.document.doctype
42
46
  tag_name = if node.special
43
47
  node.level <= 1 ? node.sectname : 'section'
44
48
  else
45
- node.document.doctype == 'book' && node.level <= 1 ? (node.level == 0 ? 'part' : 'chapter') : 'section'
49
+ doctype == 'book' && node.level <= 1 ? (node.level == 0 ? 'part' : 'chapter') : 'section'
50
+ end
51
+ if doctype == 'manpage'
52
+ if tag_name == 'section'
53
+ tag_name = 'refsection'
54
+ elsif tag_name == 'synopsis'
55
+ tag_name = 'refsynopsisdiv'
56
+ end
46
57
  end
47
58
  %(<#{tag_name}#{common_attributes node.id, node.role, node.reftext}>
48
59
  <title>#{node.title}</title>
@@ -668,6 +679,17 @@ module Asciidoctor
668
679
  end
669
680
  result << %(</#{info_tag_prefix}info>)
670
681
 
682
+ if doc.doctype == 'manpage'
683
+ result << '<refmeta>'
684
+ result << %(<refentrytitle>#{doc.attr 'mantitle'}</refentrytitle>) if doc.attr? 'mantitle'
685
+ result << %(<manvolnum>#{doc.attr 'manvolnum'}</manvolnum>) if doc.attr? 'manvolnum'
686
+ result << '</refmeta>'
687
+ result << '<refnamediv>'
688
+ result << %(<refname>#{doc.attr 'manname'}</refname>) if doc.attr? 'manname'
689
+ result << %(<refpurpose>#{doc.attr 'manpurpose'}</refpurpose>) if doc.attr? 'manpurpose'
690
+ result << '</refnamediv>'
691
+ end
692
+
671
693
  result * EOL
672
694
  end
673
695
 
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  module Asciidoctor
2
3
  module Converter
3
4
  # A factory for instantiating converters that are used to convert a
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  module Asciidoctor
2
3
  # A built-in {Converter} implementation that generates HTML 5 output
3
4
  # consistent with the html5 backend from AsciiDoc Python.
@@ -48,13 +49,13 @@ module Asciidoctor
48
49
  result << %(<meta name="author" content="#{node.attr 'authors'}"#{slash}>) if node.attr? 'authors'
49
50
  result << %(<meta name="copyright" content="#{node.attr 'copyright'}"#{slash}>) if node.attr? 'copyright'
50
51
 
51
- result << %(<title>#{node.doctitle :sanitize => true, :use_fallback => true}</title>)
52
+ result << %(<title>#{node.doctitle :sanitize => true, :use_fallback => true}</title>)
52
53
  if DEFAULT_STYLESHEET_KEYS.include?(node.attr 'stylesheet')
53
54
  if (webfonts = node.attr 'webfonts')
54
- result << %(<link rel="stylesheet" href="#{asset_uri_scheme}//fonts.googleapis.com/css?family=#{webfonts.empty? ? 'Open+Sans:300,300italic,400,400italic,600,600italic|Noto+Serif:400,400italic,700,700italic|Droid+Sans+Mono:400' : webfonts}"#{slash}>)
55
+ result << %(<link rel="stylesheet" href="#{asset_uri_scheme}//fonts.googleapis.com/css?family=#{webfonts.empty? ? 'Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400' : webfonts}"#{slash}>)
55
56
  end
56
57
  if linkcss
57
- result << %(<link rel="stylesheet" href="#{node.normalize_web_path DEFAULT_STYLESHEET_NAME, (node.attr 'stylesdir', '')}"#{slash}>)
58
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path DEFAULT_STYLESHEET_NAME, (node.attr 'stylesdir', ''), false}"#{slash}>)
58
59
  else
59
60
  result << @stylesheets.embed_primary_stylesheet
60
61
  end
@@ -63,17 +64,17 @@ module Asciidoctor
63
64
  result << %(<link rel="stylesheet" href="#{node.normalize_web_path((node.attr 'stylesheet'), (node.attr 'stylesdir', ''))}"#{slash}>)
64
65
  else
65
66
  result << %(<style>
66
- #{node.read_asset node.normalize_system_path((node.attr 'stylesheet'), (node.attr 'stylesdir', '')), true}
67
+ #{node.read_asset node.normalize_system_path((node.attr 'stylesheet'), (node.attr 'stylesdir', '')), :warn_on_failure => true}
67
68
  </style>)
68
69
  end
69
70
  end
70
71
 
71
72
  if node.attr? 'icons', 'font'
72
73
  if node.attr? 'iconfont-remote'
73
- result << %(<link rel="stylesheet" href="#{node.attr 'iconfont-cdn', %[#{cdn_base}/font-awesome/4.1.0/css/font-awesome.min.css]}"#{slash}>)
74
+ result << %(<link rel="stylesheet" href="#{node.attr 'iconfont-cdn', %[#{cdn_base}/font-awesome/4.2.0/css/font-awesome.min.css]}"#{slash}>)
74
75
  else
75
76
  iconfont_stylesheet = %(#{node.attr 'iconfont-name', 'font-awesome'}.css)
76
- result << %(<link rel="stylesheet" href="#{node.normalize_web_path iconfont_stylesheet, (node.attr 'stylesdir', '')}"#{slash}>)
77
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path iconfont_stylesheet, (node.attr 'stylesdir', ''), false}"#{slash}>)
77
78
  end
78
79
  end
79
80
 
@@ -81,7 +82,7 @@ module Asciidoctor
81
82
  when 'coderay'
82
83
  if (node.attr 'coderay-css', 'class') == 'class'
83
84
  if linkcss
84
- result << %(<link rel="stylesheet" href="#{node.normalize_web_path @stylesheets.coderay_stylesheet_name, (node.attr 'stylesdir', '')}"#{slash}>)
85
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path @stylesheets.coderay_stylesheet_name, (node.attr 'stylesdir', ''), false}"#{slash}>)
85
86
  else
86
87
  result << @stylesheets.embed_coderay_stylesheet
87
88
  end
@@ -90,13 +91,13 @@ module Asciidoctor
90
91
  if (node.attr 'pygments-css', 'class') == 'class'
91
92
  pygments_style = node.attr 'pygments-style'
92
93
  if linkcss
93
- result << %(<link rel="stylesheet" href="#{node.normalize_web_path @stylesheets.pygments_stylesheet_name(pygments_style), (node.attr 'stylesdir', '')}"#{slash}>)
94
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path @stylesheets.pygments_stylesheet_name(pygments_style), (node.attr 'stylesdir', ''), false}"#{slash}>)
94
95
  else
95
96
  result << (@stylesheets.embed_pygments_stylesheet pygments_style)
96
97
  end
97
98
  end
98
99
  when 'highlightjs', 'highlight.js'
99
- highlightjs_path = node.attr 'highlightjsdir', %(#{cdn_base}/highlight.js/8.1)
100
+ highlightjs_path = node.attr 'highlightjsdir', %(#{cdn_base}/highlight.js/8.4)
100
101
  result << %(<link rel="stylesheet" href="#{highlightjs_path}/styles/#{node.attr 'highlightjs-theme', 'github'}.min.css"#{slash}>
101
102
  <script src="#{highlightjs_path}/highlight.min.js"></script>
102
103
  <script>hljs.initHighlightingOnLoad()</script>)
@@ -108,17 +109,22 @@ module Asciidoctor
108
109
  end
109
110
 
110
111
  if node.attr? 'stem'
112
+ # IMPORTANT to_s calls on delimiter arrays are intentional for JavaScript compat (emulates JSON.stringify)
113
+ eqnums_val = node.attr 'eqnums', 'none'
114
+ eqnums_val = 'AMS' if eqnums_val == ''
115
+ eqnums_opt = %( equationNumbers: { autoNumber: "#{eqnums_val}" } )
111
116
  result << %(<script type="text/x-mathjax-config">
112
117
  MathJax.Hub.Config({
113
118
  tex2jax: {
114
- inlineMath: [#{INLINE_MATH_DELIMITERS[:latexmath]}],
115
- displayMath: [#{BLOCK_MATH_DELIMITERS[:latexmath]}],
119
+ inlineMath: [#{INLINE_MATH_DELIMITERS[:latexmath].to_s}],
120
+ displayMath: [#{BLOCK_MATH_DELIMITERS[:latexmath].to_s}],
116
121
  ignoreClass: "nostem|nolatexmath"
117
122
  },
118
123
  asciimath2jax: {
119
- delimiters: [#{BLOCK_MATH_DELIMITERS[:asciimath]}],
124
+ delimiters: [#{BLOCK_MATH_DELIMITERS[:asciimath].to_s}],
120
125
  ignoreClass: "nostem|noasciimath"
121
- }
126
+ },
127
+ TeX: {#{eqnums_opt}}
122
128
  });
123
129
  </script>
124
130
  <script src="#{cdn_base}/mathjax/2.4.0/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script>)
@@ -505,7 +511,7 @@ Your browser does not support the audio tag.
505
511
 
506
512
  def image node
507
513
  align = (node.attr? 'align') ? (node.attr 'align') : nil
508
- float = (node.attr? 'float') ? (node.attr 'float') : nil
514
+ float = (node.attr? 'float') ? (node.attr 'float') : nil
509
515
  style_attribute = if align || float
510
516
  styles = [align ? %(text-align: #{align}) : nil, float ? %(float: #{float}) : nil].compact
511
517
  %( style="#{styles * ';'}")
@@ -591,7 +597,7 @@ Your browser does not support the audio tag.
591
597
  unless ((equation = node.content).start_with? open) && (equation.end_with? close)
592
598
  equation = %(#{open}#{equation}#{close})
593
599
  end
594
-
600
+
595
601
  %(<div#{id_attribute} class="#{(role = node.role) ? ['stemblock', role] * ' ' : 'stemblock'}">
596
602
  #{title_element}<div class="content">
597
603
  #{equation}
@@ -733,7 +739,7 @@ Your browser does not support the audio tag.
733
739
  end
734
740
 
735
741
  def table node
736
- result = []
742
+ result = []
737
743
  id_attribute = node.id ? %( id="#{node.id}") : nil
738
744
  classes = ['tableblock', %(frame-#{node.attr 'frame', 'all'}), %(grid-#{node.attr 'grid', 'all'})]
739
745
  styles = []
@@ -905,30 +911,60 @@ Your browser does not support the audio tag.
905
911
  height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
906
912
  case node.attr 'poster'
907
913
  when 'vimeo'
908
- start_anchor = (node.attr? 'start') ? "#at=#{node.attr 'start'}" : nil
914
+ start_anchor = (node.attr? 'start', nil, false) ? %(#at=#{node.attr 'start'}) : nil
909
915
  delimiter = '?'
910
- autoplay_param = (node.option? 'autoplay') ? "#{delimiter}autoplay=1" : nil
916
+ autoplay_param = (node.option? 'autoplay') ? %(#{delimiter}autoplay=1) : nil
911
917
  delimiter = '&amp;' if autoplay_param
912
- loop_param = (node.option? 'loop') ? "#{delimiter}loop=1" : nil
918
+ loop_param = (node.option? 'loop') ? %(#{delimiter}loop=1) : nil
913
919
  %(<div#{id_attribute}#{class_attribute}>#{title_element}
914
920
  <div class="content">
915
- <iframe#{width_attribute}#{height_attribute} src="//player.vimeo.com/video/#{node.attr 'target'}#{start_anchor}#{autoplay_param}#{loop_param}" frameborder="0"#{append_boolean_attribute 'webkitAllowFullScreen', xml}#{append_boolean_attribute 'mozallowfullscreen', xml}#{append_boolean_attribute 'allowFullScreen', xml}></iframe>
921
+ <iframe#{width_attribute}#{height_attribute} src="//player.vimeo.com/video/#{node.attr 'target'}#{start_anchor}#{autoplay_param}#{loop_param}" frameborder="0"#{(node.option? 'nofullscreen') ? nil : (append_boolean_attribute 'allowfullscreen', xml)}></iframe>
916
922
  </div>
917
923
  </div>)
918
924
  when 'youtube'
919
- start_param = (node.attr? 'start') ? "&amp;start=#{node.attr 'start'}" : nil
920
- end_param = (node.attr? 'end') ? "&amp;end=#{node.attr 'end'}" : nil
925
+ rel_param_val = (node.option? 'related') ? 1 : 0
926
+ start_param = (node.attr? 'start', nil, false) ? %(&amp;start=#{node.attr 'start'}) : nil
927
+ end_param = (node.attr? 'end', nil, false) ? %(&amp;end=#{node.attr 'end'}) : nil
921
928
  autoplay_param = (node.option? 'autoplay') ? '&amp;autoplay=1' : nil
922
929
  loop_param = (node.option? 'loop') ? '&amp;loop=1' : nil
923
930
  controls_param = (node.option? 'nocontrols') ? '&amp;controls=0' : nil
931
+ # cover both ways of controlling fullscreen option
932
+ if node.option? 'nofullscreen'
933
+ fs_param = '&amp;fs=0'
934
+ fs_attribute = nil
935
+ else
936
+ fs_param = nil
937
+ fs_attribute = append_boolean_attribute 'allowfullscreen', xml
938
+ end
939
+ modest_param = (node.option? 'modest') ? '&amp;modestbranding=1' : nil
940
+ theme_param = (node.attr? 'theme', nil, false) ? %(&amp;theme=#{node.attr 'theme'}) : nil
941
+ hl_param = (node.attr? 'lang') ? %(&amp;hl=#{node.attr 'lang'}) : nil
942
+
943
+ # parse video_id/list_id syntax where list_id (i.e., playlist) is optional
944
+ target, list = (node.attr 'target').split '/', 2
945
+ if (list ||= (node.attr 'list', nil, false))
946
+ list_param = %(&amp;list=#{list})
947
+ else
948
+ # parse dynamic playlist syntax: video_id1,video_id2,...
949
+ target, playlist = target.split ',', 2
950
+ if (playlist ||= (node.attr 'playlist', nil, false))
951
+ # INFO playlist bar doesn't appear in Firefox unless showinfo=1 and modestbranding=1
952
+ list_param = %(&amp;playlist=#{playlist})
953
+ else
954
+ list_param = nil
955
+ end
956
+ end
957
+
924
958
  %(<div#{id_attribute}#{class_attribute}>#{title_element}
925
959
  <div class="content">
926
- <iframe#{width_attribute}#{height_attribute} src="//www.youtube.com/embed/#{node.attr 'target'}?rel=0#{start_param}#{end_param}#{autoplay_param}#{loop_param}#{controls_param}" frameborder="0"#{(node.option? 'nofullscreen') ? nil : (append_boolean_attribute 'allowfullscreen', xml)}></iframe>
960
+ <iframe#{width_attribute}#{height_attribute} src="//www.youtube.com/embed/#{target}?rel=#{rel_param_val}#{start_param}#{end_param}#{autoplay_param}#{loop_param}#{controls_param}#{list_param}#{fs_param}#{modest_param}#{theme_param}#{hl_param}" frameborder="0"#{fs_attribute}></iframe>
927
961
  </div>
928
962
  </div>)
929
- else
963
+ else
930
964
  poster_attribute = %(#{poster = node.attr 'poster'}).empty? ? nil : %( poster="#{node.media_uri poster}")
931
- time_anchor = ((node.attr? 'start') || (node.attr? 'end')) ? %(#t=#{node.attr 'start'}#{(node.attr? 'end') ? ',' : nil}#{node.attr 'end'}) : nil
965
+ start_t = node.attr 'start', nil, false
966
+ end_t = node.attr 'end', nil, false
967
+ time_anchor = (start_t || end_t) ? %(#t=#{start_t}#{end_t ? ',' : nil}#{end_t}) : nil
932
968
  %(<div#{id_attribute}#{class_attribute}>#{title_element}
933
969
  <div class="content">
934
970
  <video src="#{node.media_uri(node.attr 'target')}#{time_anchor}"#{width_attribute}#{height_attribute}#{poster_attribute}#{(node.option? 'autoplay') ? (append_boolean_attribute 'autoplay', xml) : nil}#{(node.option? 'nocontrols') ? nil : (append_boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (append_boolean_attribute 'loop', xml) : nil}>
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  module Asciidoctor
2
3
  # A {Converter} implementation that uses templates composed in template
3
4
  # languages supported by {https://github.com/rtomayko/tilt Tilt} to convert
@@ -166,40 +167,28 @@ module Asciidoctor
166
167
  # template_name - the String name of the template to use, or the value of
167
168
  # the node_name property on the node if a template name is
168
169
  # not specified. (optional, default: nil)
170
+ # opts - an optional Hash that is passed as local variables to the
171
+ # template. (optional, default: {})
169
172
  #
170
173
  # Returns the [String] result from rendering the template
171
- def convert node, template_name = nil
174
+ def convert node, template_name = nil, opts = {}
172
175
  template_name ||= node.node_name
173
176
  unless (template = @templates[template_name])
174
177
  raise %(Could not find a custom template to handle transform: #{template_name})
175
178
  end
176
- if template_name == 'document'
177
- (template.render node).strip
178
- else
179
- (template.render node).chomp
179
+
180
+ # Slim doesn't include helpers in the template's execution scope such as
181
+ # HAML, so we must do it ourselves.
182
+ if (defined? ::Slim::Helpers) && (template.is_a? ::Slim::Template)
183
+ node.extend ::Slim::Helpers
180
184
  end
181
- end
182
185
 
183
- # Public: Convert an {AbstractNode} using the named template with the
184
- # additional options provided.
185
- #
186
- # Looks for a template that matches the value of the
187
- # {AbstractNode#node_name} property if a template name is not specified.
188
- #
189
- # node - the AbstractNode to convert
190
- # template_name - the String name of the template to use, or the value of
191
- # the node_name property on the node if a template name is
192
- # not specified. (optional, default: nil)
193
- # opts - an optional Hash that is passed as local variables to the
194
- # template. (optional, default: {})
195
- #
196
- # Returns the [String] result from rendering the template
197
- def convert_with_options node, template_name = nil, opts = {}
198
- template_name ||= node.node_name
199
- unless (template = @templates[template_name])
200
- raise %(Could not find a custom template to handle transform: #{template_name})
186
+ # NOTE opts become locals in the template
187
+ if template_name == 'document'
188
+ (template.render node, opts).strip
189
+ else
190
+ (template.render node, opts).chomp
201
191
  end
202
- (template.render node, opts).chomp
203
192
  end
204
193
 
205
194
  # Public: Checks whether there is a Tilt template registered with the specified name.
@@ -260,8 +249,12 @@ module Asciidoctor
260
249
  if ext_name == 'slim'
261
250
  # slim doesn't get loaded by Tilt, so we have to load it explicitly
262
251
  Helpers.require_library 'slim' unless defined? ::Slim
252
+ # load include plugin when using Slim >= 2.1
253
+ unless ::Slim::VERSION < '2.1' || (defined? ::Slim::Include)
254
+ Helpers.require_library 'slim/include', false
255
+ end
263
256
  elsif ext_name == 'erb'
264
- template_class, extra_engine_options = (eruby_loaded ||= load_eruby @eruby)
257
+ template_class, extra_engine_options = (eruby_loaded ||= load_eruby(@eruby))
265
258
  end
266
259
  next unless ::Tilt.registered? ext_name
267
260
  unless template_cache && (template = template_cache[file])
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  module Asciidoctor
2
3
  # Public: Methods for parsing and converting AsciiDoc documents.
3
4
  #
@@ -133,6 +134,9 @@ class Document < AbstractBlock
133
134
  # If the source is a string, defaults to the current directory.
134
135
  attr_reader :base_dir
135
136
 
137
+ # Public: Get the Hash of resolved options used to initialize this Document
138
+ attr_reader :options
139
+
136
140
  # Public: Get a reference to the parent Document of this nested document.
137
141
  attr_reader :parent_document
138
142
 
@@ -151,6 +155,8 @@ class Document < AbstractBlock
151
155
  # options - A Hash of options to control processing (e.g., safe mode value (:safe), backend (:backend),
152
156
  # header/footer toggle (:header_footer), custom attributes (:attributes)). (default: {})
153
157
  #
158
+ # Duplication of the options Hash is handled in the enclosing API.
159
+ #
154
160
  # Examples
155
161
  #
156
162
  # data = File.read filename
@@ -235,7 +241,9 @@ class Document < AbstractBlock
235
241
  @callouts = Callouts.new
236
242
  @attributes_modified = ::Set.new
237
243
  @options = options
244
+ @docinfo_processor_extensions = {}
238
245
  header_footer = (options[:header_footer] ||= false)
246
+ options.freeze
239
247
 
240
248
  attrs = @attributes
241
249
  attrs['encoding'] = 'UTF-8'
@@ -263,6 +271,7 @@ class Document < AbstractBlock
263
271
  #attrs['listing-caption'] = 'Listing'
264
272
  attrs['table-caption'] = 'Table'
265
273
  attrs['toc-title'] = 'Table of Contents'
274
+ #attrs['preface-title'] = 'Preface'
266
275
  attrs['manname-title'] = 'NAME'
267
276
  attrs['untitled-label'] = 'Untitled'
268
277
  attrs['version-label'] = 'Version'
@@ -346,7 +355,7 @@ class Document < AbstractBlock
346
355
  else
347
356
  # a value ending in @ indicates this attribute does not override
348
357
  # an attribute with the same key in the document souce
349
- if (val.is_a? ::String) && (val.end_with? '@')
358
+ if ::String === val && (val.end_with? '@')
350
359
  val = val.chop
351
360
  verdict = true
352
361
  end
@@ -403,16 +412,20 @@ class Document < AbstractBlock
403
412
  attrs['stylesdir'] ||= '.'
404
413
  attrs['iconsdir'] ||= ::File.join(attrs.fetch('imagesdir', './images'), 'icons')
405
414
 
406
- @extensions = if initialize_extensions
407
- registry = if (ext_registry = options[:extensions_registry])
408
- if (ext_registry.is_a? Extensions::Registry) ||
409
- (::RUBY_ENGINE_JRUBY && (ext_registry.is_a? ::AsciidoctorJ::Extensions::ExtensionRegistry))
410
- ext_registry
415
+ if initialize_extensions
416
+ if (registry = options[:extensions_registry])
417
+ if Extensions::Registry === registry || (::RUBY_ENGINE_JRUBY &&
418
+ ::AsciidoctorJ::Extensions::ExtensionRegistry === registry)
419
+ # take it as it is
420
+ else
421
+ registry = Extensions::Registry.new
411
422
  end
412
- elsif (ext_block = options[:extensions]).is_a? ::Proc
413
- Extensions.build_registry(&ext_block)
423
+ elsif ::Proc === (ext_block = options[:extensions])
424
+ registry = Extensions.build_registry(&ext_block)
425
+ else
426
+ registry = Extensions::Registry.new
414
427
  end
415
- (registry ||= Extensions::Registry.new).activate self
428
+ @extensions = registry.activate self
416
429
  end
417
430
 
418
431
  @reader = PreprocessorReader.new self, data, Reader::Cursor.new(attrs['docfile'], @base_dir)
@@ -504,7 +517,7 @@ class Document < AbstractBlock
504
517
  #
505
518
  # returns the next value in the sequence according to the current value's type
506
519
  def nextval(current)
507
- if current.is_a?(::Integer)
520
+ if ::Integer === current
508
521
  current + 1
509
522
  else
510
523
  intval = current.to_i
@@ -519,7 +532,7 @@ class Document < AbstractBlock
519
532
  def register(type, value)
520
533
  case type
521
534
  when :ids
522
- if value.is_a?(::Array)
535
+ if ::Array === value
523
536
  @references[:ids][value[0]] = (value[1] || '[' + value[0] + ']')
524
537
  else
525
538
  @references[:ids][value] = '[' + value + ']'
@@ -902,7 +915,7 @@ class Document < AbstractBlock
902
915
  attrs['backend'] = new_backend
903
916
  attrs[%(backend-#{new_backend})] = ''
904
917
  # (re)initialize converter
905
- if (@converter = create_converter).is_a? Converter::BackendInfo
918
+ if Converter::BackendInfo === (@converter = create_converter)
906
919
  new_basebackend = @converter.basebackend
907
920
  attrs['outfilesuffix'] = @converter.outfilesuffix unless attribute_locked? 'outfilesuffix'
908
921
  new_filetype = @converter.filetype
@@ -974,10 +987,10 @@ class Document < AbstractBlock
974
987
  converter_opts[:template_engine_options] = @options[:template_engine_options]
975
988
  converter_opts[:eruby] = @options[:eruby]
976
989
  end
977
- converter_factory = if (converter = @options[:converter])
978
- Converter::Factory.new ::Hash[backend, converter]
990
+ if (converter = @options[:converter])
991
+ converter_factory = Converter::Factory.new ::Hash[backend, converter]
979
992
  else
980
- Converter::Factory.default false
993
+ converter_factory = Converter::Factory.default false
981
994
  end
982
995
  # QUESTION should we honor the convert_opts?
983
996
  # QUESTION should we pass through all options and attributes too?
@@ -992,6 +1005,11 @@ class Document < AbstractBlock
992
1005
  def convert opts = {}
993
1006
  parse unless @parsed
994
1007
  restore_attributes
1008
+ unless @safe >= SafeMode::SERVER || opts.empty?
1009
+ # QUESTION should we store these on the Document object?
1010
+ @attributes.delete 'outfile' unless (@attributes['outfile'] = opts['outfile'])
1011
+ @attributes.delete 'outdir' unless (@attributes['outdir'] = opts['outdir'])
1012
+ end
995
1013
 
996
1014
  # QUESTION should we add processors that execute before conversion begins?
997
1015
  unless @converter
@@ -1029,7 +1047,7 @@ class Document < AbstractBlock
1029
1047
  # If the converter responds to :write, delegate the work of writing the file
1030
1048
  # to that method. Otherwise, write the output the specified file.
1031
1049
  def write output, target
1032
- if @converter.is_a? Writer
1050
+ if Writer === @converter
1033
1051
  @converter.write output, target
1034
1052
  else
1035
1053
  if target.respond_to? :write
@@ -1068,59 +1086,69 @@ class Document < AbstractBlock
1068
1086
  super
1069
1087
  end
1070
1088
 
1071
- # Public: Read the docinfo file(s) for inclusion in the
1072
- # document template
1089
+ # Public: Read the docinfo file(s) for inclusion in the document template
1073
1090
  #
1074
1091
  # If the docinfo1 attribute is set, read the docinfo.ext file. If the docinfo
1075
1092
  # attribute is set, read the doc-name.docinfo.ext file. If the docinfo2
1076
1093
  # attribute is set, read both files in that order.
1077
1094
  #
1078
- # pos - The Symbol position of the docinfo, either :header or :footer. (default: :header)
1079
- # ext - The extension of the docinfo file(s). If not set, the extension
1080
- # will be determined based on the basebackend. (default: nil)
1095
+ # location - The Symbol location of the docinfo, either :header or :footer. (default: :header)
1096
+ # ext - The extension of the docinfo file(s). If not set, the extension
1097
+ # will be determined based on the basebackend. (default: nil)
1081
1098
  #
1082
1099
  # returns The contents of the docinfo file(s)
1083
- def docinfo(pos = :header, ext = nil)
1100
+ def docinfo(location = :header, ext = nil)
1084
1101
  if safe >= SafeMode::SECURE
1085
1102
  ''
1086
1103
  else
1087
- case pos
1088
- when :footer
1089
- qualifier = '-footer'
1090
- else
1091
- qualifier = nil
1092
- end
1093
- ext = @attributes['outfilesuffix'] if ext.nil?
1104
+ qualifier = (location == :footer ? '-footer' : nil)
1105
+ ext = @attributes['outfilesuffix'] unless ext
1106
+ docinfodir = @attributes['docinfodir']
1094
1107
 
1095
1108
  content = nil
1096
1109
 
1097
1110
  docinfo = @attributes.key?('docinfo')
1098
1111
  docinfo1 = @attributes.key?('docinfo1')
1099
1112
  docinfo2 = @attributes.key?('docinfo2')
1100
- docinfo_filename = "docinfo#{qualifier}#{ext}"
1113
+ docinfo_filename = %(docinfo#{qualifier}#{ext})
1101
1114
  if docinfo1 || docinfo2
1102
- docinfo_path = normalize_system_path(docinfo_filename)
1103
- content = read_asset(docinfo_path)
1104
- unless content.nil?
1105
- # FIXME normalize these lines!
1106
- content.force_encoding ::Encoding::UTF_8 if FORCE_ENCODING
1107
- content = sub_attributes(content.split EOL) * EOL
1115
+ docinfo_path = normalize_system_path(docinfo_filename, docinfodir)
1116
+ # NOTE normalizing the lines is essential if we're performing substitutions
1117
+ if (content = read_asset(docinfo_path, :normalize => true))
1118
+ content = sub_attributes(content)
1108
1119
  end
1109
1120
  end
1110
1121
 
1111
1122
  if (docinfo || docinfo2) && @attributes.key?('docname')
1112
- docinfo_path = normalize_system_path("#{@attributes['docname']}-#{docinfo_filename}")
1113
- content2 = read_asset(docinfo_path)
1114
- unless content2.nil?
1115
- # FIXME normalize these lines!
1116
- content2.force_encoding ::Encoding::UTF_8 if FORCE_ENCODING
1117
- content2 = sub_attributes(content2.split EOL) * EOL
1118
- content = content.nil? ? content2 : "#{content}#{EOL}#{content2}"
1123
+ docinfo_path = normalize_system_path(%(#{@attributes['docname']}-#{docinfo_filename}), docinfodir)
1124
+ # NOTE normalizing the lines is essential if we're performing substitutions
1125
+ if (content2 = read_asset(docinfo_path, :normalize => true))
1126
+ content2 = sub_attributes(content2)
1127
+ content = content ? %(#{content}#{EOL}#{content2}) : content2
1119
1128
  end
1120
1129
  end
1121
1130
 
1122
- # to_s forces nil to empty string
1123
- content.to_s
1131
+ # TODO allow document to control whether extension docinfo is contributed
1132
+ if @extensions && docinfo_processors?(location)
1133
+ contentx = @docinfo_processor_extensions[location].map {|candidate| candidate.process_method[self] }.compact * EOL
1134
+ content = content ? %(#{content}#{EOL}#{contentx}) : contentx
1135
+ end
1136
+
1137
+ # coerce to string (in case the value is nil)
1138
+ %(#{content})
1139
+ end
1140
+ end
1141
+
1142
+ def docinfo_processors?(location = :header)
1143
+ if @docinfo_processor_extensions.key?(location)
1144
+ # false means we already performed a lookup and didn't find any
1145
+ @docinfo_processor_extensions[location] != false
1146
+ else
1147
+ if @extensions && @document.extensions.docinfo_processors?(location)
1148
+ !!(@docinfo_processor_extensions[location] = @document.extensions.docinfo_processors(location))
1149
+ else
1150
+ @docinfo_processor_extensions[location] = false
1151
+ end
1124
1152
  end
1125
1153
  end
1126
1154