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,58 @@
1
+ module Asciidoctor
2
+ module Converter; end # required for Opal
3
+
4
+ # An abstract base class for defining converters that can be used to convert
5
+ # {AbstractNode} objects in a parsed AsciiDoc document to a backend format
6
+ # such as HTML or DocBook.
7
+ #
8
+ # Concrete subclasses must implement the {#convert} method and, optionally,
9
+ # the {#convert_with_options} method.
10
+ class Converter::Base
11
+ include Converter
12
+ end
13
+
14
+ # An abstract base class for built-in {Converter} classes.
15
+ class Converter::BuiltIn
16
+ def initialize backend, opts = {}
17
+ end
18
+
19
+ # Public: Converts the specified {AbstractNode} using the specified transform.
20
+ #
21
+ # See {Converter#convert} for more details.
22
+ #
23
+ # Returns the [String] result of conversion
24
+ def convert node, transform = nil
25
+ transform ||= node.node_name
26
+ send transform, node
27
+ end
28
+
29
+ # Public: Converts the specified {AbstractNode} using the specified transform
30
+ # with additional options.
31
+ #
32
+ # See {Converter#convert_with_options} for more details.
33
+ #
34
+ # Returns the [String] result of conversion
35
+ def convert_with_options node, transform = nil, opts = {}
36
+ transform ||= node.node_name
37
+ send transform, node, opts
38
+ end
39
+
40
+ alias :handles? :respond_to?
41
+
42
+ # Public: Returns the converted content of the {AbstractNode}.
43
+ #
44
+ # Returns the converted [String] content of the {AbstractNode}.
45
+ def content node
46
+ node.content
47
+ end
48
+
49
+ alias :pass :content
50
+
51
+ # Public: Skips conversion of the {AbstractNode}.
52
+ #
53
+ # Returns [NilClass]
54
+ def skip node
55
+ nil
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,66 @@
1
+ module Asciidoctor
2
+ # A {Converter} implementation that delegates to the chain of {Converter}
3
+ # objects passed to the constructor. Selects the first {Converter} that
4
+ # identifies itself as the handler for a given transform.
5
+ class Converter::CompositeConverter < Converter::Base
6
+
7
+ # Get the Array of Converter objects in the chain
8
+ attr_reader :converters
9
+
10
+ def initialize backend, *converters
11
+ @backend = backend
12
+ (@converters = converters.flatten.compact).each do |converter|
13
+ converter.composed self if converter.respond_to? :composed
14
+ end
15
+ @converter_map = {}
16
+ end
17
+
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
+ # Public: Delegates to the first converter that identifies itself as the
33
+ # handler for the given transform. The optional Hash is passed as the last
34
+ # option to the delegate's convert method.
35
+ #
36
+ # node - the AbstractNode to convert
37
+ # transform - the optional String transform, or the name of the node if no
38
+ # transform is specified. (default: nil)
39
+ # opts - a optional Hash that is passed to the delegate's convert method. (default: {})
40
+ #
41
+ # Returns the String result returned from the delegate's convert method
42
+ def convert_with_options node, transform = nil, opts = {}
43
+ 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
46
+ end
47
+
48
+ # Public: Retrieve the converter for the specified transform.
49
+ #
50
+ # Returns the matching [Converter] object
51
+ def converter_for transform
52
+ @converter_map[transform] ||= find_converter transform
53
+ end
54
+
55
+ # Internal: Find the converter for the specified transform.
56
+ # Raise an exception if no converter is found.
57
+ #
58
+ # Returns the matching [Converter] object
59
+ def find_converter transform
60
+ @converters.each do |candidate|
61
+ return candidate if candidate.handles? transform
62
+ end
63
+ raise %(Could not find a converter to handle transform: #{transform})
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,94 @@
1
+ require 'asciidoctor/converter/docbook5'
2
+
3
+ module Asciidoctor
4
+ # A built-in {Converter} implementation that generates DocBook 4.5 output
5
+ # consistent with the docbook45 backend from AsciiDoc Python.
6
+ class Converter::DocBook45Converter < Converter::DocBook5Converter
7
+ def admonition node
8
+ # address a bug in the DocBook 4.5 DTD
9
+ if node.parent.context == :example
10
+ %(<para>
11
+ #{super}
12
+ </para>)
13
+ else
14
+ super
15
+ end
16
+ end
17
+
18
+ def olist node
19
+ result = []
20
+ num_attribute = node.style ? %( numeration="#{node.style}") : nil
21
+ start_attribute = (node.attr? 'start') ? %( override="#{node.attr 'start'}") : nil
22
+ result << %(<orderedlist#{common_attributes node.id, node.role, node.reftext}#{num_attribute}>)
23
+ result << %(<title>#{node.title}</title>) if node.title?
24
+ node.items.each_with_index do |item, idx|
25
+ result << (idx == 0 ? %(<listitem#{start_attribute}>) : '<listitem>')
26
+ result << %(<simpara>#{item.text}</simpara>)
27
+ result << item.content if item.blocks?
28
+ result << '</listitem>'
29
+ end
30
+ result << %(</orderedlist>)
31
+ result * EOL
32
+ end
33
+
34
+ def inline_anchor node
35
+ target = node.target
36
+ case node.type
37
+ when :ref
38
+ %(<anchor#{common_attributes target, nil, node.text}/>)
39
+ when :xref
40
+ if node.attr? 'path', nil
41
+ linkend = (node.attr 'fragment') || target
42
+ (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
43
+ else
44
+ text = node.text || (node.attr 'path')
45
+ %(<ulink url="#{target}">#{text}</ulink>)
46
+ end
47
+ when :link
48
+ %(<ulink url="#{target}">#{node.text}</ulink>)
49
+ when :bibref
50
+ %(<anchor#{common_attributes target, nil, "[#{target}]"}/>[#{target}])
51
+ end
52
+ end
53
+
54
+ def author_element doc, index = nil
55
+ firstname_key = index ? %(firstname_#{index}) : 'firstname'
56
+ middlename_key = index ? %(middlename_#{index}) : 'middlename'
57
+ lastname_key = index ? %(lastname_#{index}) : 'lastname'
58
+ email_key = index ? %(email_#{index}) : 'email'
59
+
60
+ result = []
61
+ result << '<author>'
62
+ result << %(<firstname>#{doc.attr firstname_key}</firstname>) if doc.attr? firstname_key
63
+ result << %(<othername>#{doc.attr middlename_key}</othername>) if doc.attr? middlename_key
64
+ result << %(<surname>#{doc.attr lastname_key}</surname>) if doc.attr? lastname_key
65
+ result << %(<email>#{doc.attr email_key}</email>) if doc.attr? email_key
66
+ result << '</author>'
67
+
68
+ result * EOL
69
+ end
70
+
71
+ def common_attributes id, role = nil, reftext = nil
72
+ res = id ? %( id="#{id}") : ''
73
+ res = %(#{res} role="#{role}") if role
74
+ res = %(#{res} xreflabel="#{reftext}") if reftext
75
+ res
76
+ end
77
+
78
+ def doctype_declaration root_tag_name
79
+ %(<!DOCTYPE #{root_tag_name} PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">)
80
+ end
81
+
82
+ def document_info_element doc, info_tag_prefix
83
+ super doc, info_tag_prefix, true
84
+ end
85
+
86
+ def document_ns_attributes doc
87
+ if (ns = doc.attr 'xmlns')
88
+ ns.empty? ? ' xmlns="http://docbook.org/ns/docbook"' : %( xmlns="#{ns}")
89
+ else
90
+ nil
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,684 @@
1
+ module Asciidoctor
2
+ # A built-in {Converter} implementation that generates DocBook 5 output
3
+ # similar to the docbook45 backend from AsciiDoc Python, but migrated to the
4
+ # DocBook 5 specification.
5
+ class Converter::DocBook5Converter < Converter::BuiltIn
6
+ def document node
7
+ result = []
8
+ root_tag_name = node.doctype
9
+ result << '<?xml version="1.0" encoding="UTF-8"?>'
10
+ if (doctype_line = doctype_declaration root_tag_name)
11
+ result << doctype_line
12
+ end
13
+ result << '<?asciidoc-toc?>' if node.attr? 'toc'
14
+ result << '<?asciidoc-numbered?>' if node.attr? 'sectnums'
15
+ lang_attribute = (node.attr? 'nolang') ? nil : %( lang="#{node.attr 'lang', 'en'}")
16
+ result << %(<#{root_tag_name}#{document_ns_attributes node}#{lang_attribute}>)
17
+ result << (document_info_element node, root_tag_name)
18
+ result << node.content if node.blocks?
19
+ unless (footer_docinfo = node.docinfo :footer).empty?
20
+ result << footer_docinfo
21
+ end
22
+ result << %(</#{root_tag_name}>)
23
+
24
+ result * EOL
25
+ end
26
+
27
+ alias :embedded :content
28
+
29
+ def section node
30
+ tag_name = if node.special
31
+ node.level <= 1 ? node.sectname : 'section'
32
+ else
33
+ node.document.doctype == 'book' && node.level <= 1 ? (node.level == 0 ? 'part' : 'chapter') : 'section'
34
+ end
35
+ %(<#{tag_name}#{common_attributes node.id, node.role, node.reftext}>
36
+ <title>#{node.title}</title>
37
+ #{node.content}
38
+ </#{tag_name}>)
39
+ end
40
+
41
+ def admonition node
42
+ %(<#{tag_name = node.attr 'name'}#{common_attributes node.id, node.role, node.reftext}>
43
+ #{title_tag node}#{resolve_content node}
44
+ </#{tag_name}>)
45
+ end
46
+
47
+ alias :audio :skip
48
+
49
+ def colist node
50
+ result = []
51
+ result << %(<calloutlist#{common_attributes node.id, node.role, node.reftext}>)
52
+ result << %(<title>#{node.title}</title>) if node.title?
53
+ node.items.each do |item|
54
+ result << %(<callout arearefs="#{item.attr 'coids'}">)
55
+ result << %(<para>#{item.text}</para>)
56
+ result << item.content if item.blocks?
57
+ result << '</callout>'
58
+ end
59
+ result << %(</calloutlist>)
60
+ result * EOL
61
+ end
62
+
63
+ DLIST_TAGS = {
64
+ 'labeled' => {
65
+ :list => 'variablelist',
66
+ :entry => 'varlistentry',
67
+ :term => 'term',
68
+ :item => 'listitem'
69
+ },
70
+ 'qanda' => {
71
+ :list => 'qandaset',
72
+ :entry => 'qandaentry',
73
+ :label => 'question',
74
+ :term => 'simpara',
75
+ :item => 'answer'
76
+ },
77
+ 'glossary' => {
78
+ :list => nil,
79
+ :entry => 'glossentry',
80
+ :term => 'glossterm',
81
+ :item => 'glossdef'
82
+ }
83
+ }
84
+ DLIST_TAGS.default = DLIST_TAGS['labeled']
85
+
86
+ def dlist node
87
+ result = []
88
+ if node.style == 'horizontal'
89
+ result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext} tabstyle="horizontal" frame="none" colsep="0" rowsep="0">
90
+ #{title_tag node}<tgroup cols="2">
91
+ <colspec colwidth="#{node.attr 'labelwidth', 15}*"/>
92
+ <colspec colwidth="#{node.attr 'itemwidth', 85}*"/>
93
+ <tbody valign="top">)
94
+ node.items.each do |terms, dd|
95
+ result << %(<row>
96
+ <entry>)
97
+ [*terms].each do |dt|
98
+ result << %(<simpara>#{dt.text}</simpara>)
99
+ end
100
+ result << %(</entry>
101
+ <entry>)
102
+ unless dd.nil?
103
+ result << %(<simpara>#{dd.text}</simpara>) if dd.text?
104
+ result << dd.content if dd.blocks?
105
+ end
106
+ result << %(</entry>
107
+ </row>)
108
+ end
109
+ result << %(</tbody>
110
+ </tgroup>
111
+ </#{tag_name}>)
112
+ else
113
+ tags = DLIST_TAGS[node.style]
114
+ list_tag = tags[:list]
115
+ entry_tag = tags[:entry]
116
+ label_tag = tags[:label]
117
+ term_tag = tags[:term]
118
+ item_tag = tags[:item]
119
+ if list_tag
120
+ result << %(<#{list_tag}#{common_attributes node.id, node.role, node.reftext}>)
121
+ result << %(<title>#{node.title}</title>) if node.title?
122
+ end
123
+
124
+ node.items.each do |terms, dd|
125
+ result << %(<#{entry_tag}>)
126
+ result << %(<#{label_tag}>) if label_tag
127
+
128
+ [*terms].each do |dt|
129
+ result << %(<#{term_tag}>#{dt.text}</#{term_tag}>)
130
+ end
131
+
132
+ result << %(</#{label_tag}>) if label_tag
133
+ result << %(<#{item_tag}>)
134
+ unless dd.nil?
135
+ result << %(<simpara>#{dd.text}</simpara>) if dd.text?
136
+ result << dd.content if dd.blocks?
137
+ end
138
+ result << %(</#{item_tag}>)
139
+ result << %(</#{entry_tag}>)
140
+ end
141
+
142
+ result << %(</#{list_tag}>) if list_tag
143
+ end
144
+
145
+ result * EOL
146
+ end
147
+
148
+ def example node
149
+ if node.title?
150
+ %(<example#{common_attributes node.id, node.role, node.reftext}>
151
+ <title>#{node.title}</title>
152
+ #{resolve_content node}
153
+ </example>)
154
+ else
155
+ %(<informalexample#{common_attributes node.id, node.role, node.reftext}>
156
+ #{resolve_content node}
157
+ </informalexample>)
158
+ end
159
+ end
160
+
161
+ def floating_title node
162
+ %(<bridgehead#{common_attributes node.id, node.role, node.reftext} renderas="sect#{node.level}">#{node.title}</bridgehead>)
163
+ end
164
+
165
+ def image node
166
+ width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : nil
167
+ depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : nil
168
+ swidth_attribute = (node.attr? 'scaledwidth') ? %( width="#{node.attr 'scaledwidth'}" scalefit="1") : nil
169
+ scale_attribute = (node.attr? 'scale') ? %( scale="#{node.attr 'scale'}") : nil
170
+ align_attribute = (node.attr? 'align') ? %( align="#{node.attr 'align'}") : nil
171
+
172
+ mediaobject = %(<mediaobject>
173
+ <imageobject>
174
+ <imagedata fileref="#{node.image_uri(node.attr 'target')}"#{width_attribute}#{depth_attribute}#{swidth_attribute}#{scale_attribute}#{align_attribute}/>
175
+ </imageobject>
176
+ <textobject><phrase>#{node.attr 'alt'}</phrase></textobject>
177
+ </mediaobject>)
178
+
179
+ if node.title?
180
+ %(<figure#{common_attributes node.id, node.role, node.reftext}>
181
+ <title>#{node.title}</title>
182
+ #{mediaobject}
183
+ </figure>)
184
+ else
185
+ %(<informalfigure#{common_attributes node.id, node.role, node.reftext}>
186
+ #{mediaobject}
187
+ </informalfigure>)
188
+ end
189
+ end
190
+
191
+ def listing node
192
+ informal = !node.title?
193
+ listing_attributes = (common_attributes node.id, node.role, node.reftext)
194
+ if node.style == 'source' && (node.attr? 'language')
195
+ numbering = (node.attr? 'linenums') ? 'numbered' : 'unnumbered'
196
+ listing_content = %(<programlisting#{informal ? listing_attributes : nil} language="#{node.attr 'language', nil, false}" linenumbering="#{numbering}">#{node.content}</programlisting>)
197
+ else
198
+ listing_content = %(<screen#{informal ? listing_attributes : nil}>#{node.content}</screen>)
199
+ end
200
+ if informal
201
+ listing_content
202
+ else
203
+ %(<formalpara#{listing_attributes}>
204
+ <title>#{node.title}</title>
205
+ <para>
206
+ #{listing_content}
207
+ </para>
208
+ </formalpara>)
209
+ end
210
+ end
211
+
212
+ def literal node
213
+ if node.title?
214
+ %(<formalpara#{common_attributes node.id, node.role, node.reftext}>
215
+ <title>#{node.title}</title>
216
+ <para>
217
+ <literallayout class="monospaced">#{node.content}</literallayout>
218
+ </para>
219
+ </formalpara>)
220
+ else
221
+ %(<literallayout#{common_attributes node.id, node.role, node.reftext} class="monospaced">#{node.content}</literallayout>)
222
+ end
223
+ end
224
+
225
+ def stem node
226
+ if (idx = node.subs.index :specialcharacters)
227
+ node.subs.delete :specialcharacters
228
+ end
229
+ equation = node.content
230
+ node.subs.insert idx, :specialcharacters if idx
231
+ if node.style == 'latexmath'
232
+ equation_data = %(<alt><![CDATA[#{equation}]]></alt>
233
+ <mediaobject><textobject><phrase></phrase></textobject></mediaobject>)
234
+ # asciimath
235
+ else
236
+ # DocBook backends can't handle AsciiMath, so output raw expression in text object
237
+ equation_data = %(<mediaobject><textobject><phrase><![CDATA[#{equation}]]></phrase></textobject></mediaobject>)
238
+ end
239
+ if node.title?
240
+ %(<equation#{common_attributes node.id, node.role, node.reftext}>
241
+ <title>#{node.title}</title>
242
+ #{equation_data}
243
+ </equation>)
244
+ else
245
+ %(<informalequation#{common_attributes node.id, node.role, node.reftext}>
246
+ #{equation_data}
247
+ </informalequation>)
248
+ end
249
+ end
250
+
251
+ def olist node
252
+ result = []
253
+ num_attribute = node.style ? %( numeration="#{node.style}") : nil
254
+ start_attribute = (node.attr? 'start') ? %( startingnumber="#{node.attr 'start'}") : nil
255
+ result << %(<orderedlist#{common_attributes node.id, node.role, node.reftext}#{num_attribute}#{start_attribute}>)
256
+ result << %(<title>#{node.title}</title>) if node.title?
257
+ node.items.each do |item|
258
+ result << '<listitem>'
259
+ result << %(<simpara>#{item.text}</simpara>)
260
+ result << item.content if item.blocks?
261
+ result << '</listitem>'
262
+ end
263
+ result << %(</orderedlist>)
264
+ result * EOL
265
+ end
266
+
267
+ def open node
268
+ case node.style
269
+ when 'abstract'
270
+ if node.parent == node.document && node.document.attr?('doctype', 'book')
271
+ warn 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
272
+ ''
273
+ else
274
+ %(<abstract>
275
+ #{title_tag node}#{resolve_content node}
276
+ </abstract>)
277
+ end
278
+ when 'partintro'
279
+ unless node.level == 0 && node.parent.context == :section && node.document.doctype == 'book'
280
+ warn 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a part section. Excluding block content.'
281
+ ''
282
+ else
283
+ %(<partintro#{common_attributes node.id, node.role, node.reftext}>
284
+ #{title_tag node}#{resolve_content node}
285
+ </partintro>)
286
+ end
287
+ else
288
+ node.content
289
+ end
290
+ end
291
+
292
+ def page_break node
293
+ '<simpara><?asciidoc-pagebreak?></simpara>'
294
+ end
295
+
296
+ def paragraph node
297
+ if node.title?
298
+ %(<formalpara#{common_attributes node.id, node.role, node.reftext}>
299
+ <title>#{node.title}</title>
300
+ <para>#{node.content}</para>
301
+ </formalpara>)
302
+ else
303
+ %(<simpara#{common_attributes node.id, node.role, node.reftext}>#{node.content}</simpara>)
304
+ end
305
+ end
306
+
307
+ def preamble node
308
+ if node.document.doctype == 'book'
309
+ %(<preface#{common_attributes node.id, node.role, node.reftext}>
310
+ #{title_tag node, false}#{node.content}
311
+ </preface>)
312
+ else
313
+ node.content
314
+ end
315
+ end
316
+
317
+ def quote node
318
+ result = []
319
+ result << %(<blockquote#{common_attributes node.id, node.role, node.reftext}>)
320
+ result << %(<title>#{node.title}</title>) if node.title?
321
+ if (node.attr? 'attribution') || (node.attr? 'citetitle')
322
+ result << '<attribution>'
323
+ if node.attr? 'attribution'
324
+ result << (node.attr 'attribution')
325
+ end
326
+ if node.attr? 'citetitle'
327
+ result << %(<citetitle>#{node.attr 'citetitle'}</citetitle>)
328
+ end
329
+ result << '</attribution>'
330
+ end
331
+ result << (resolve_content node)
332
+ result << '</blockquote>'
333
+ result * EOL
334
+ end
335
+
336
+ def thematic_break node
337
+ '<simpara><?asciidoc-hr?></simpara>'
338
+ end
339
+
340
+ def sidebar node
341
+ %(<sidebar#{common_attributes node.id, node.role, node.reftext}>
342
+ #{title_tag node}#{resolve_content node}
343
+ </sidebar>)
344
+ end
345
+
346
+ TABLE_PI_NAMES = ['dbhtml', 'dbfo', 'dblatex']
347
+ TABLE_SECTIONS = [:head, :foot, :body]
348
+
349
+ def table node
350
+ has_body = false
351
+ result = []
352
+ pgwide_attribute = (node.option? 'pgwide') ? ' pgwide="1"' : nil
353
+ result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext}#{pgwide_attribute} frame="#{node.attr 'frame', 'all'}" rowsep="#{['none', 'cols'].include?(node.attr 'grid') ? 0 : 1}" colsep="#{['none', 'rows'].include?(node.attr 'grid') ? 0 : 1}">)
354
+ result << %(<title>#{node.title}</title>) if tag_name == 'table'
355
+ if (width = (node.attr? 'width') ? (node.attr 'width') : nil)
356
+ TABLE_PI_NAMES.each do |pi_name|
357
+ result << %(<?#{pi_name} table-width="#{width}"?>)
358
+ end
359
+ end
360
+ result << %(<tgroup cols="#{node.attr 'colcount'}">)
361
+ node.columns.each do |col|
362
+ result << %(<colspec colname="col_#{col.attr 'colnumber'}" colwidth="#{col.attr(width ? 'colabswidth' : 'colpcwidth')}*"/>)
363
+ end
364
+ TABLE_SECTIONS.select {|tblsec| !node.rows[tblsec].empty? }.each do |tblsec|
365
+ has_body = true if tblsec == :body
366
+ result << %(<t#{tblsec}>)
367
+ node.rows[tblsec].each do |row|
368
+ result << '<row>'
369
+ row.each do |cell|
370
+ halign_attribute = (cell.attr? 'halign') ? %( align="#{cell.attr 'halign'}") : nil
371
+ valign_attribute = (cell.attr? 'valign') ? %( valign="#{cell.attr 'valign'}") : nil
372
+ colspan_attribute = cell.colspan ? %( namest="col_#{colnum = cell.column.attr 'colnumber'}" nameend="col_#{colnum + cell.colspan - 1}") : nil
373
+ rowspan_attribute = cell.rowspan ? %( morerows="#{cell.rowspan - 1}") : nil
374
+ # NOTE <entry> may not have whitespace (e.g., line breaks) as a direct descendant according to DocBook rules
375
+ entry_start = %(<entry#{halign_attribute}#{valign_attribute}#{colspan_attribute}#{rowspan_attribute}>)
376
+ cell_content = if tblsec == :head
377
+ cell.text
378
+ else
379
+ case cell.style
380
+ when :asciidoc
381
+ cell.content
382
+ when :verse
383
+ %(<literallayout>#{cell.text}</literallayout>)
384
+ when :literal
385
+ %(<literallayout class="monospaced">#{cell.text}</literallayout>)
386
+ when :header
387
+ cell.content.map {|text| %(<simpara><emphasis role="strong">#{text}</emphasis></simpara>) }.join
388
+ else
389
+ cell.content.map {|text| %(<simpara>#{text}</simpara>) }.join
390
+ end
391
+ end
392
+ entry_end = (node.document.attr? 'cellbgcolor') ? %(<?dbfo bgcolor="#{node.document.attr 'cellbgcolor'}"?></entry>) : '</entry>'
393
+ result << %(#{entry_start}#{cell_content}#{entry_end})
394
+ end
395
+ result << '</row>'
396
+ end
397
+ result << %(</t#{tblsec}>)
398
+ end
399
+ result << '</tgroup>'
400
+ result << %(</#{tag_name}>)
401
+
402
+ warn 'asciidoctor: WARNING: tables must have at least one body row' unless has_body
403
+ result * EOL
404
+ end
405
+
406
+ alias :toc :skip
407
+
408
+ def ulist node
409
+ result = []
410
+ if node.style == 'bibliography'
411
+ result << %(<bibliodiv#{common_attributes node.id, node.role, node.reftext}>)
412
+ result << %(<title>#{node.title}</title>) if node.title?
413
+ node.items.each do |item|
414
+ result << '<bibliomixed>'
415
+ result << %(<bibliomisc>#{item.text}</bibliomisc>)
416
+ result << item.content if item.blocks?
417
+ result << '</bibliomixed>'
418
+ end
419
+ result << '</bibliodiv>'
420
+ else
421
+ mark_type = (checklist = node.option? 'checklist') ? 'none' : node.style
422
+ mark_attribute = mark_type ? %( mark="#{mark_type}") : nil
423
+ result << %(<itemizedlist#{common_attributes node.id, node.role, node.reftext}#{mark_attribute}>)
424
+ result << %(<title>#{node.title}</title>) if node.title?
425
+ node.items.each do |item|
426
+ text_marker = if checklist && (item.attr? 'checkbox')
427
+ (item.attr? 'checked') ? '&#10003; ' : '&#10063; '
428
+ else
429
+ nil
430
+ end
431
+ result << '<listitem>'
432
+ result << %(<simpara>#{text_marker}#{item.text}</simpara>)
433
+ result << item.content if item.blocks?
434
+ result << '</listitem>'
435
+ end
436
+ result << '</itemizedlist>'
437
+ end
438
+
439
+ result * EOL
440
+ end
441
+
442
+ def verse node
443
+ result = []
444
+ result << %(<blockquote#{common_attributes node.id, node.role, node.reftext}>)
445
+ result << %(<title>#{node.title}</title>) if node.title?
446
+ if (node.attr? 'attribution') || (node.attr? 'citetitle')
447
+ result << '<attribution>'
448
+ if node.attr? 'attribution'
449
+ result << (node.attr 'attribution')
450
+ end
451
+ if node.attr? 'citetitle'
452
+ result << %(<citetitle>#{node.attr 'citetitle'}</citetitle>)
453
+ end
454
+ result << '</attribution>'
455
+ end
456
+ result << %(<literallayout>#{node.content}</literallayout>)
457
+ result << '</blockquote>'
458
+ result * EOL
459
+ end
460
+
461
+ alias :video :skip
462
+
463
+ def inline_anchor node
464
+ case node.type
465
+ when :ref
466
+ %(<anchor#{common_attributes node.target, nil, node.text}/>)
467
+ when :xref
468
+ if node.attr? 'path', nil
469
+ linkend = (node.attr 'fragment') || node.target
470
+ (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
471
+ else
472
+ %(<link xlink:href="#{target}">#{node.text || (node.attr 'path')}</link>)
473
+ end
474
+ when :link
475
+ %(<link xlink:href="#{node.target}">#{node.text}</link>)
476
+ when :bibref
477
+ %(<anchor#{common_attributes node.target, nil, "[#{node.target}]"}/>[#{node.target}])
478
+ else
479
+ warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect})
480
+ end
481
+ end
482
+
483
+ def inline_break node
484
+ %(#{node.text}<?asciidoc-br?>)
485
+ end
486
+
487
+ def inline_button node
488
+ %(<guibutton>#{node.text}</guibutton>)
489
+ end
490
+
491
+ def inline_callout node
492
+ %(<co#{common_attributes node.id}/>)
493
+ end
494
+
495
+ def inline_footnote node
496
+ if node.type == :xref
497
+ %(<footnoteref linkend="#{node.target}"/>)
498
+ else
499
+ %(<footnote#{common_attributes node.id}><simpara>#{node.text}</simpara></footnote>)
500
+ end
501
+ end
502
+
503
+ def inline_image node
504
+ width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : nil
505
+ depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : nil
506
+ %(<inlinemediaobject>
507
+ <imageobject>
508
+ <imagedata fileref="#{node.type == 'icon' ? (node.icon_uri node.target) : (node.image_uri node.target)}"#{width_attribute}#{depth_attribute}/>
509
+ </imageobject>
510
+ <textobject><phrase>#{node.attr 'alt'}</phrase></textobject>
511
+ </inlinemediaobject>)
512
+ end
513
+
514
+ def inline_indexterm node
515
+ if node.type == :visible
516
+ %(<indexterm><primary>#{node.text}</primary></indexterm>#{node.text})
517
+ else
518
+ terms = node.attr 'terms'
519
+ result = []
520
+ if (numterms = terms.size) > 2
521
+ result << %(<indexterm>
522
+ <primary>#{terms[0]}</primary><secondary>#{terms[1]}</secondary><tertiary>#{terms[2]}</tertiary>
523
+ </indexterm>)
524
+ end
525
+ if numterms > 1
526
+ result << %(<indexterm>
527
+ <primary>#{terms[-2]}</primary><secondary>#{terms[-1]}</secondary>
528
+ </indexterm>)
529
+ end
530
+ result << %(<indexterm>
531
+ <primary>#{terms[-1]}</primary>
532
+ </indexterm>)
533
+ result * EOL
534
+ end
535
+ end
536
+
537
+ def inline_kbd node
538
+ if (keys = node.attr 'keys').size == 1
539
+ %(<keycap>#{keys[0]}</keycap>)
540
+ else
541
+ key_combo = keys.map {|key| %(<keycap>#{key}</keycap>) }.join
542
+ %(<keycombo>#{key_combo}</keycombo>)
543
+ end
544
+ end
545
+
546
+ def inline_menu node
547
+ menu = node.attr 'menu'
548
+ if !(submenus = node.attr 'submenus').empty?
549
+ submenu_path = submenus.map {|submenu| %(<guisubmenu>#{submenu}</guisubmenu> ) }.join.chop
550
+ %(<menuchoice><guimenu>#{menu}</guimenu> #{submenu_path} <guimenuitem>#{node.attr 'menuitem'}</guimenuitem></menuchoice>)
551
+ elsif (menuitem = node.attr 'menuitem')
552
+ %(<menuchoice><guimenu>#{menu}</guimenu> <guimenuitem>#{menuitem}</guimenuitem></menuchoice>)
553
+ else
554
+ %(<guimenu>#{menu}</guimenu>)
555
+ end
556
+ end
557
+
558
+ QUOTE_TAGS = {
559
+ :emphasis => ['<emphasis>', '</emphasis>', true],
560
+ :strong => ['<emphasis role="strong">', '</emphasis>', true],
561
+ :monospaced => ['<literal>', '</literal>', false],
562
+ :superscript => ['<superscript>', '</superscript>', false],
563
+ :subscript => ['<subscript>', '</subscript>', false],
564
+ :double => ['&#8220;', '&#8221;', true],
565
+ :single => ['&#8216;', '&#8217;', true],
566
+ :mark => ['<emphasis role="marked">', '</emphasis>', false]
567
+ }
568
+ QUOTE_TAGS.default = [nil, nil, true]
569
+
570
+ def inline_quoted node
571
+ if (type = node.type) == :latexmath
572
+ %(<inlineequation>
573
+ <alt><![CDATA[#{node.text}]]></alt>
574
+ <inlinemediaobject><textobject><phrase><![CDATA[#{node.text}]]></phrase></textobject></inlinemediaobject>
575
+ </inlineequation>)
576
+ else
577
+ open, close, supports_phrase = QUOTE_TAGS[type]
578
+ text = node.text
579
+ if (role = node.role)
580
+ if supports_phrase
581
+ quoted_text = %(#{open}<phrase role="#{role}">#{text}</phrase>#{close})
582
+ else
583
+ quoted_text = %(#{open.chop} role="#{role}">#{text}#{close})
584
+ end
585
+ else
586
+ quoted_text = %(#{open}#{text}#{close})
587
+ end
588
+
589
+ node.id ? %(<anchor#{common_attributes node.id, nil, text}/>#{quoted_text}) : quoted_text
590
+ end
591
+ end
592
+
593
+ def author_element doc, index = nil
594
+ firstname_key = index ? %(firstname_#{index}) : 'firstname'
595
+ middlename_key = index ? %(middlename_#{index}) : 'middlename'
596
+ lastname_key = index ? %(lastname_#{index}) : 'lastname'
597
+ email_key = index ? %(email_#{index}) : 'email'
598
+
599
+ result = []
600
+ result << '<author>'
601
+ result << '<personname>'
602
+ result << %(<firstname>#{doc.attr firstname_key}</firstname>) if doc.attr? firstname_key
603
+ result << %(<othername>#{doc.attr middlename_key}</othername>) if doc.attr? middlename_key
604
+ result << %(<surname>#{doc.attr lastname_key}</surname>) if doc.attr? lastname_key
605
+ result << '</personname>'
606
+ result << %(<email>#{doc.attr email_key}</email>) if doc.attr? email_key
607
+ result << '</author>'
608
+
609
+ result * EOL
610
+ end
611
+
612
+ def common_attributes id, role = nil, reftext = nil
613
+ res = id ? %( xml:id="#{id}") : ''
614
+ res = %(#{res} role="#{role}") if role
615
+ res = %(#{res} xreflabel="#{reftext}") if reftext
616
+ res
617
+ end
618
+
619
+ def doctype_declaration root_tag_name
620
+ nil
621
+ end
622
+
623
+ def document_info_element doc, info_tag_prefix, use_info_tag_prefix = false
624
+ info_tag_prefix = '' unless use_info_tag_prefix
625
+ result = []
626
+ result << %(<#{info_tag_prefix}info>)
627
+ result << document_title_tags(doc.doctitle :partition => true, :use_fallback => true) unless doc.notitle
628
+ result << %(<date>#{(doc.attr? 'revdate') ? (doc.attr 'revdate') : (doc.attr 'docdate')}</date>)
629
+ if doc.has_header?
630
+ if doc.attr? 'author'
631
+ if (authorcount = (doc.attr 'authorcount').to_i) < 2
632
+ result << (author_element doc)
633
+ result << %(<authorinitials>#{doc.attr 'authorinitials'}</authorinitials>) if doc.attr? 'authorinitials'
634
+ else
635
+ result << '<authorgroup>'
636
+ authorcount.times do |index|
637
+ result << (author_element doc, index + 1)
638
+ end
639
+ result << '</authorgroup>'
640
+ end
641
+ end
642
+ if (doc.attr? 'revdate') && ((doc.attr? 'revnumber') || (doc.attr? 'revremark'))
643
+ result << %(<revhistory>
644
+ <revision>)
645
+ result << %(<revnumber>#{doc.attr 'revnumber'}</revnumber>) if doc.attr? 'revnumber'
646
+ result << %(<date>#{doc.attr 'revdate'}</date>) if doc.attr? 'revdate'
647
+ result << %(<authorinitials>#{doc.attr 'authorinitials'}</authorinitials>) if doc.attr? 'authorinitials'
648
+ result << %(<revremark>#{doc.attr 'revremark'}</revremark>) if doc.attr? 'revremark'
649
+ result << %(</revision>
650
+ </revhistory>)
651
+ end
652
+ unless (header_docinfo = doc.docinfo :header).empty?
653
+ result << header_docinfo
654
+ end
655
+ result << %(<orgname>#{doc.attr 'orgname'}</orgname>) if doc.attr? 'orgname'
656
+ end
657
+ result << %(</#{info_tag_prefix}info>)
658
+
659
+ result * EOL
660
+ end
661
+
662
+ def document_ns_attributes doc
663
+ ' xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0"'
664
+ end
665
+
666
+ def document_title_tags title
667
+ if title.subtitle?
668
+ %(<title>#{title.main}</title>
669
+ <subtitle>#{title.subtitle}</subtitle>)
670
+ else
671
+ %(<title>#{title}</title>)
672
+ end
673
+ end
674
+
675
+ # FIXME this should be handled through a template mechanism
676
+ def resolve_content node
677
+ node.content_model == :compound ? node.content : %(<simpara>#{node.content}</simpara>)
678
+ end
679
+
680
+ def title_tag node, optional = true
681
+ !optional || node.title? ? %(<title>#{node.title}</title>\n) : nil
682
+ end
683
+ end
684
+ end