asciidoctor 0.1.4 → 1.5.0

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

Potentially problematic release.


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

Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +209 -25
  3. data/{LICENSE → LICENSE.adoc} +4 -3
  4. data/README.adoc +392 -395
  5. data/Rakefile +94 -137
  6. data/benchmark/benchmark.rb +127 -0
  7. data/benchmark/sample-data/mdbasics.adoc +334 -0
  8. data/bin/asciidoctor +5 -8
  9. data/bin/asciidoctor-safe +4 -8
  10. data/compat/asciidoc.conf +78 -11
  11. data/compat/font-awesome-3-compat.css +397 -0
  12. data/data/stylesheets/asciidoctor-default.css +399 -0
  13. data/data/stylesheets/coderay-asciidoctor.css +89 -0
  14. data/features/open_block.feature +92 -0
  15. data/features/pass_block.feature +66 -0
  16. data/features/step_definitions.rb +42 -0
  17. data/features/text_formatting.feature +55 -0
  18. data/features/xref.feature +116 -0
  19. data/lib/asciidoctor.rb +1155 -605
  20. data/lib/asciidoctor/abstract_block.rb +157 -71
  21. data/lib/asciidoctor/abstract_node.rb +150 -93
  22. data/lib/asciidoctor/attribute_list.rb +85 -90
  23. data/lib/asciidoctor/block.rb +51 -24
  24. data/lib/asciidoctor/callouts.rb +4 -7
  25. data/lib/asciidoctor/cli.rb +3 -0
  26. data/lib/asciidoctor/cli/invoker.rb +86 -76
  27. data/lib/asciidoctor/cli/options.rb +111 -61
  28. data/lib/asciidoctor/converter.rb +232 -0
  29. data/lib/asciidoctor/converter/base.rb +58 -0
  30. data/lib/asciidoctor/converter/composite.rb +66 -0
  31. data/lib/asciidoctor/converter/docbook45.rb +94 -0
  32. data/lib/asciidoctor/converter/docbook5.rb +684 -0
  33. data/lib/asciidoctor/converter/factory.rb +225 -0
  34. data/lib/asciidoctor/converter/html5.rb +1081 -0
  35. data/lib/asciidoctor/converter/template.rb +296 -0
  36. data/lib/asciidoctor/core_ext.rb +7 -0
  37. data/lib/asciidoctor/core_ext/object/nil_or_empty.rb +23 -0
  38. data/lib/asciidoctor/core_ext/string/chr.rb +6 -0
  39. data/lib/asciidoctor/core_ext/symbol/length.rb +6 -0
  40. data/lib/asciidoctor/document.rb +590 -304
  41. data/lib/asciidoctor/extensions.rb +1100 -308
  42. data/lib/asciidoctor/helpers.rb +109 -46
  43. data/lib/asciidoctor/inline.rb +16 -9
  44. data/lib/asciidoctor/list.rb +23 -15
  45. data/lib/asciidoctor/opal_ext.rb +4 -0
  46. data/lib/asciidoctor/opal_ext/comparable.rb +38 -0
  47. data/lib/asciidoctor/opal_ext/dir.rb +13 -0
  48. data/lib/asciidoctor/opal_ext/error.rb +2 -0
  49. data/lib/asciidoctor/opal_ext/file.rb +125 -0
  50. data/lib/asciidoctor/{lexer.rb → parser.rb} +646 -455
  51. data/lib/asciidoctor/path_resolver.rb +141 -77
  52. data/lib/asciidoctor/reader.rb +257 -187
  53. data/lib/asciidoctor/section.rb +12 -16
  54. data/lib/asciidoctor/stylesheets.rb +91 -0
  55. data/lib/asciidoctor/substitutors.rb +1548 -0
  56. data/lib/asciidoctor/table.rb +73 -57
  57. data/lib/asciidoctor/timings.rb +39 -0
  58. data/lib/asciidoctor/version.rb +1 -1
  59. data/man/asciidoctor.1 +22 -14
  60. data/man/asciidoctor.adoc +18 -10
  61. data/test/attributes_test.rb +314 -14
  62. data/test/blocks_test.rb +763 -118
  63. data/test/converter_test.rb +352 -0
  64. data/test/document_test.rb +518 -199
  65. data/test/extensions_test.rb +273 -103
  66. data/test/fixtures/asciidoc_index.txt +27 -13
  67. data/test/fixtures/basic-docinfo.xml +1 -1
  68. data/test/fixtures/chapter-a.adoc +3 -0
  69. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
  70. data/test/fixtures/docinfo.xml +1 -1
  71. data/test/fixtures/include-file.asciidoc +2 -0
  72. data/test/fixtures/master.adoc +5 -0
  73. data/test/invoker_test.rb +173 -61
  74. data/test/links_test.rb +97 -21
  75. data/test/lists_test.rb +181 -22
  76. data/test/options_test.rb +86 -2
  77. data/test/paragraphs_test.rb +47 -5
  78. data/test/{lexer_test.rb → parser_test.rb} +128 -57
  79. data/test/paths_test.rb +36 -1
  80. data/test/preamble_test.rb +25 -17
  81. data/test/reader_test.rb +404 -249
  82. data/test/sections_test.rb +623 -58
  83. data/test/substitutions_test.rb +609 -132
  84. data/test/tables_test.rb +198 -24
  85. data/test/test_helper.rb +101 -31
  86. data/test/text_test.rb +88 -31
  87. metadata +160 -64
  88. data/Gemfile +0 -12
  89. data/Guardfile +0 -18
  90. data/asciidoctor.gemspec +0 -143
  91. data/lib/asciidoctor/backends/_stylesheets.rb +0 -466
  92. data/lib/asciidoctor/backends/base_template.rb +0 -114
  93. data/lib/asciidoctor/backends/docbook45.rb +0 -774
  94. data/lib/asciidoctor/backends/docbook5.rb +0 -103
  95. data/lib/asciidoctor/backends/html5.rb +0 -1214
  96. data/lib/asciidoctor/renderer.rb +0 -259
  97. data/lib/asciidoctor/substituters.rb +0 -1083
  98. data/test/fixtures/asciidoc.txt +0 -105
  99. data/test/fixtures/ascshort.txt +0 -32
  100. data/test/fixtures/list_elements.asciidoc +0 -10
  101. data/test/renderer_test.rb +0 -162
