asciidoctor 2.0.10 → 2.0.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +211 -22
- data/LICENSE +1 -1
- data/README-de.adoc +12 -13
- data/README-fr.adoc +11 -15
- data/README-jp.adoc +9 -17
- data/README-zh_CN.adoc +17 -18
- data/README.adoc +140 -132
- data/asciidoctor.gemspec +6 -6
- data/data/locale/attributes-ar.adoc +4 -3
- data/data/locale/attributes-be.adoc +23 -0
- data/data/locale/attributes-bg.adoc +4 -3
- data/data/locale/attributes-ca.adoc +6 -5
- data/data/locale/attributes-cs.adoc +4 -3
- data/data/locale/attributes-da.adoc +6 -5
- data/data/locale/attributes-de.adoc +4 -4
- data/data/locale/attributes-en.adoc +4 -4
- data/data/locale/attributes-es.adoc +6 -5
- data/data/locale/attributes-fa.adoc +4 -3
- data/data/locale/attributes-fi.adoc +4 -3
- data/data/locale/attributes-fr.adoc +6 -5
- data/data/locale/attributes-hu.adoc +4 -3
- data/data/locale/attributes-id.adoc +4 -3
- data/data/locale/attributes-it.adoc +6 -5
- data/data/locale/attributes-ja.adoc +4 -3
- data/data/locale/{attributes-kr.adoc → attributes-ko.adoc} +4 -3
- data/data/locale/attributes-nb.adoc +4 -3
- data/data/locale/attributes-nl.adoc +6 -5
- data/data/locale/attributes-nn.adoc +4 -3
- data/data/locale/attributes-pl.adoc +8 -7
- data/data/locale/attributes-pt.adoc +6 -5
- data/data/locale/attributes-pt_BR.adoc +6 -5
- data/data/locale/attributes-ro.adoc +4 -3
- data/data/locale/attributes-ru.adoc +6 -5
- data/data/locale/attributes-sr.adoc +4 -4
- data/data/locale/attributes-sr_Latn.adoc +4 -4
- data/data/locale/attributes-sv.adoc +4 -4
- data/data/locale/attributes-tr.adoc +4 -3
- data/data/locale/attributes-uk.adoc +6 -5
- data/data/locale/attributes-zh_CN.adoc +4 -3
- data/data/locale/attributes-zh_TW.adoc +4 -3
- data/data/reference/syntax.adoc +14 -7
- data/data/stylesheets/asciidoctor-default.css +27 -28
- data/lib/asciidoctor.rb +38 -12
- data/lib/asciidoctor/abstract_block.rb +9 -4
- data/lib/asciidoctor/abstract_node.rb +16 -6
- data/lib/asciidoctor/attribute_list.rb +64 -72
- data/lib/asciidoctor/cli/invoker.rb +2 -0
- data/lib/asciidoctor/cli/options.rb +10 -9
- data/lib/asciidoctor/convert.rb +167 -162
- data/lib/asciidoctor/converter.rb +13 -12
- data/lib/asciidoctor/converter/docbook5.rb +29 -12
- data/lib/asciidoctor/converter/html5.rb +69 -46
- data/lib/asciidoctor/converter/manpage.rb +68 -49
- data/lib/asciidoctor/converter/template.rb +3 -0
- data/lib/asciidoctor/document.rb +43 -50
- data/lib/asciidoctor/extensions.rb +2 -4
- data/lib/asciidoctor/helpers.rb +20 -15
- data/lib/asciidoctor/load.rb +102 -101
- data/lib/asciidoctor/parser.rb +40 -32
- data/lib/asciidoctor/path_resolver.rb +14 -12
- data/lib/asciidoctor/reader.rb +22 -13
- data/lib/asciidoctor/rx.rb +7 -6
- data/lib/asciidoctor/substitutors.rb +78 -57
- data/lib/asciidoctor/syntax_highlighter.rb +8 -5
- data/lib/asciidoctor/syntax_highlighter/coderay.rb +1 -1
- data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +12 -4
- data/lib/asciidoctor/syntax_highlighter/prettify.rb +7 -4
- data/lib/asciidoctor/syntax_highlighter/pygments.rb +6 -7
- data/lib/asciidoctor/syntax_highlighter/rouge.rb +33 -19
- data/lib/asciidoctor/table.rb +52 -23
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +8 -8
- data/man/asciidoctor.adoc +4 -4
- metadata +16 -15
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Asciidoctor
|
3
3
|
# A built-in {Converter} implementation that generates HTML 5 output
|
4
|
-
# consistent with the html5 backend from AsciiDoc
|
4
|
+
# consistent with the html5 backend from AsciiDoc.py.
|
5
5
|
class Converter::Html5Converter < Converter::Base
|
6
6
|
register_for 'html5'
|
7
7
|
|
@@ -21,15 +21,15 @@ class Converter::Html5Converter < Converter::Base
|
|
21
21
|
#latexmath: INLINE_MATH_DELIMITERS[:latexmath] + [false],
|
22
22
|
}).default = ['', '']
|
23
23
|
|
24
|
-
DropAnchorRx = /<(?:a[
|
24
|
+
DropAnchorRx = /<(?:a\b[^>]*|\/a)>/
|
25
25
|
StemBreakRx = / *\\\n(?:\\?\n)*|\n\n+/
|
26
26
|
if RUBY_ENGINE == 'opal'
|
27
27
|
# NOTE In JavaScript, ^ matches the start of the string when the m flag is not set
|
28
|
-
SvgPreambleRx = /^#{CC_ALL}*?(?=<svg\
|
29
|
-
SvgStartTagRx = /^<svg[^>]
|
28
|
+
SvgPreambleRx = /^#{CC_ALL}*?(?=<svg[\s>])/
|
29
|
+
SvgStartTagRx = /^<svg(?:\s[^>]*)?>/
|
30
30
|
else
|
31
|
-
SvgPreambleRx = /\A.*?(?=<svg\
|
32
|
-
SvgStartTagRx = /\A<svg[^>]
|
31
|
+
SvgPreambleRx = /\A.*?(?=<svg[\s>])/m
|
32
|
+
SvgStartTagRx = /\A<svg(?:\s[^>]*)?>/
|
33
33
|
end
|
34
34
|
DimensionAttributeRx = /\s(?:width|height|style)=(["'])#{CC_ANY}*?\1/
|
35
35
|
|
@@ -96,6 +96,7 @@ class Converter::Html5Converter < Converter::Base
|
|
96
96
|
end
|
97
97
|
cdn_base_url = %(#{asset_uri_scheme}//cdnjs.cloudflare.com/ajax/libs)
|
98
98
|
linkcss = node.attr? 'linkcss'
|
99
|
+
max_width_attr = (node.attr? 'max-width') ? %( style="max-width: #{node.attr 'max-width'};") : ''
|
99
100
|
result = ['<!DOCTYPE html>']
|
100
101
|
lang_attribute = (node.attr? 'nolang') ? '' : %( lang="#{node.attr 'lang', 'en'}")
|
101
102
|
result << %(<html#{@xml_mode ? ' xmlns="http://www.w3.org/1999/xhtml"' : ''}#{lang_attribute}>)
|
@@ -138,7 +139,7 @@ class Converter::Html5Converter < Converter::Base
|
|
138
139
|
result << %(<link rel="stylesheet" href="#{node.normalize_web_path((node.attr 'stylesheet'), (node.attr 'stylesdir', ''))}"#{slash}>)
|
139
140
|
else
|
140
141
|
result << %(<style>
|
141
|
-
#{node.
|
142
|
+
#{node.read_contents (node.attr 'stylesheet'), start: (node.attr 'stylesdir'), warn_on_failure: true, label: 'stylesheet'}
|
142
143
|
</style>)
|
143
144
|
end
|
144
145
|
end
|
@@ -152,8 +153,8 @@ class Converter::Html5Converter < Converter::Base
|
|
152
153
|
end
|
153
154
|
end
|
154
155
|
|
155
|
-
if (syntax_hl = node.syntax_highlighter)
|
156
|
-
result << (
|
156
|
+
if (syntax_hl = node.syntax_highlighter)
|
157
|
+
result << (syntax_hl_docinfo_head_idx = result.size)
|
157
158
|
end
|
158
159
|
|
159
160
|
unless (docinfo_content = node.docinfo).empty?
|
@@ -161,29 +162,27 @@ class Converter::Html5Converter < Converter::Base
|
|
161
162
|
end
|
162
163
|
|
163
164
|
result << '</head>'
|
164
|
-
|
165
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
165
166
|
if (sectioned = node.sections?) && (node.attr? 'toc-class') && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
|
166
167
|
classes = [node.doctype, (node.attr 'toc-class'), %(toc-#{node.attr 'toc-position', 'header'})]
|
167
168
|
else
|
168
169
|
classes = [node.doctype]
|
169
170
|
end
|
170
171
|
classes << node.role if node.role?
|
171
|
-
|
172
|
-
body_attrs << %(style="max-width: #{node.attr 'max-width'};") if node.attr? 'max-width'
|
173
|
-
result << %(<body #{body_attrs.join ' '}>)
|
172
|
+
result << %(<body#{id_attr} class="#{classes.join ' '}">)
|
174
173
|
|
175
174
|
unless (docinfo_content = node.docinfo :header).empty?
|
176
175
|
result << docinfo_content
|
177
176
|
end
|
178
177
|
|
179
178
|
unless node.noheader
|
180
|
-
result <<
|
179
|
+
result << %(<div id="header"#{max_width_attr}>)
|
181
180
|
if node.doctype == 'manpage'
|
182
181
|
result << %(<h1>#{node.doctitle} Manual Page</h1>)
|
183
182
|
if sectioned && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
|
184
183
|
result << %(<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
|
185
184
|
<div id="toctitle">#{node.attr 'toc-title'}</div>
|
186
|
-
#{
|
185
|
+
#{node.converter.convert node, 'outline'}
|
187
186
|
</div>)
|
188
187
|
end
|
189
188
|
result << (generate_manname_section node) if node.attr? 'manpurpose'
|
@@ -216,19 +215,19 @@ class Converter::Html5Converter < Converter::Base
|
|
216
215
|
if sectioned && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
|
217
216
|
result << %(<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
|
218
217
|
<div id="toctitle">#{node.attr 'toc-title'}</div>
|
219
|
-
#{
|
218
|
+
#{node.converter.convert node, 'outline'}
|
220
219
|
</div>)
|
221
220
|
end
|
222
221
|
end
|
223
222
|
result << '</div>'
|
224
223
|
end
|
225
224
|
|
226
|
-
result << %(<div id="content">
|
225
|
+
result << %(<div id="content"#{max_width_attr}>
|
227
226
|
#{node.content}
|
228
227
|
</div>)
|
229
228
|
|
230
229
|
if node.footnotes? && !(node.attr? 'nofootnotes')
|
231
|
-
result << %(<div id="footnotes">
|
230
|
+
result << %(<div id="footnotes"#{max_width_attr}>
|
232
231
|
<hr#{slash}>)
|
233
232
|
node.footnotes.each do |footnote|
|
234
233
|
result << %(<div class="footnote" id="_footnotedef_#{footnote.index}">
|
@@ -239,7 +238,7 @@ class Converter::Html5Converter < Converter::Base
|
|
239
238
|
end
|
240
239
|
|
241
240
|
unless node.nofooter
|
242
|
-
result <<
|
241
|
+
result << %(<div id="footer"#{max_width_attr}>)
|
243
242
|
result << '<div id="footer-text">'
|
244
243
|
result << %(#{node.attr 'version-label'} #{node.attr 'revnumber'}#{br}) if node.attr? 'revnumber'
|
245
244
|
result << %(#{node.attr 'last-update-label'} #{node.attr 'docdatetime'}) if (node.attr? 'last-update-label') && !(node.attr? 'reproducible')
|
@@ -250,8 +249,15 @@ class Converter::Html5Converter < Converter::Base
|
|
250
249
|
# JavaScript (and auxiliary stylesheets) loaded at the end of body for performance reasons
|
251
250
|
# See http://www.html5rocks.com/en/tutorials/speed/script-loading/
|
252
251
|
|
253
|
-
if syntax_hl
|
254
|
-
|
252
|
+
if syntax_hl
|
253
|
+
if syntax_hl.docinfo? :head
|
254
|
+
result[syntax_hl_docinfo_head_idx] = syntax_hl.docinfo :head, node, cdn_base_url: cdn_base_url, linkcss: linkcss, self_closing_tag_slash: slash
|
255
|
+
else
|
256
|
+
result.delete_at syntax_hl_docinfo_head_idx
|
257
|
+
end
|
258
|
+
if syntax_hl.docinfo? :footer
|
259
|
+
result << (syntax_hl.docinfo :footer, node, cdn_base_url: cdn_base_url, linkcss: linkcss, self_closing_tag_slash: slash)
|
260
|
+
end
|
255
261
|
end
|
256
262
|
|
257
263
|
if node.attr? 'stem'
|
@@ -275,7 +281,7 @@ MathJax.Hub.Config({
|
|
275
281
|
})
|
276
282
|
MathJax.Hub.Register.StartupHook("AsciiMath Jax Ready", function () {
|
277
283
|
MathJax.InputJax.AsciiMath.postfilterHooks.Add(function (data, node) {
|
278
|
-
if ((node = data.script.parentNode) && (node = node.parentNode) && node.classList.contains(
|
284
|
+
if ((node = data.script.parentNode) && (node = node.parentNode) && node.classList.contains("stemblock")) {
|
279
285
|
data.math.root.display = "block"
|
280
286
|
}
|
281
287
|
return data
|
@@ -311,7 +317,7 @@ MathJax.Hub.Register.StartupHook("AsciiMath Jax Ready", function () {
|
|
311
317
|
if node.sections? && (node.attr? 'toc') && (toc_p = node.attr 'toc-placement') != 'macro' && toc_p != 'preamble'
|
312
318
|
result << %(<div id="toc" class="toc">
|
313
319
|
<div id="toctitle">#{node.attr 'toc-title'}</div>
|
314
|
-
#{
|
320
|
+
#{node.converter.convert node, 'outline'}
|
315
321
|
</div>)
|
316
322
|
end
|
317
323
|
|
@@ -628,9 +634,7 @@ Your browser does not support the audio tag.
|
|
628
634
|
end
|
629
635
|
end
|
630
636
|
img ||= %(<img src="#{node.image_uri target}" alt="#{encode_attribute_value node.alt}"#{width_attr}#{height_attr}#{@void_element_slash}>)
|
631
|
-
if node.attr? 'link'
|
632
|
-
img = %(<a class="image" href="#{node.attr 'link'}"#{(append_link_constraint_attrs node).join}>#{img}</a>)
|
633
|
-
end
|
637
|
+
img = %(<a class="image" href="#{node.attr 'link'}"#{(append_link_constraint_attrs node).join}>#{img}</a>) if node.attr? 'link'
|
634
638
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
635
639
|
classes = ['imageblock']
|
636
640
|
classes << (node.attr 'float') if node.attr? 'float'
|
@@ -689,8 +693,8 @@ Your browser does not support the audio tag.
|
|
689
693
|
open, close = BLOCK_MATH_DELIMITERS[style = node.style.to_sym]
|
690
694
|
if (equation = node.content)
|
691
695
|
if style == :asciimath && (equation.include? LF)
|
692
|
-
br = %(<br#{@void_element_slash}
|
693
|
-
equation = equation.gsub(StemBreakRx) { %(#{close}#{br * ($&.count LF)}#{open}) }
|
696
|
+
br = %(#{LF}<br#{@void_element_slash}>)
|
697
|
+
equation = equation.gsub(StemBreakRx) { %(#{close}#{br * (($&.count LF) - 1)}#{LF}#{open}) }
|
694
698
|
end
|
695
699
|
unless (equation.start_with? open) && (equation.end_with? close)
|
696
700
|
equation = %(#{open}#{equation}#{close})
|
@@ -796,7 +800,7 @@ Your browser does not support the audio tag.
|
|
796
800
|
toc = %(
|
797
801
|
<div id="toc" class="#{doc.attr 'toc-class', 'toc'}">
|
798
802
|
<div id="toctitle">#{doc.attr 'toc-title'}</div>
|
799
|
-
#{
|
803
|
+
#{doc.converter.convert doc, 'outline'}
|
800
804
|
</div>)
|
801
805
|
else
|
802
806
|
toc = ''
|
@@ -848,7 +852,8 @@ Your browser does not support the audio tag.
|
|
848
852
|
def convert_table node
|
849
853
|
result = []
|
850
854
|
id_attribute = node.id ? %( id="#{node.id}") : ''
|
851
|
-
|
855
|
+
frame = 'ends' if (frame = node.attr 'frame', 'all', 'table-frame') == 'topbot'
|
856
|
+
classes = ['tableblock', %(frame-#{frame}), %(grid-#{node.attr 'grid', 'all', 'table-grid'})]
|
852
857
|
if (stripes = node.attr 'stripes', nil, 'table-stripes')
|
853
858
|
classes << %(stripes-#{stripes})
|
854
859
|
end
|
@@ -934,7 +939,7 @@ Your browser does not support the audio tag.
|
|
934
939
|
|
935
940
|
%(<div#{id_attr} class="#{role}">
|
936
941
|
<div#{title_id_attr} class="title">#{title}</div>
|
937
|
-
#{
|
942
|
+
#{doc.converter.convert doc, 'outline', toclevels: levels}
|
938
943
|
</div>)
|
939
944
|
end
|
940
945
|
|
@@ -1089,7 +1094,7 @@ Your browser does not support the audio tag.
|
|
1089
1094
|
time_anchor = (start_t || end_t) ? %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''}) : ''
|
1090
1095
|
%(<div#{id_attribute}#{class_attribute}>#{title_element}
|
1091
1096
|
<div class="content">
|
1092
|
-
<video src="#{node.media_uri(node.attr 'target')}#{time_anchor}"#{width_attribute}#{height_attribute}#{poster_attribute}#{(node.option? 'autoplay') ? (append_boolean_attribute 'autoplay', xml) : ''}#{(node.option? 'nocontrols') ? '' : (append_boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (append_boolean_attribute 'loop', xml) : ''}#{preload_attribute}>
|
1097
|
+
<video src="#{node.media_uri(node.attr 'target')}#{time_anchor}"#{width_attribute}#{height_attribute}#{poster_attribute}#{(node.option? 'autoplay') ? (append_boolean_attribute 'autoplay', xml) : ''}#{(node.option? 'muted') ? (append_boolean_attribute 'muted', xml) : ''}#{(node.option? 'nocontrols') ? '' : (append_boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (append_boolean_attribute 'loop', xml) : ''}#{preload_attribute}>
|
1093
1098
|
Your browser does not support the video tag.
|
1094
1099
|
</video>
|
1095
1100
|
</div>
|
@@ -1106,9 +1111,17 @@ Your browser does not support the video tag.
|
|
1106
1111
|
else
|
1107
1112
|
attrs = node.role ? %( class="#{node.role}") : ''
|
1108
1113
|
unless (text = node.text)
|
1109
|
-
refid = node.attributes['refid']
|
1110
|
-
|
1111
|
-
|
1114
|
+
if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid = node.attributes['refid']] || (refid.nil_or_empty? ? (top = get_root_document node) : nil))
|
1115
|
+
if (@resolving_xref ||= (outer = true)) && outer
|
1116
|
+
if (text = ref.xreftext node.attr 'xrefstyle', nil, true)
|
1117
|
+
text = text.gsub DropAnchorRx, '' if text.include? '<a'
|
1118
|
+
else
|
1119
|
+
text = top ? '[^top]' : %([#{refid}])
|
1120
|
+
end
|
1121
|
+
@resolving_xref = nil
|
1122
|
+
else
|
1123
|
+
text = top ? '[^top]' : %([#{refid}])
|
1124
|
+
end
|
1112
1125
|
else
|
1113
1126
|
text = %([#{refid}])
|
1114
1127
|
end
|
@@ -1144,8 +1157,10 @@ Your browser does not support the video tag.
|
|
1144
1157
|
elsif node.document.attr? 'icons'
|
1145
1158
|
src = node.icon_uri("callouts/#{node.text}")
|
1146
1159
|
%(<img src="#{src}" alt="#{node.text}"#{@void_element_slash}>)
|
1160
|
+
elsif ::Array === (guard = node.attributes['guard'])
|
1161
|
+
%(<!--<b class="conum">(#{node.text})</b>-->)
|
1147
1162
|
else
|
1148
|
-
%(#{
|
1163
|
+
%(#{guard}<b class="conum">(#{node.text})</b>)
|
1149
1164
|
end
|
1150
1165
|
end
|
1151
1166
|
|
@@ -1186,9 +1201,7 @@ Your browser does not support the video tag.
|
|
1186
1201
|
end
|
1187
1202
|
img ||= %(<img src="#{type == 'icon' ? (node.icon_uri target) : (node.image_uri target)}" alt="#{encode_attribute_value node.alt}"#{attrs}#{@void_element_slash}>)
|
1188
1203
|
end
|
1189
|
-
if node.attr? 'link'
|
1190
|
-
img = %(<a class="image" href="#{node.attr 'link'}"#{(append_link_constraint_attrs node).join}>#{img}</a>)
|
1191
|
-
end
|
1204
|
+
img = %(<a class="image" href="#{node.attr 'link'}"#{(append_link_constraint_attrs node).join}>#{img}</a>) if node.attr? 'link'
|
1192
1205
|
if (role = node.role)
|
1193
1206
|
if node.attr? 'float'
|
1194
1207
|
class_attr_val = %(#{type} #{node.attr 'float'} #{role})
|
@@ -1252,16 +1265,19 @@ Your browser does not support the video tag.
|
|
1252
1265
|
|
1253
1266
|
# NOTE expose read_svg_contents for Bespoke converter
|
1254
1267
|
def read_svg_contents node, target
|
1255
|
-
if (svg = node.read_contents target, start: (node.document.attr 'imagesdir'), normalize: true, label: 'SVG')
|
1268
|
+
if (svg = node.read_contents target, start: (node.document.attr 'imagesdir'), normalize: true, label: 'SVG', warn_if_empty: true)
|
1269
|
+
return if svg.empty?
|
1256
1270
|
svg = svg.sub SvgPreambleRx, '' unless svg.start_with? '<svg'
|
1257
|
-
old_start_tag = new_start_tag = nil
|
1271
|
+
old_start_tag = new_start_tag = start_tag_match = nil
|
1258
1272
|
# NOTE width, height and style attributes are removed if either width or height is specified
|
1259
1273
|
['width', 'height'].each do |dim|
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
new_start_tag =
|
1274
|
+
next unless node.attr? dim
|
1275
|
+
unless new_start_tag
|
1276
|
+
next if (start_tag_match ||= (svg.match SvgStartTagRx) || :no_match) == :no_match
|
1277
|
+
new_start_tag = (old_start_tag = start_tag_match[0]).gsub DimensionAttributeRx, ''
|
1264
1278
|
end
|
1279
|
+
# NOTE a unitless value in HTML is assumed to be px, so we can pass the value straight through
|
1280
|
+
new_start_tag = %(#{new_start_tag.chop} #{dim}="#{node.attr dim}">)
|
1265
1281
|
end
|
1266
1282
|
svg = %(#{new_start_tag}#{svg[old_start_tag.length..-1]}) if new_start_tag
|
1267
1283
|
end
|
@@ -1297,10 +1313,17 @@ Your browser does not support the video tag.
|
|
1297
1313
|
manname_id_attr = (manname_id = node.attr 'manname-id') ? %( id="#{manname_id}") : ''
|
1298
1314
|
%(<h2#{manname_id_attr}>#{manname_title}</h2>
|
1299
1315
|
<div class="sectionbody">
|
1300
|
-
<p>#{node.attr '
|
1316
|
+
<p>#{(node.attr 'mannames').join ', '} - #{node.attr 'manpurpose'}</p>
|
1301
1317
|
</div>)
|
1302
1318
|
end
|
1303
1319
|
|
1320
|
+
def get_root_document node
|
1321
|
+
while (node = node.document).nested?
|
1322
|
+
node = node.parent_document
|
1323
|
+
end
|
1324
|
+
node
|
1325
|
+
end
|
1326
|
+
|
1304
1327
|
# NOTE adapt to older converters that relied on unprefixed method names
|
1305
1328
|
def method_missing id, *params
|
1306
1329
|
!((name = id.to_s).start_with? 'convert_') && (handles? name) ? (send %(convert_#{name}), *params) : super
|
@@ -2,8 +2,12 @@
|
|
2
2
|
module Asciidoctor
|
3
3
|
# A built-in {Converter} implementation that generates the man page (troff) format.
|
4
4
|
#
|
5
|
-
# The output
|
6
|
-
#
|
5
|
+
# The output of this converter adheres to the man definition as defined by
|
6
|
+
# groff and uses the manpage output of the DocBook toolchain as a foundation.
|
7
|
+
# That means if you've previously been generating man pages using the a2x tool
|
8
|
+
# from AsciiDoc.py, you should be able to achieve a very similar result
|
9
|
+
# using this converter. Though you'll also get to enjoy some notable
|
10
|
+
# enhancements that have been added since, such as the customizable linkstyle.
|
7
11
|
#
|
8
12
|
# See http://www.gnu.org/software/groff/manual/html_node/Man-usage.html#Man-usage
|
9
13
|
class Converter::ManPageConverter < Converter::Base
|
@@ -15,13 +19,16 @@ class Converter::ManPageConverter < Converter::Base
|
|
15
19
|
ESC_BS = %(#{ESC}\\) # escaped backslash (indicates troff formatting sequence)
|
16
20
|
ESC_FS = %(#{ESC}.) # escaped full stop (indicates troff macro)
|
17
21
|
|
18
|
-
LiteralBackslashRx =
|
22
|
+
LiteralBackslashRx = /\A\\|(#{ESC})?\\/
|
19
23
|
LeadingPeriodRx = /^\./
|
20
24
|
EscapedMacroRx = /^(?:#{ESC}\\c\n)?#{ESC}\.((?:URL|MTO) "#{CC_ANY}*?" "#{CC_ANY}*?" )( |[^\s]*)(#{CC_ANY}*?)(?: *#{ESC}\\c)?$/
|
21
|
-
|
25
|
+
MalformedEscapedMacroRx = /(#{ESC}\\c) (#{ESC}\.(?:URL|MTO) )/
|
26
|
+
MockMacroRx = /<\/?(#{ESC}\\[^>]+)>/
|
22
27
|
EmDashCharRefRx = /—(?:​)?/
|
23
28
|
EllipsisCharRefRx = /…(?:​)?/
|
24
29
|
WrappedIndentRx = /#{CG_BLANK}*#{LF}#{CG_BLANK}*/
|
30
|
+
XMLMarkupRx = /&#?[a-z\d]+;|</
|
31
|
+
PCDATAFilterRx = /(&#?[a-z\d]+;|<[^>]+>)|([^&<]+)/
|
25
32
|
|
26
33
|
def initialize backend, opts = {}
|
27
34
|
@backend = backend
|
@@ -91,17 +98,14 @@ class Converter::ManPageConverter < Converter::Base
|
|
91
98
|
if node.attr? 'manpurpose'
|
92
99
|
mannames = node.attr 'mannames', [manname]
|
93
100
|
result << %(.SH "#{(node.attr 'manname-title', 'NAME').upcase}"
|
94
|
-
#{mannames.map {|n| manify n }.join ', '} \\- #{manify node.attr('manpurpose'), whitespace: :normalize})
|
101
|
+
#{mannames.map {|n| (manify n).gsub '\-', '-' }.join ', '} \\- #{manify node.attr('manpurpose'), whitespace: :normalize})
|
95
102
|
end
|
96
103
|
end
|
97
104
|
|
98
105
|
result << node.content
|
99
106
|
|
100
107
|
# QUESTION should NOTES come after AUTHOR(S)?
|
101
|
-
|
102
|
-
result << '.SH "NOTES"'
|
103
|
-
result.concat(node.footnotes.map {|fn| %(#{fn.index}. #{fn.text}) })
|
104
|
-
end
|
108
|
+
append_footnotes result, node
|
105
109
|
|
106
110
|
unless (authors = node.authors).empty?
|
107
111
|
if authors.size > 1
|
@@ -124,10 +128,7 @@ class Converter::ManPageConverter < Converter::Base
|
|
124
128
|
def convert_embedded node
|
125
129
|
result = [node.content]
|
126
130
|
|
127
|
-
|
128
|
-
result << '.SH "NOTES"'
|
129
|
-
result.concat(node.footnotes.map {|fn| %(#{fn.index}. #{fn.text}) })
|
130
|
-
end
|
131
|
+
append_footnotes result, node
|
131
132
|
|
132
133
|
# QUESTION should we add an AUTHOR(S) section?
|
133
134
|
|
@@ -142,7 +143,7 @@ class Converter::ManPageConverter < Converter::Base
|
|
142
143
|
stitle = node.captioned_title
|
143
144
|
else
|
144
145
|
macro = 'SH'
|
145
|
-
stitle = node.title
|
146
|
+
stitle = uppercase_pcdata node.title
|
146
147
|
end
|
147
148
|
result << %(.#{macro} "#{manify stitle}"
|
148
149
|
#{node.content})
|
@@ -247,7 +248,9 @@ r lw(\n(.lu*75u/100u).'
|
|
247
248
|
result << %(.sp
|
248
249
|
.if n .RS 4
|
249
250
|
.nf
|
251
|
+
.fam C
|
250
252
|
#{manify node.content, whitespace: :preserve}
|
253
|
+
.fam
|
251
254
|
.fi
|
252
255
|
.if n .RE)
|
253
256
|
result.join LF
|
@@ -261,7 +264,9 @@ r lw(\n(.lu*75u/100u).'
|
|
261
264
|
result << %(.sp
|
262
265
|
.if n .RS 4
|
263
266
|
.nf
|
267
|
+
.fam C
|
264
268
|
#{manify node.content, whitespace: :preserve}
|
269
|
+
.fam
|
265
270
|
.fi
|
266
271
|
.if n .RE)
|
267
272
|
result.join LF
|
@@ -284,15 +289,16 @@ r lw(\n(.lu*75u/100u).'
|
|
284
289
|
.B #{manify node.title}
|
285
290
|
.br) if node.title?
|
286
291
|
|
292
|
+
start = (node.attr 'start', 1).to_i
|
287
293
|
node.items.each_with_index do |item, idx|
|
288
294
|
result << %(.sp
|
289
295
|
.RS 4
|
290
296
|
.ie n \\{\\
|
291
|
-
\\h'-04' #{idx +
|
297
|
+
\\h'-04' #{numeral = idx + start}.\\h'+01'\\c
|
292
298
|
.\\}
|
293
299
|
.el \\{\\
|
294
300
|
. sp -1
|
295
|
-
. IP " #{
|
301
|
+
. IP " #{numeral}." 4.2
|
296
302
|
.\\}
|
297
303
|
#{manify item.text, whitespace: :normalize})
|
298
304
|
result << item.content if item.blocks?
|
@@ -310,8 +316,9 @@ r lw(\n(.lu*75u/100u).'
|
|
310
316
|
end
|
311
317
|
end
|
312
318
|
|
313
|
-
|
314
|
-
|
319
|
+
def convert_page_break node
|
320
|
+
'.bp'
|
321
|
+
end
|
315
322
|
|
316
323
|
def convert_paragraph node
|
317
324
|
if node.title?
|
@@ -526,12 +533,13 @@ allbox tab(:);'
|
|
526
533
|
result.join LF
|
527
534
|
end
|
528
535
|
|
529
|
-
# FIXME git uses [verse] for the synopsis; detect this special case
|
530
536
|
def convert_verse node
|
531
537
|
result = []
|
532
|
-
|
538
|
+
if node.title?
|
539
|
+
result << %(.sp
|
533
540
|
.B #{manify node.title}
|
534
|
-
.br)
|
541
|
+
.br)
|
542
|
+
end
|
535
543
|
attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil
|
536
544
|
attribution_line = (node.attr? 'attribution') ? %[#{attribution_line}\\(em #{node.attr 'attribution'}] : nil
|
537
545
|
result << %(.sp
|
@@ -580,11 +588,7 @@ allbox tab(:);'
|
|
580
588
|
when :xref
|
581
589
|
unless (text = node.text)
|
582
590
|
refid = node.attributes['refid']
|
583
|
-
|
584
|
-
text = (ref.xreftext node.attr('xrefstyle', nil, true)) || %([#{refid}])
|
585
|
-
else
|
586
|
-
text = %([#{refid}])
|
587
|
-
end
|
591
|
+
text = %([#{refid}]) unless AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid]) && (@resolving_xref ||= outer = true) && outer && (text = ref.xreftext node.attr 'xrefstyle', nil, true)
|
588
592
|
end
|
589
593
|
text
|
590
594
|
when :ref, :bibref
|
@@ -601,14 +605,13 @@ allbox tab(:);'
|
|
601
605
|
end
|
602
606
|
|
603
607
|
def convert_inline_button node
|
604
|
-
%(
|
608
|
+
%(<#{ESC_BS}fB>[#{ESC_BS}0#{node.text}#{ESC_BS}0]</#{ESC_BS}fP>)
|
605
609
|
end
|
606
610
|
|
607
611
|
def convert_inline_callout node
|
608
|
-
%(
|
612
|
+
%(<#{ESC_BS}fB>(#{node.text})<#{ESC_BS}fP>)
|
609
613
|
end
|
610
614
|
|
611
|
-
# TODO supposedly groff has footnotes, but we're in search of an example
|
612
615
|
def convert_inline_footnote node
|
613
616
|
if (index = node.attr 'index')
|
614
617
|
%([#{index}])
|
@@ -626,39 +629,35 @@ allbox tab(:);'
|
|
626
629
|
end
|
627
630
|
|
628
631
|
def convert_inline_kbd node
|
629
|
-
|
630
|
-
keys[0]
|
631
|
-
else
|
632
|
-
keys.join %(#{ESC_BS}0+#{ESC_BS}0)
|
633
|
-
end
|
632
|
+
%[<#{ESC_BS}f(CR>#{(keys = node.attr 'keys').size == 1 ? keys[0] : (keys.join "#{ESC_BS}0+#{ESC_BS}0")}</#{ESC_BS}fP>]
|
634
633
|
end
|
635
634
|
|
636
635
|
def convert_inline_menu node
|
637
636
|
caret = %[#{ESC_BS}0#{ESC_BS}(fc#{ESC_BS}0]
|
638
637
|
menu = node.attr 'menu'
|
639
638
|
if !(submenus = node.attr 'submenus').empty?
|
640
|
-
submenu_path = submenus.map {|item| %(
|
641
|
-
%(
|
639
|
+
submenu_path = submenus.map {|item| %(<#{ESC_BS}fI>#{item}</#{ESC_BS}fP>) }.join caret
|
640
|
+
%(<#{ESC_BS}fI>#{menu}</#{ESC_BS}fP>#{caret}#{submenu_path}#{caret}<#{ESC_BS}fI>#{node.attr 'menuitem'}</#{ESC_BS}fP>)
|
642
641
|
elsif (menuitem = node.attr 'menuitem')
|
643
|
-
%(
|
642
|
+
%(<#{ESC_BS}fI>#{menu}#{caret}#{menuitem}</#{ESC_BS}fP>)
|
644
643
|
else
|
645
|
-
%(
|
644
|
+
%(<#{ESC_BS}fI>#{menu}</#{ESC_BS}fP>)
|
646
645
|
end
|
647
646
|
end
|
648
647
|
|
649
|
-
# NOTE use fake
|
648
|
+
# NOTE use fake XML elements to prevent creating artificial word boundaries
|
650
649
|
def convert_inline_quoted node
|
651
650
|
case node.type
|
652
651
|
when :emphasis
|
653
|
-
%(
|
652
|
+
%(<#{ESC_BS}fI>#{node.text}</#{ESC_BS}fP>)
|
654
653
|
when :strong
|
655
|
-
%(
|
654
|
+
%(<#{ESC_BS}fB>#{node.text}</#{ESC_BS}fP>)
|
656
655
|
when :monospaced
|
657
|
-
%[
|
656
|
+
%[<#{ESC_BS}f(CR>#{node.text}</#{ESC_BS}fP>]
|
658
657
|
when :single
|
659
|
-
%[
|
658
|
+
%[<#{ESC_BS}(oq>#{node.text}</#{ESC_BS}(cq>]
|
660
659
|
when :double
|
661
|
-
%[
|
660
|
+
%[<#{ESC_BS}(lq>#{node.text}</#{ESC_BS}(rq>]
|
662
661
|
else
|
663
662
|
node.text
|
664
663
|
end
|
@@ -677,6 +676,22 @@ allbox tab(:);'
|
|
677
676
|
|
678
677
|
private
|
679
678
|
|
679
|
+
def append_footnotes result, node
|
680
|
+
if node.footnotes? && !(node.attr? 'nofootnotes')
|
681
|
+
result << '.SH "NOTES"'
|
682
|
+
node.footnotes.each_with_index do |fn, idx|
|
683
|
+
result << %(.IP [#{fn.index}])
|
684
|
+
# NOTE restore newline in escaped macro that gets removed by normalize_text in substitutor
|
685
|
+
if (text = fn.text).include? %(#{ESC}\\c #{ESC}.)
|
686
|
+
text = (manify %(#{text.gsub MalformedEscapedMacroRx, %(\\1#{LF}\\2)} ), whitespace: :normalize).chomp ' '
|
687
|
+
else
|
688
|
+
text = manify text, whitespace: :normalize
|
689
|
+
end
|
690
|
+
result << text
|
691
|
+
end
|
692
|
+
end
|
693
|
+
end
|
694
|
+
|
680
695
|
# Converts HTML entity references back to their original form, escapes
|
681
696
|
# special man characters and strips trailing whitespace.
|
682
697
|
#
|
@@ -699,10 +714,11 @@ allbox tab(:);'
|
|
699
714
|
str = str.tr_s WHITESPACE, ' '
|
700
715
|
end
|
701
716
|
str = str.
|
702
|
-
gsub(LiteralBackslashRx
|
717
|
+
gsub(LiteralBackslashRx) { $1 ? $& : '\\(rs' }. # literal backslash (not a troff escape sequence)
|
718
|
+
gsub(EllipsisCharRefRx, '...'). # horizontal ellipsis
|
703
719
|
gsub(LeadingPeriodRx, '\\\&.'). # leading . is used in troff for macro call or other formatting; replace with \&.
|
704
720
|
# drop orphaned \c escape lines, unescape troff macro, quote adjacent character, isolate macro line
|
705
|
-
gsub(EscapedMacroRx) { (rest = $3.lstrip).empty? ? %(.#$1"#$2") : %(.#$1"
|
721
|
+
gsub(EscapedMacroRx) { (rest = $3.lstrip).empty? ? %(.#$1"#$2") : %(.#$1"#{$2.rstrip}"#{LF}#{rest}) }.
|
706
722
|
gsub('-', '\-').
|
707
723
|
gsub('<', '<').
|
708
724
|
gsub('>', '>').
|
@@ -717,21 +733,24 @@ allbox tab(:);'
|
|
717
733
|
gsub('’', '\(cq'). # right single quotation mark
|
718
734
|
gsub('“', '\(lq'). # left double quotation mark
|
719
735
|
gsub('”', '\(rq'). # right double quotation mark
|
720
|
-
gsub(EllipsisCharRefRx, '...'). # horizontal ellipsis
|
721
736
|
gsub('←', '\(<-'). # leftwards arrow
|
722
737
|
gsub('→', '\(->'). # rightwards arrow
|
723
738
|
gsub('⇐', '\(lA'). # leftwards double arrow
|
724
739
|
gsub('⇒', '\(rA'). # rightwards double arrow
|
725
740
|
gsub('​', '\:'). # zero width space
|
726
|
-
gsub('&','&').
|
741
|
+
gsub('&', '&'). # literal ampersand (NOTE must take place after any other replacement that includes &)
|
727
742
|
gsub('\'', '\(aq'). # apostrophe-quote
|
728
|
-
gsub(
|
743
|
+
gsub(MockMacroRx, '\1'). # mock boundary
|
729
744
|
gsub(ESC_BS, '\\'). # unescape troff backslash (NOTE update if more escapes are added)
|
730
745
|
gsub(ESC_FS, '.'). # unescape full stop in troff commands (NOTE must take place after gsub(LeadingPeriodRx))
|
731
746
|
rstrip # strip trailing space
|
732
747
|
opts[:append_newline] ? %(#{str}#{LF}) : str
|
733
748
|
end
|
734
749
|
|
750
|
+
def uppercase_pcdata string
|
751
|
+
(XMLMarkupRx.match? string) ? string.gsub(PCDATAFilterRx) { $2 ? $2.upcase : $1 } : string.upcase
|
752
|
+
end
|
753
|
+
|
735
754
|
def enclose_content node
|
736
755
|
node.content_model == :compound ? node.content : %(.sp#{LF}#{manify node.content, whitespace: :normalize})
|
737
756
|
end
|