@@ -0,0 +1,225 @@
1
+ module Asciidoctor
2
+ module Converter
3
+ # A factory for instantiating converters that are used to convert a
4
+ # {Document} (i.e., a parsed AsciiDoc tree structure) or {AbstractNode} to
5
+ # a backend format such as HTML or DocBook. {Factory Converter::Factory} is
6
+ # the primary entry point for creating, registering and accessing
7
+ # converters.
8
+ #
9
+ # {Converter} objects are instantiated by passing a String backend name
10
+ # and, optionally, an options Hash to the {Factory#create} method. The
11
+ # backend can be thought of as an intent to convert a document to a
12
+ # specified format. For example:
13
+ #
14
+ # converter = Asciidoctor::Converter::Factory.create 'html5', :htmlsyntax => 'xml'
15
+ #
16
+ # Converter objects are thread safe. They only survive the lifetime of a single conversion.
17
+ #
18
+ # A singleton instance of {Factory Converter::Factory} can be accessed
19
+ # using the {Factory.default} method. This instance maintains the global
20
+ # registry of statically registered converters. The registery includes
21
+ # built-in converters for {Html5Converter HTML 5}, {DocBook5Converter
22
+ # DocBook 5} and {DocBook45Converter DocBook 4.5}, as well as any custom
23
+ # converters that have been discovered or explicitly registered.
24
+ #
25
+ # If the {https://rubygems.org/gems/thread_safe thread_safe} gem is
26
+ # installed, access to the default factory is guaranteed to be thread safe.
27
+ # Otherwise, a warning is issued to the user.
28
+ class Factory
29
+ @__default__ = nil
30
+ class << self
31
+
32
+ # Public: Retrieves a singleton instance of {Factory Converter::Factory}.
33
+ #
34
+ # If the thread_safe gem is installed, the registry of converters is
35
+ # initialized as a ThreadSafe::Cache. Otherwise, a warning is issued and
36
+ # the registry of converters is initialized using a normal Hash.
37
+ #
38
+ # initialize_singleton - A Boolean to indicate whether the singleton should
39
+ # be initialize if it has not already been created.
40
+ # If false, and a singleton has not been previously
41
+ # initialized, a fresh instance is returned.
42
+ #
43
+ # Returns the default [Factory] singleton instance
44
+ def default initialize_singleton = true
45
+ return @__default__ || new unless initialize_singleton
46
+ # FIXME this assignment is not thread_safe, may need to use a ::Threadsafe helper here
47
+ @__default__ ||= begin
48
+ require 'thread_safe'.to_s unless defined? ::ThreadSafe
49
+ new ::ThreadSafe::Cache.new
50
+ rescue ::LoadError
51
+ warn 'asciidoctor: WARNING: gem \'thread_safe\' is not installed. This gem recommended when registering custom converters.'
52
+ new
53
+ end
54
+ end
55
+
56
+ # Public: Register a custom converter in the global converter factory to
57
+ # handle conversion to the specified backends. If the backend value is an
58
+ # asterisk, the converter is used to handle any backend that does not have
59
+ # an explicit converter.
60
+ #
61
+ # converter - The Converter class to register
62
+ # backends - A String Array of backend names that this converter should
63
+ # be registered to handle (optional, default: ['*'])
64
+ #
65
+ # Returns nothing
66
+ def register converter, backends = ['*']
67
+ default.register converter, backends
68
+ end
69
+
70
+ # Public: Lookup the custom converter for the specified backend in the
71
+ # global factory.
72
+ #
73
+ # This method does not resolve the built-in converters.
74
+ #
75
+ # backend - The String backend name
76
+ #
77
+ # Returns the [Converter] class registered to convert the specified backend
78
+ # or nil if no match is found
79
+ def resolve backend
80
+ default.resolve backend
81
+ end
82
+
83
+ # Public: Lookup the converter for the specified backend in the global
84
+ # factory and instantiate it, forwarding the Hash of options to the
85
+ # constructor of the converter class.
86
+ #
87
+ # If the custom converter is not found, an attempt will be made to find
88
+ # and instantiate a built-in converter.
89
+ #
90
+ #
91
+ # backend - The String backend name
92
+ # opts - A Hash of options to pass to the converter
93
+ #
94
+ # Returns an instance of [Converter] for converting the specified backend or
95
+ # nil if no match is found.
96
+ def create backend, opts = {}
97
+ default.create backend, opts
98
+ end
99
+
100
+ # Public: Retrieve the global Hash of custom Converter classes keyed by backend.
101
+ #
102
+ # Returns the the global [Hash] of custom Converter classes
103
+ def converters
104
+ default.converters
105
+ end
106
+
107
+ # Public: Unregister all Converter classes in the global factory.
108
+ #
109
+ # Returns nothing
110
+ def unregister_all
111
+ default.unregister_all
112
+ end
113
+ end
114
+
115
+ # Public: Get the Hash of Converter classes keyed by backend name
116
+ attr_reader :converters
117
+
118
+ def initialize converters = nil
119
+ @converters = converters || {}
120
+ @star_converter = nil
121
+ end
122
+
123
+ # Public: Register a custom converter with this factory to handle conversion
124
+ # to the specified backends. If the backend value is an asterisk, the
125
+ # converter is used to handle any backend that does not have an explicit
126
+ # converter.
127
+ #
128
+ # converter - The Converter class to register
129
+ # backends - A String Array of backend names that this converter should
130
+ # be registered to handle (optional, default: ['*'])
131
+ #
132
+ # Returns nothing
133
+ def register converter, backends = ['*']
134
+ backends.each do |backend|
135
+ @converters[backend] = converter
136
+ if backend == '*'
137
+ @star_converter = converter
138
+ end
139
+ end
140
+ nil
141
+ end
142
+
143
+ # Public: Lookup the custom converter registered with this factory to handle
144
+ # the specified backend.
145
+ #
146
+ # backend - The String backend name
147
+ #
148
+ # Returns the [Converter] class registered to convert the specified backend
149
+ # or nil if no match is found
150
+ def resolve backend
151
+ @converters && (@converters[backend] || @star_converter)
152
+ end
153
+
154
+ # Public: Unregister all Converter classes that are registered with this
155
+ # factory.
156
+ #
157
+ # Returns nothing
158
+ def unregister_all
159
+ @converters.clear
160
+ @star_converter = nil
161
+ end
162
+
163
+ # Public: Create a new Converter object that can be used to convert the
164
+ # {AbstractNode} (typically a {Document}) to the specified String backend.
165
+ # This method accepts an optional Hash of options that are passed to the
166
+ # converter's constructor.
167
+ #
168
+ # If a custom Converter is found to convert the specified backend, it is
169
+ # instantiated (if necessary) and returned immediately. If a custom
170
+ # Converter is not found, an attempt is made to resolve a built-in
171
+ # converter. If the `:template_dirs` key is found in the Hash passed as the
172
+ # second argument, a {CompositeConverter} is created that delegates to a
173
+ # {TemplateConverter} and, if resolved, the built-in converter. If the
174
+ # `:template_dirs` key is not found, the built-in converter is returned
175
+ # or nil if no converter is resolved.
176
+ #
177
+ # backend - the String backend name
178
+ # opts - an optional Hash of options that get passed on to the converter's
179
+ # constructor. If the :template_dirs key is found in the options
180
+ # Hash, this method returns a {CompositeConverter} that delegates
181
+ # to a {TemplateConverter}. (optional, default: {})
182
+ #
183
+ # Returns the [Converter] object
184
+ def create backend, opts = {}
185
+ if (converter = resolve backend)
186
+ if converter.is_a? ::Class
187
+ return converter.new backend, opts
188
+ else
189
+ return converter
190
+ end
191
+ end
192
+
193
+ base_converter = case backend
194
+ when 'html5'
195
+ unless defined? ::Asciidoctor::Converter::Html5Converter
196
+ require 'asciidoctor/converter/html5'.to_s
197
+ end
198
+ Html5Converter.new backend, opts
199
+ when 'docbook5'
200
+ unless defined? ::Asciidoctor::Converter::DocBook5Converter
201
+ require 'asciidoctor/converter/docbook5'.to_s
202
+ end
203
+ DocBook5Converter.new backend, opts
204
+ when 'docbook45'
205
+ unless defined? ::Asciidoctor::Converter::DocBook45Converter
206
+ require 'asciidoctor/converter/docbook45'.to_s
207
+ end
208
+ DocBook45Converter.new backend, opts
209
+ end
210
+
211
+ return base_converter unless opts.key? :template_dirs
212
+
213
+ unless defined? ::Asciidoctor::Converter::TemplateConverter
214
+ require 'asciidoctor/converter/template'.to_s
215
+ end
216
+ unless defined? ::Asciidoctor::Converter::CompositeConverter
217
+ require 'asciidoctor/converter/composite'.to_s
218
+ end
219
+ template_converter = TemplateConverter.new backend, opts[:template_dirs], opts
220
+ # QUESTION should we omit the composite converter if built_in_converter is nil?
221
+ CompositeConverter.new backend, template_converter, base_converter
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,1081 @@
1
+ module Asciidoctor
2
+ # A built-in {Converter} implementation that generates HTML 5 output
3
+ # consistent with the html5 backend from AsciiDoc Python.
4
+ class Converter::Html5Converter < Converter::BuiltIn
5
+ QUOTE_TAGS = {
6
+ :emphasis => ['<em>', '</em>', true],
7
+ :strong => ['<strong>', '</strong>', true],
8
+ :monospaced => ['<code>', '</code>', true],
9
+ :superscript => ['<sup>', '</sup>', true],
10
+ :subscript => ['<sub>', '</sub>', true],
11
+ :double => ['&#8220;', '&#8221;', false],
12
+ :single => ['&#8216;', '&#8217;', false],
13
+ :mark => ['<mark>', '</mark>', true],
14
+ :asciimath => ['\\$', '\\$', false],
15
+ :latexmath => ['\\(', '\\)', false]
16
+ # Opal can't resolve these constants when referenced here
17
+ #:asciimath => INLINE_MATH_DELIMITERS[:asciimath] + [false],
18
+ #:latexmath => INLINE_MATH_DELIMITERS[:latexmath] + [false]
19
+ }
20
+ QUOTE_TAGS.default = [nil, nil, nil]
21
+
22
+ def initialize backend, opts = {}
23
+ @xml_mode = opts[:htmlsyntax] == 'xml'
24
+ @void_element_slash = @xml_mode ? '/' : nil
25
+ @stylesheets = Stylesheets.instance
26
+ end
27
+
28
+ def document node
29
+ result = []
30
+ slash = @void_element_slash
31
+ br = %(<br#{slash}>)
32
+ asset_uri_scheme = (node.attr 'asset-uri-scheme', 'https')
33
+ asset_uri_scheme = %(#{asset_uri_scheme}:) unless asset_uri_scheme.empty?
34
+ cdn_base = %(#{asset_uri_scheme}//cdnjs.cloudflare.com/ajax/libs)
35
+ linkcss = node.safe >= SafeMode::SECURE || (node.attr? 'linkcss')
36
+ result << '<!DOCTYPE html>'
37
+ lang_attribute = (node.attr? 'nolang') ? nil : %( lang="#{node.attr 'lang', 'en'}")
38
+ result << %(<html#{@xml_mode ? ' xmlns="http://www.w3.org/1999/xhtml"' : nil}#{lang_attribute}>)
39
+ result << %(<head>
40
+ <meta charset="#{node.attr 'encoding', 'UTF-8'}"#{slash}>
41
+ <!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"#{slash}><![endif]-->
42
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"#{slash}>
43
+ <meta name="generator" content="Asciidoctor #{node.attr 'asciidoctor-version'}"#{slash}>)
44
+
45
+ result << %(<meta name="application-name" content="#{node.attr 'app-name'}"#{slash}>) if node.attr? 'app-name'
46
+ result << %(<meta name="description" content="#{node.attr 'description'}"#{slash}>) if node.attr? 'description'
47
+ result << %(<meta name="keywords" content="#{node.attr 'keywords'}"#{slash}>) if node.attr? 'keywords'
48
+ result << %(<meta name="author" content="#{node.attr 'authors'}"#{slash}>) if node.attr? 'authors'
49
+ result << %(<meta name="copyright" content="#{node.attr 'copyright'}"#{slash}>) if node.attr? 'copyright'
50
+
51
+ result << %(<title>#{node.doctitle :sanitize => true, :use_fallback => true}</title>)
52
+ if DEFAULT_STYLESHEET_KEYS.include?(node.attr 'stylesheet')
53
+ 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
+ end
56
+ if linkcss
57
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path DEFAULT_STYLESHEET_NAME, (node.attr 'stylesdir', '')}"#{slash}>)
58
+ else
59
+ result << @stylesheets.embed_primary_stylesheet
60
+ end
61
+ elsif node.attr? 'stylesheet'
62
+ if linkcss
63
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path((node.attr 'stylesheet'), (node.attr 'stylesdir', ''))}"#{slash}>)
64
+ else
65
+ result << %(<style>
66
+ #{node.read_asset node.normalize_system_path((node.attr 'stylesheet'), (node.attr 'stylesdir', '')), true}
67
+ </style>)
68
+ end
69
+ end
70
+
71
+ if node.attr? 'icons', 'font'
72
+ 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
+ else
75
+ 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
+ end
78
+ end
79
+
80
+ case node.attr 'source-highlighter'
81
+ when 'coderay'
82
+ if (node.attr 'coderay-css', 'class') == 'class'
83
+ if linkcss
84
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path @stylesheets.coderay_stylesheet_name, (node.attr 'stylesdir', '')}"#{slash}>)
85
+ else
86
+ result << @stylesheets.embed_coderay_stylesheet
87
+ end
88
+ end
89
+ when 'pygments'
90
+ if (node.attr 'pygments-css', 'class') == 'class'
91
+ pygments_style = node.attr 'pygments-style'
92
+ if linkcss
93
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path @stylesheets.pygments_stylesheet_name(pygments_style), (node.attr 'stylesdir', '')}"#{slash}>)
94
+ else
95
+ result << (@stylesheets.embed_pygments_stylesheet pygments_style)
96
+ end
97
+ end
98
+ when 'highlightjs', 'highlight.js'
99
+ highlightjs_path = node.attr 'highlightjsdir', %(#{cdn_base}/highlight.js/8.1)
100
+ result << %(<link rel="stylesheet" href="#{highlightjs_path}/styles/#{node.attr 'highlightjs-theme', 'github'}.min.css"#{slash}>
101
+ <script src="#{highlightjs_path}/highlight.min.js"></script>
102
+ <script>hljs.initHighlightingOnLoad()</script>)
103
+ when 'prettify'
104
+ prettify_path = node.attr 'prettifydir', %(#{cdn_base}/prettify/r298)
105
+ result << %(<link rel="stylesheet" href="#{prettify_path}/#{node.attr 'prettify-theme', 'prettify'}.min.css"#{slash}>
106
+ <script src="#{prettify_path}/prettify.min.js"></script>
107
+ <script>document.addEventListener('DOMContentLoaded', prettyPrint)</script>)
108
+ end
109
+
110
+ if node.attr? 'stem'
111
+ result << %(<script type="text/x-mathjax-config">
112
+ MathJax.Hub.Config({
113
+ tex2jax: {
114
+ inlineMath: [#{INLINE_MATH_DELIMITERS[:latexmath]}],
115
+ displayMath: [#{BLOCK_MATH_DELIMITERS[:latexmath]}],
116
+ ignoreClass: "nostem|nolatexmath"
117
+ },
118
+ asciimath2jax: {
119
+ delimiters: [#{BLOCK_MATH_DELIMITERS[:asciimath]}],
120
+ ignoreClass: "nostem|noasciimath"
121
+ }
122
+ });
123
+ </script>
124
+ <script src="#{cdn_base}/mathjax/2.4.0/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script>)
125
+ end
126
+
127
+ unless (docinfo_content = node.docinfo).empty?
128
+ result << docinfo_content
129
+ end
130
+
131
+ result << '</head>'
132
+ body_attrs = []
133
+ if node.id
134
+ body_attrs << %(id="#{node.id}")
135
+ end
136
+ if (node.attr? 'toc-class') && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
137
+ body_attrs << %(class="#{node.doctype} #{node.attr 'toc-class'} toc-#{node.attr 'toc-position', 'header'}")
138
+ else
139
+ body_attrs << %(class="#{node.doctype}")
140
+ end
141
+ if node.attr? 'max-width'
142
+ body_attrs << %(style="max-width: #{node.attr 'max-width'};")
143
+ end
144
+ result << %(<body #{body_attrs * ' '}>)
145
+
146
+ unless node.noheader
147
+ result << '<div id="header">'
148
+ if node.doctype == 'manpage'
149
+ result << %(<h1>#{node.doctitle} Manual Page</h1>)
150
+ if (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
151
+ result << %(<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
152
+ <div id="toctitle">#{node.attr 'toc-title'}</div>
153
+ #{outline node}
154
+ </div>)
155
+ end
156
+ result << %(<h2>#{node.attr 'manname-title'}</h2>
157
+ <div class="sectionbody">
158
+ <p>#{node.attr 'manname'} - #{node.attr 'manpurpose'}</p>
159
+ </div>)
160
+ else
161
+ if node.has_header?
162
+ result << %(<h1>#{node.header.title}</h1>) unless node.notitle
163
+ details = []
164
+ if node.attr? 'author'
165
+ details << %(<span id="author" class="author">#{node.attr 'author'}</span>#{br})
166
+ if node.attr? 'email'
167
+ details << %(<span id="email" class="email">#{node.sub_macros(node.attr 'email')}</span>#{br})
168
+ end
169
+ if (authorcount = (node.attr 'authorcount').to_i) > 1
170
+ (2..authorcount).each do |idx|
171
+ details << %(<span id="author#{idx}" class="author">#{node.attr "author_#{idx}"}</span>#{br})
172
+ if node.attr? %(email_#{idx})
173
+ details << %(<span id="email#{idx}" class="email">#{node.sub_macros(node.attr "email_#{idx}")}</span>#{br})
174
+ end
175
+ end
176
+ end
177
+ end
178
+ if node.attr? 'revnumber'
179
+ details << %(<span id="revnumber">#{((node.attr 'version-label') || '').downcase} #{node.attr 'revnumber'}#{(node.attr? 'revdate') ? ',' : ''}</span>)
180
+ end
181
+ if node.attr? 'revdate'
182
+ details << %(<span id="revdate">#{node.attr 'revdate'}</span>)
183
+ end
184
+ if node.attr? 'revremark'
185
+ details << %(#{br}<span id="revremark">#{node.attr 'revremark'}</span>)
186
+ end
187
+ unless details.empty?
188
+ result << '<div class="details">'
189
+ result.concat details
190
+ result << '</div>'
191
+ end
192
+ end
193
+
194
+ if (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
195
+ result << %(<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
196
+ <div id="toctitle">#{node.attr 'toc-title'}</div>
197
+ #{outline node}
198
+ </div>)
199
+ end
200
+ end
201
+ result << '</div>'
202
+ end
203
+
204
+ result << %(<div id="content">
205
+ #{node.content}
206
+ </div>)
207
+
208
+ if node.footnotes? && !(node.attr? 'nofootnotes')
209
+ result << %(<div id="footnotes">
210
+ <hr#{slash}>)
211
+ node.footnotes.each do |footnote|
212
+ result << %(<div class="footnote" id="_footnote_#{footnote.index}">
213
+ <a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a>. #{footnote.text}
214
+ </div>)
215
+ end
216
+ result << '</div>'
217
+ end
218
+ unless node.nofooter
219
+ result << '<div id="footer">'
220
+ result << '<div id="footer-text">'
221
+ if node.attr? 'revnumber'
222
+ result << %(#{node.attr 'version-label'} #{node.attr 'revnumber'}#{br})
223
+ end
224
+ if node.attr? 'last-update-label'
225
+ result << %(#{node.attr 'last-update-label'} #{node.attr 'docdatetime'})
226
+ end
227
+ result << '</div>'
228
+ unless (docinfo_content = node.docinfo :footer).empty?
229
+ result << docinfo_content
230
+ end
231
+ result << '</div>'
232
+ end
233
+
234
+ result << '</body>'
235
+ result << '</html>'
236
+ result * EOL
237
+ end
238
+
239
+ def embedded node
240
+ result = []
241
+ if !node.notitle && node.has_header?
242
+ id_attr = node.id ? %( id="#{node.id}") : nil
243
+ result << %(<h1#{id_attr}>#{node.header.title}</h1>)
244
+ end
245
+
246
+ result << node.content
247
+
248
+ if node.footnotes? && !(node.attr? 'nofootnotes')
249
+ result << %(<div id="footnotes">
250
+ <hr#{@void_element_slash}>)
251
+ node.footnotes.each do |footnote|
252
+ result << %(<div class="footnote" id="_footnote_#{footnote.index}">
253
+ <a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a> #{footnote.text}
254
+ </div>)
255
+ end
256
+
257
+ result << '</div>'
258
+ end
259
+
260
+ result * EOL
261
+ end
262
+
263
+ def outline node, opts = {}
264
+ return if (sections = node.sections).empty?
265
+ sectnumlevels = opts[:sectnumlevels] || (node.document.attr 'sectnumlevels', 3).to_i
266
+ toclevels = opts[:toclevels] || (node.document.attr 'toclevels', 2).to_i
267
+ result = []
268
+ # FIXME the level for special sections should be set correctly in the model
269
+ # slevel will only be 0 if we have a book doctype with parts
270
+ slevel = (first_section = sections[0]).level
271
+ slevel = 1 if slevel == 0 && first_section.special
272
+ result << %(<ul class="sectlevel#{slevel}">)
273
+ sections.each do |section|
274
+ section_num = (section.numbered && !section.caption && section.level <= sectnumlevels) ? %(#{section.sectnum} ) : nil
275
+ if section.level < toclevels && (child_toc_level = outline section, :toclevels => toclevels, :secnumlevels => sectnumlevels)
276
+ result << %(<li><a href="##{section.id}">#{section_num}#{section.captioned_title}</a>)
277
+ result << child_toc_level
278
+ result << '</li>'
279
+ else
280
+ result << %(<li><a href="##{section.id}">#{section_num}#{section.captioned_title}</a></li>)
281
+ end
282
+ end
283
+ result << '</ul>'
284
+ result * EOL
285
+ end
286
+
287
+ def section node
288
+ slevel = node.level
289
+ # QUESTION should the check for slevel be done in section?
290
+ slevel = 1 if slevel == 0 && node.special
291
+ htag = %(h#{slevel + 1})
292
+ id_attr = anchor = link_start = link_end = nil
293
+ if node.id
294
+ id_attr = %( id="#{node.id}")
295
+ if node.document.attr? 'sectanchors'
296
+ anchor = %(<a class="anchor" href="##{node.id}"></a>)
297
+ # possible idea - anchor icons GitHub-style
298
+ #if node.document.attr? 'icons', 'font'
299
+ # anchor = %(<a class="anchor" href="##{node.id}"><i class="fa fa-anchor"></i></a>)
300
+ #else
301
+ elsif node.document.attr? 'sectlinks'
302
+ link_start = %(<a class="link" href="##{node.id}">)
303
+ link_end = '</a>'
304
+ end
305
+ end
306
+
307
+ if slevel == 0
308
+ %(<h1#{id_attr} class="sect0">#{anchor}#{link_start}#{node.title}#{link_end}</h1>
309
+ #{node.content})
310
+ else
311
+ class_attr = (role = node.role) ? %( class="sect#{slevel} #{role}") : %( class="sect#{slevel}")
312
+ sectnum = if node.numbered && !node.caption && slevel <= (node.document.attr 'sectnumlevels', 3).to_i
313
+ %(#{node.sectnum} )
314
+ end
315
+ %(<div#{class_attr}>
316
+ <#{htag}#{id_attr}>#{anchor}#{link_start}#{sectnum}#{node.captioned_title}#{link_end}</#{htag}>
317
+ #{slevel == 1 ? %[<div class="sectionbody">\n#{node.content}\n</div>] : node.content}
318
+ </div>)
319
+ end
320
+ end
321
+
322
+ def admonition node
323
+ id_attr = node.id ? %( id="#{node.id}") : nil
324
+ name = node.attr 'name'
325
+ title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
326
+ caption = if node.document.attr? 'icons'
327
+ if node.document.attr? 'icons', 'font'
328
+ %(<i class="fa icon-#{name}" title="#{node.caption}"></i>)
329
+ else
330
+ %(<img src="#{node.icon_uri name}" alt="#{node.caption}"#{@void_element_slash}>)
331
+ end
332
+ else
333
+ %(<div class="title">#{node.caption}</div>)
334
+ end
335
+ %(<div#{id_attr} class="admonitionblock #{name}#{(role = node.role) && " #{role}"}">
336
+ <table>
337
+ <tr>
338
+ <td class="icon">
339
+ #{caption}
340
+ </td>
341
+ <td class="content">
342
+ #{title_element}#{node.content}
343
+ </td>
344
+ </tr>
345
+ </table>
346
+ </div>)
347
+ end
348
+
349
+ def audio node
350
+ xml = node.document.attr? 'htmlsyntax', 'xml'
351
+ id_attribute = node.id ? %( id="#{node.id}") : nil
352
+ classes = ['audioblock', node.style, node.role].compact
353
+ class_attribute = %( class="#{classes * ' '}")
354
+ title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : nil
355
+ %(<div#{id_attribute}#{class_attribute}>
356
+ #{title_element}<div class="content">
357
+ <audio src="#{node.media_uri(node.attr 'target')}"#{(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}>
358
+ Your browser does not support the audio tag.
359
+ </audio>
360
+ </div>
361
+ </div>)
362
+ end
363
+
364
+ def colist node
365
+ result = []
366
+ id_attribute = node.id ? %( id="#{node.id}") : nil
367
+ classes = ['colist', node.style, node.role].compact
368
+ class_attribute = %( class="#{classes * ' '}")
369
+
370
+ result << %(<div#{id_attribute}#{class_attribute}>)
371
+ result << %(<div class="title">#{node.title}</div>) if node.title?
372
+
373
+ if node.document.attr? 'icons'
374
+ result << '<table>'
375
+
376
+ font_icons = node.document.attr? 'icons', 'font'
377
+ node.items.each_with_index do |item, i|
378
+ num = i + 1
379
+ num_element = if font_icons
380
+ %(<i class="conum" data-value="#{num}"></i><b>#{num}</b>)
381
+ else
382
+ %(<img src="#{node.icon_uri "callouts/#{num}"}" alt="#{num}"#{@void_element_slash}>)
383
+ end
384
+ result << %(<tr>
385
+ <td>#{num_element}</td>
386
+ <td>#{item.text}</td>
387
+ </tr>)
388
+ end
389
+
390
+ result << '</table>'
391
+ else
392
+ result << '<ol>'
393
+ node.items.each do |item|
394
+ result << %(<li>
395
+ <p>#{item.text}</p>
396
+ </li>)
397
+ end
398
+ result << '</ol>'
399
+ end
400
+
401
+ result << '</div>'
402
+ result * EOL
403
+ end
404
+
405
+ def dlist node
406
+ result = []
407
+ id_attribute = node.id ? %( id="#{node.id}") : nil
408
+
409
+ classes = case node.style
410
+ when 'qanda'
411
+ ['qlist', 'qanda', node.role]
412
+ when 'horizontal'
413
+ ['hdlist', node.role]
414
+ else
415
+ ['dlist', node.style, node.role]
416
+ end.compact
417
+
418
+ class_attribute = %( class="#{classes * ' '}")
419
+
420
+ result << %(<div#{id_attribute}#{class_attribute}>)
421
+ result << %(<div class="title">#{node.title}</div>) if node.title?
422
+ case node.style
423
+ when 'qanda'
424
+ result << '<ol>'
425
+ node.items.each do |terms, dd|
426
+ result << '<li>'
427
+ [*terms].each do |dt|
428
+ result << %(<p><em>#{dt.text}</em></p>)
429
+ end
430
+ if dd
431
+ result << %(<p>#{dd.text}</p>) if dd.text?
432
+ result << dd.content if dd.blocks?
433
+ end
434
+ result << '</li>'
435
+ end
436
+ result << '</ol>'
437
+ when 'horizontal'
438
+ slash = @void_element_slash
439
+ result << '<table>'
440
+ if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
441
+ result << '<colgroup>'
442
+ col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : nil
443
+ result << %(<col#{col_style_attribute}#{slash}>)
444
+ col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : nil
445
+ result << %(<col#{col_style_attribute}#{slash}>)
446
+ result << '</colgroup>'
447
+ end
448
+ node.items.each do |terms, dd|
449
+ result << '<tr>'
450
+ result << %(<td class="hdlist1#{(node.option? 'strong') ? ' strong' : nil}">)
451
+ terms_array = [*terms]
452
+ last_term = terms_array[-1]
453
+ terms_array.each do |dt|
454
+ result << dt.text
455
+ result << %(<br#{slash}>) if dt != last_term
456
+ end
457
+ result << '</td>'
458
+ result << '<td class="hdlist2">'
459
+ if dd
460
+ result << %(<p>#{dd.text}</p>) if dd.text?
461
+ result << dd.content if dd.blocks?
462
+ end
463
+ result << '</td>'
464
+ result << '</tr>'
465
+ end
466
+ result << '</table>'
467
+ else
468
+ result << '<dl>'
469
+ dt_style_attribute = node.style ? nil : ' class="hdlist1"'
470
+ node.items.each do |terms, dd|
471
+ [*terms].each do |dt|
472
+ result << %(<dt#{dt_style_attribute}>#{dt.text}</dt>)
473
+ end
474
+ if dd
475
+ result << '<dd>'
476
+ result << %(<p>#{dd.text}</p>) if dd.text?
477
+ result << dd.content if dd.blocks?
478
+ result << '</dd>'
479
+ end
480
+ end
481
+ result << '</dl>'
482
+ end
483
+
484
+ result << '</div>'
485
+ result * EOL
486
+ end
487
+
488
+ def example node
489
+ id_attribute = node.id ? %( id="#{node.id}") : nil
490
+ title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : nil
491
+
492
+ %(<div#{id_attribute} class="#{(role = node.role) ? ['exampleblock', role] * ' ' : 'exampleblock'}">
493
+ #{title_element}<div class="content">
494
+ #{node.content}
495
+ </div>
496
+ </div>)
497
+ end
498
+
499
+ def floating_title node
500
+ tag_name = %(h#{node.level + 1})
501
+ id_attribute = node.id ? %( id="#{node.id}") : nil
502
+ classes = [node.style, node.role].compact
503
+ %(<#{tag_name}#{id_attribute} class="#{classes * ' '}">#{node.title}</#{tag_name}>)
504
+ end
505
+
506
+ def image node
507
+ align = (node.attr? 'align') ? (node.attr 'align') : nil
508
+ float = (node.attr? 'float') ? (node.attr 'float') : nil
509
+ style_attribute = if align || float
510
+ styles = [align ? %(text-align: #{align}) : nil, float ? %(float: #{float}) : nil].compact
511
+ %( style="#{styles * ';'}")
512
+ end
513
+
514
+ width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
515
+ height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
516
+
517
+ img_element = %(<img src="#{node.image_uri node.attr('target')}" alt="#{node.attr 'alt'}"#{width_attribute}#{height_attribute}#{@void_element_slash}>)
518
+ if (link = node.attr 'link')
519
+ img_element = %(<a class="image" href="#{link}">#{img_element}</a>)
520
+ end
521
+ id_attribute = node.id ? %( id="#{node.id}") : nil
522
+ classes = ['imageblock', node.style, node.role].compact
523
+ class_attribute = %( class="#{classes * ' '}")
524
+ title_element = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : nil
525
+
526
+ %(<div#{id_attribute}#{class_attribute}#{style_attribute}>
527
+ <div class="content">
528
+ #{img_element}
529
+ </div>#{title_element}
530
+ </div>)
531
+ end
532
+
533
+ def listing node
534
+ nowrap = !(node.document.attr? 'prewrap') || (node.option? 'nowrap')
535
+ if node.style == 'source'
536
+ if (language = node.attr 'language', nil, false)
537
+ code_attrs = %( data-lang="#{language}")
538
+ else
539
+ code_attrs = nil
540
+ end
541
+ case node.document.attr 'source-highlighter'
542
+ when 'coderay'
543
+ pre_class = %( class="CodeRay highlight#{nowrap ? ' nowrap' : nil}")
544
+ when 'pygments'
545
+ pre_class = %( class="pygments highlight#{nowrap ? ' nowrap' : nil}")
546
+ when 'highlightjs', 'highlight.js'
547
+ pre_class = %( class="highlightjs highlight#{nowrap ? ' nowrap' : nil}")
548
+ code_attrs = %( class="language-#{language}"#{code_attrs}) if language
549
+ when 'prettify'
550
+ pre_class = %( class="prettyprint highlight#{nowrap ? ' nowrap' : nil}#{(node.attr? 'linenums') ? ' linenums' : nil}")
551
+ code_attrs = %( class="language-#{language}"#{code_attrs}) if language
552
+ when 'html-pipeline'
553
+ pre_class = language ? %( lang="#{language}") : nil
554
+ code_attrs = nil
555
+ else
556
+ pre_class = %( class="highlight#{nowrap ? ' nowrap' : nil}")
557
+ code_attrs = %( class="language-#{language}"#{code_attrs}) if language
558
+ end
559
+ pre_start = %(<pre#{pre_class}><code#{code_attrs}>)
560
+ pre_end = '</code></pre>'
561
+ else
562
+ pre_start = %(<pre#{nowrap ? ' class="nowrap"' : nil}>)
563
+ pre_end = '</pre>'
564
+ end
565
+
566
+ id_attribute = node.id ? %( id="#{node.id}") : nil
567
+ title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : nil
568
+ %(<div#{id_attribute} class="listingblock#{(role = node.role) && " #{role}"}">
569
+ #{title_element}<div class="content">
570
+ #{pre_start}#{node.content}#{pre_end}
571
+ </div>
572
+ </div>)
573
+ end
574
+
575
+ def literal node
576
+ id_attribute = node.id ? %( id="#{node.id}") : nil
577
+ title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
578
+ nowrap = !(node.document.attr? 'prewrap') || (node.option? 'nowrap')
579
+ %(<div#{id_attribute} class="literalblock#{(role = node.role) && " #{role}"}">
580
+ #{title_element}<div class="content">
581
+ <pre#{nowrap ? ' class="nowrap"' : nil}>#{node.content}</pre>
582
+ </div>
583
+ </div>)
584
+ end
585
+
586
+ def stem node
587
+ id_attribute = node.id ? %( id="#{node.id}") : nil
588
+ title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
589
+ open, close = BLOCK_MATH_DELIMITERS[node.style.to_sym]
590
+
591
+ unless ((equation = node.content).start_with? open) && (equation.end_with? close)
592
+ equation = %(#{open}#{equation}#{close})
593
+ end
594
+
595
+ %(<div#{id_attribute} class="#{(role = node.role) ? ['stemblock', role] * ' ' : 'stemblock'}">
596
+ #{title_element}<div class="content">
597
+ #{equation}
598
+ </div>
599
+ </div>)
600
+ end
601
+
602
+ def olist node
603
+ result = []
604
+ id_attribute = node.id ? %( id="#{node.id}") : nil
605
+ classes = ['olist', node.style, node.role].compact
606
+ class_attribute = %( class="#{classes * ' '}")
607
+
608
+ result << %(<div#{id_attribute}#{class_attribute}>)
609
+ result << %(<div class="title">#{node.title}</div>) if node.title?
610
+
611
+ type_attribute = (keyword = node.list_marker_keyword) ? %( type="#{keyword}") : nil
612
+ start_attribute = (node.attr? 'start') ? %( start="#{node.attr 'start'}") : nil
613
+ result << %(<ol class="#{node.style}"#{type_attribute}#{start_attribute}>)
614
+
615
+ node.items.each do |item|
616
+ result << '<li>'
617
+ result << %(<p>#{item.text}</p>)
618
+ result << item.content if item.blocks?
619
+ result << '</li>'
620
+ end
621
+
622
+ result << '</ol>'
623
+ result << '</div>'
624
+ result * EOL
625
+ end
626
+
627
+ def open node
628
+ if (style = node.style) == 'abstract'
629
+ if node.parent == node.document && node.document.doctype == 'book'
630
+ warn 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
631
+ ''
632
+ else
633
+ id_attr = node.id ? %( id="#{node.id}") : nil
634
+ title_el = node.title? ? %(<div class="title">#{node.title}</div>) : nil
635
+ %(<div#{id_attr} class="quoteblock abstract#{(role = node.role) && " #{role}"}">
636
+ #{title_el}<blockquote>
637
+ #{node.content}
638
+ </blockquote>
639
+ </div>)
640
+ end
641
+ elsif style == 'partintro' && (node.level != 0 || node.parent.context != :section || node.document.doctype != 'book')
642
+ warn 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a book part. Excluding block content.'
643
+ ''
644
+ else
645
+ id_attr = node.id ? %( id="#{node.id}") : nil
646
+ title_el = node.title? ? %(<div class="title">#{node.title}</div>) : nil
647
+ %(<div#{id_attr} class="openblock#{style && style != 'open' ? " #{style}" : ''}#{(role = node.role) && " #{role}"}">
648
+ #{title_el}<div class="content">
649
+ #{node.content}
650
+ </div>
651
+ </div>)
652
+ end
653
+ end
654
+
655
+ def page_break node
656
+ '<div style="page-break-after: always;"></div>'
657
+ end
658
+
659
+ def paragraph node
660
+ attributes = if node.id
661
+ if node.role
662
+ %( id="#{node.id}" class="paragraph #{node.role}")
663
+ else
664
+ %( id="#{node.id}" class="paragraph")
665
+ end
666
+ elsif node.role
667
+ %( class="paragraph #{node.role}")
668
+ else
669
+ ' class="paragraph"'
670
+ end
671
+
672
+ if node.title?
673
+ %(<div#{attributes}>
674
+ <div class="title">#{node.title}</div>
675
+ <p>#{node.content}</p>
676
+ </div>)
677
+ else
678
+ %(<div#{attributes}>
679
+ <p>#{node.content}</p>
680
+ </div>)
681
+ end
682
+ end
683
+
684
+ def preamble node
685
+ toc = if (node.attr? 'toc') && (node.attr? 'toc-placement', 'preamble')
686
+ %(\n<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
687
+ <div id="toctitle">#{node.attr 'toc-title'}</div>
688
+ #{outline node.document}
689
+ </div>)
690
+ end
691
+
692
+ %(<div id="preamble">
693
+ <div class="sectionbody">
694
+ #{node.content}
695
+ </div>#{toc}
696
+ </div>)
697
+ end
698
+
699
+ def quote node
700
+ id_attribute = node.id ? %( id="#{node.id}") : nil
701
+ classes = ['quoteblock', node.role].compact
702
+ class_attribute = %( class="#{classes * ' '}")
703
+ title_element = node.title? ? %(\n<div class="title">#{node.title}</div>) : nil
704
+ attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil
705
+ citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil
706
+ if attribution || citetitle
707
+ cite_element = citetitle ? %(<cite>#{citetitle}</cite>) : nil
708
+ attribution_text = attribution ? %(&#8212; #{attribution}#{citetitle ? "<br#{@void_element_slash}>\n" : nil}) : nil
709
+ attribution_element = %(\n<div class="attribution">\n#{attribution_text}#{cite_element}\n</div>)
710
+ else
711
+ attribution_element = nil
712
+ end
713
+
714
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
715
+ <blockquote>
716
+ #{node.content}
717
+ </blockquote>#{attribution_element}
718
+ </div>)
719
+ end
720
+
721
+ def thematic_break node
722
+ %(<hr#{@void_element_slash}>)
723
+ end
724
+
725
+ def sidebar node
726
+ id_attribute = node.id ? %( id="#{node.id}") : nil
727
+ title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
728
+ %(<div#{id_attribute} class="#{(role = node.role) ? ['sidebarblock', role] * ' ' : 'sidebarblock'}">
729
+ <div class="content">
730
+ #{title_element}#{node.content}
731
+ </div>
732
+ </div>)
733
+ end
734
+
735
+ def table node
736
+ result = []
737
+ id_attribute = node.id ? %( id="#{node.id}") : nil
738
+ classes = ['tableblock', %(frame-#{node.attr 'frame', 'all'}), %(grid-#{node.attr 'grid', 'all'})]
739
+ styles = []
740
+ unless node.option? 'autowidth'
741
+ if (tablepcwidth = node.attr 'tablepcwidth') == 100
742
+ classes << 'spread'
743
+ else
744
+ styles << %(width: #{tablepcwidth}%;)
745
+ end
746
+ end
747
+ if (role = node.role)
748
+ classes << role
749
+ end
750
+ class_attribute = %( class="#{classes * ' '}")
751
+ styles << %(float: #{node.attr 'float'};) if node.attr? 'float'
752
+ style_attribute = styles.empty? ? nil : %( style="#{styles * ' '}")
753
+
754
+ result << %(<table#{id_attribute}#{class_attribute}#{style_attribute}>)
755
+ result << %(<caption class="title">#{node.captioned_title}</caption>) if node.title?
756
+ if (node.attr 'rowcount') > 0
757
+ slash = @void_element_slash
758
+ result << '<colgroup>'
759
+ if node.option? 'autowidth'
760
+ tag = %(<col#{slash}>)
761
+ node.columns.size.times do
762
+ result << tag
763
+ end
764
+ else
765
+ node.columns.each do |col|
766
+ result << %(<col style="width: #{col.attr 'colpcwidth'}%;"#{slash}>)
767
+ end
768
+ end
769
+ result << '</colgroup>'
770
+ [:head, :foot, :body].select {|tsec| !node.rows[tsec].empty? }.each do |tsec|
771
+ result << %(<t#{tsec}>)
772
+ node.rows[tsec].each do |row|
773
+ result << '<tr>'
774
+ row.each do |cell|
775
+ if tsec == :head
776
+ cell_content = cell.text
777
+ else
778
+ case cell.style
779
+ when :asciidoc
780
+ cell_content = %(<div>#{cell.content}</div>)
781
+ when :verse
782
+ cell_content = %(<div class="verse">#{cell.text}</div>)
783
+ when :literal
784
+ cell_content = %(<div class="literal"><pre>#{cell.text}</pre></div>)
785
+ else
786
+ cell_content = ''
787
+ cell.content.each do |text|
788
+ cell_content = %(#{cell_content}<p class="tableblock">#{text}</p>)
789
+ end
790
+ end
791
+ end
792
+
793
+ cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')
794
+ cell_class_attribute = %( class="tableblock halign-#{cell.attr 'halign'} valign-#{cell.attr 'valign'}")
795
+ cell_colspan_attribute = cell.colspan ? %( colspan="#{cell.colspan}") : nil
796
+ cell_rowspan_attribute = cell.rowspan ? %( rowspan="#{cell.rowspan}") : nil
797
+ cell_style_attribute = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'};") : nil
798
+ result << %(<#{cell_tag_name}#{cell_class_attribute}#{cell_colspan_attribute}#{cell_rowspan_attribute}#{cell_style_attribute}>#{cell_content}</#{cell_tag_name}>)
799
+ end
800
+ result << '</tr>'
801
+ end
802
+ result << %(</t#{tsec}>)
803
+ end
804
+ end
805
+ result << '</table>'
806
+ result * EOL
807
+ end
808
+
809
+ def toc node
810
+ return '<!-- toc disabled -->' unless (doc = node.document).attr?('toc-placement', 'macro') && doc.attr?('toc')
811
+
812
+ if node.id
813
+ id_attr = %( id="#{node.id}")
814
+ title_id_attr = %( id="#{node.id}title")
815
+ else
816
+ id_attr = ' id="toc"'
817
+ title_id_attr = ' id="toctitle"'
818
+ end
819
+ title = node.title? ? node.title : (doc.attr 'toc-title')
820
+ levels = (node.attr? 'levels') ? (node.attr 'levels').to_i : nil
821
+ role = node.role? ? node.role : (doc.attr 'toc-class', 'toc')
822
+
823
+ %(<div#{id_attr} class="#{role}">
824
+ <div#{title_id_attr} class="title">#{title}</div>
825
+ #{outline doc, :toclevels => levels}
826
+ </div>)
827
+ end
828
+
829
+ def ulist node
830
+ result = []
831
+ id_attribute = node.id ? %( id="#{node.id}") : nil
832
+ div_classes = ['ulist', node.style, node.role].compact
833
+ marker_checked = nil
834
+ marker_unchecked = nil
835
+ if (checklist = node.option? 'checklist')
836
+ div_classes.insert 1, 'checklist'
837
+ ul_class_attribute = ' class="checklist"'
838
+ if node.option? 'interactive'
839
+ if node.document.attr? 'htmlsyntax', 'xml'
840
+ marker_checked = '<input type="checkbox" data-item-complete="1" checked="checked"/> '
841
+ marker_unchecked = '<input type="checkbox" data-item-complete="0"/> '
842
+ else
843
+ marker_checked = '<input type="checkbox" data-item-complete="1" checked> '
844
+ marker_unchecked = '<input type="checkbox" data-item-complete="0"> '
845
+ end
846
+ else
847
+ if node.document.attr? 'icons', 'font'
848
+ marker_checked = '<i class="fa fa-check-square-o"></i> '
849
+ marker_unchecked = '<i class="fa fa-square-o"></i> '
850
+ else
851
+ marker_checked = '&#10003; '
852
+ marker_unchecked = '&#10063; '
853
+ end
854
+ end
855
+ else
856
+ ul_class_attribute = node.style ? %( class="#{node.style}") : nil
857
+ end
858
+ result << %(<div#{id_attribute} class="#{div_classes * ' '}">)
859
+ result << %(<div class="title">#{node.title}</div>) if node.title?
860
+ result << %(<ul#{ul_class_attribute}>)
861
+
862
+ node.items.each do |item|
863
+ result << '<li>'
864
+ if checklist && (item.attr? 'checkbox')
865
+ result << %(<p>#{(item.attr? 'checked') ? marker_checked : marker_unchecked}#{item.text}</p>)
866
+ else
867
+ result << %(<p>#{item.text}</p>)
868
+ end
869
+ result << item.content if item.blocks?
870
+ result << '</li>'
871
+ end
872
+
873
+ result << '</ul>'
874
+ result << '</div>'
875
+ result * EOL
876
+ end
877
+
878
+ def verse node
879
+ id_attribute = node.id ? %( id="#{node.id}") : nil
880
+ classes = ['verseblock', node.role].compact
881
+ class_attribute = %( class="#{classes * ' '}")
882
+ title_element = node.title? ? %(\n<div class="title">#{node.title}</div>) : nil
883
+ attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil
884
+ citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil
885
+ if attribution || citetitle
886
+ cite_element = citetitle ? %(<cite>#{citetitle}</cite>) : nil
887
+ attribution_text = attribution ? %(&#8212; #{attribution}#{citetitle ? "<br#{@void_element_slash}>\n" : nil}) : nil
888
+ attribution_element = %(\n<div class="attribution">\n#{attribution_text}#{cite_element}\n</div>)
889
+ else
890
+ attribution_element = nil
891
+ end
892
+
893
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
894
+ <pre class="content">#{node.content}</pre>#{attribution_element}
895
+ </div>)
896
+ end
897
+
898
+ def video node
899
+ xml = node.document.attr? 'htmlsyntax', 'xml'
900
+ id_attribute = node.id ? %( id="#{node.id}") : nil
901
+ classes = ['videoblock', node.style, node.role].compact
902
+ class_attribute = %( class="#{classes * ' '}")
903
+ title_element = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : nil
904
+ width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
905
+ height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
906
+ case node.attr 'poster'
907
+ when 'vimeo'
908
+ start_anchor = (node.attr? 'start') ? "#at=#{node.attr 'start'}" : nil
909
+ delimiter = '?'
910
+ autoplay_param = (node.option? 'autoplay') ? "#{delimiter}autoplay=1" : nil
911
+ delimiter = '&amp;' if autoplay_param
912
+ loop_param = (node.option? 'loop') ? "#{delimiter}loop=1" : nil
913
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
914
+ <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>
916
+ </div>
917
+ </div>)
918
+ 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
921
+ autoplay_param = (node.option? 'autoplay') ? '&amp;autoplay=1' : nil
922
+ loop_param = (node.option? 'loop') ? '&amp;loop=1' : nil
923
+ controls_param = (node.option? 'nocontrols') ? '&amp;controls=0' : nil
924
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
925
+ <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>
927
+ </div>
928
+ </div>)
929
+ else
930
+ 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
932
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
933
+ <div class="content">
934
+ <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}>
935
+ Your browser does not support the video tag.
936
+ </video>
937
+ </div>
938
+ </div>)
939
+ end
940
+ end
941
+
942
+ def inline_anchor node
943
+ target = node.target
944
+ case node.type
945
+ when :xref
946
+ refid = (node.attr 'refid') || target
947
+ # NOTE we lookup text in converter because DocBook doesn't need this logic
948
+ text = node.text || (node.document.references[:ids][refid] || %([#{refid}]))
949
+ %(<a href="#{target}">#{text}</a>)
950
+ when :ref
951
+ %(<a id="#{target}"></a>)
952
+ when :link
953
+ attrs = []
954
+ attrs << %( id="#{node.id}") if node.id
955
+ if (role = node.role)
956
+ attrs << %( class="#{role}")
957
+ end
958
+ attrs << %( title="#{node.attr 'title'}") if node.attr? 'title'
959
+ attrs << %( target="#{node.attr 'window'}") if node.attr? 'window'
960
+ %(<a href="#{target}"#{attrs.join}>#{node.text}</a>)
961
+ when :bibref
962
+ %(<a id="#{target}"></a>[#{target}])
963
+ else
964
+ warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect})
965
+ end
966
+ end
967
+
968
+ def inline_break node
969
+ %(#{node.text}<br#{@void_element_slash}>)
970
+ end
971
+
972
+ def inline_button node
973
+ %(<b class="button">#{node.text}</b>)
974
+ end
975
+
976
+ def inline_callout node
977
+ if node.document.attr? 'icons', 'font'
978
+ %(<i class="conum" data-value="#{node.text}"></i><b>(#{node.text})</b>)
979
+ elsif node.document.attr? 'icons'
980
+ src = node.icon_uri("callouts/#{node.text}")
981
+ %(<img src="#{src}" alt="#{node.text}"#{@void_element_slash}>)
982
+ else
983
+ %(<b class="conum">(#{node.text})</b>)
984
+ end
985
+ end
986
+
987
+ def inline_footnote node
988
+ if (index = node.attr 'index')
989
+ if node.type == :xref
990
+ %(<span class="footnoteref">[<a class="footnote" href="#_footnote_#{index}" title="View footnote.">#{index}</a>]</span>)
991
+ else
992
+ id_attr = node.id ? %( id="_footnote_#{node.id}") : nil
993
+ %(<span class="footnote"#{id_attr}>[<a id="_footnoteref_#{index}" class="footnote" href="#_footnote_#{index}" title="View footnote.">#{index}</a>]</span>)
994
+ end
995
+ elsif node.type == :xref
996
+ %(<span class="footnoteref red" title="Unresolved footnote reference.">[#{node.text}]</span>)
997
+ end
998
+ end
999
+
1000
+ def inline_image node
1001
+ if (type = node.type) == 'icon' && (node.document.attr? 'icons', 'font')
1002
+ style_class = %(fa fa-#{node.target})
1003
+ if node.attr? 'size'
1004
+ style_class = %(#{style_class} fa-#{node.attr 'size'})
1005
+ end
1006
+ if node.attr? 'rotate'
1007
+ style_class = %(#{style_class} fa-rotate-#{node.attr 'rotate'})
1008
+ end
1009
+ if node.attr? 'flip'
1010
+ style_class = %(#{style_class} fa-flip-#{node.attr 'flip'})
1011
+ end
1012
+ title_attribute = (node.attr? 'title') ? %( title="#{node.attr 'title'}") : nil
1013
+ img = %(<i class="#{style_class}"#{title_attribute}></i>)
1014
+ elsif type == 'icon' && !(node.document.attr? 'icons')
1015
+ img = %([#{node.attr 'alt'}])
1016
+ else
1017
+ resolved_target = (type == 'icon') ? (node.icon_uri node.target) : (node.image_uri node.target)
1018
+
1019
+ attrs = ['alt', 'width', 'height', 'title'].map {|name|
1020
+ (node.attr? name) ? %( #{name}="#{node.attr name}") : nil
1021
+ }.join
1022
+
1023
+ img = %(<img src="#{resolved_target}"#{attrs}#{@void_element_slash}>)
1024
+ end
1025
+
1026
+ if node.attr? 'link'
1027
+ window_attr = (node.attr? 'window') ? %( target="#{node.attr 'window'}") : nil
1028
+ img = %(<a class="image" href="#{node.attr 'link'}"#{window_attr}>#{img}</a>)
1029
+ end
1030
+
1031
+ style_classes = (role = node.role) ? %(#{type} #{role}) : type
1032
+ style_attr = (node.attr? 'float') ? %( style="float: #{node.attr 'float'}") : nil
1033
+
1034
+ %(<span class="#{style_classes}"#{style_attr}>#{img}</span>)
1035
+ end
1036
+
1037
+ def inline_indexterm node
1038
+ node.type == :visible ? node.text : ''
1039
+ end
1040
+
1041
+ def inline_kbd node
1042
+ if (keys = node.attr 'keys').size == 1
1043
+ %(<kbd>#{keys[0]}</kbd>)
1044
+ else
1045
+ key_combo = keys.map {|key| %(<kbd>#{key}</kbd>+) }.join.chop
1046
+ %(<span class="keyseq">#{key_combo}</span>)
1047
+ end
1048
+ end
1049
+
1050
+ def inline_menu node
1051
+ menu = node.attr 'menu'
1052
+ if !(submenus = node.attr 'submenus').empty?
1053
+ submenu_path = submenus.map {|submenu| %(<span class="submenu">#{submenu}</span>&#160;&#9656; ) }.join.chop
1054
+ %(<span class="menuseq"><span class="menu">#{menu}</span>&#160;&#9656; #{submenu_path} <span class="menuitem">#{node.attr 'menuitem'}</span></span>)
1055
+ elsif (menuitem = node.attr 'menuitem')
1056
+ %(<span class="menuseq"><span class="menu">#{menu}</span>&#160;&#9656; <span class="menuitem">#{menuitem}</span></span>)
1057
+ else
1058
+ %(<span class="menu">#{menu}</span>)
1059
+ end
1060
+ end
1061
+
1062
+ def inline_quoted node
1063
+ open, close, is_tag = QUOTE_TAGS[node.type]
1064
+ if (role = node.role)
1065
+ if is_tag
1066
+ quoted_text = %(#{open.chop} class="#{role}">#{node.text}#{close})
1067
+ else
1068
+ quoted_text = %(<span class="#{role}">#{open}#{node.text}#{close}</span>)
1069
+ end
1070
+ else
1071
+ quoted_text = %(#{open}#{node.text}#{close})
1072
+ end
1073
+
1074
+ node.id ? %(<a id="#{node.id}"></a>#{quoted_text}) : quoted_text
1075
+ end
1076
+
1077
+ def append_boolean_attribute name, xml
1078
+ xml ? %( #{name}="#{name}") : %( #{name})
1079
+ end
1080
+ end
1081
+ end