asciidoctor 2.0.10 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +211 -22
  3. data/LICENSE +1 -1
  4. data/README-de.adoc +12 -13
  5. data/README-fr.adoc +11 -15
  6. data/README-jp.adoc +9 -17
  7. data/README-zh_CN.adoc +17 -18
  8. data/README.adoc +140 -132
  9. data/asciidoctor.gemspec +6 -6
  10. data/data/locale/attributes-ar.adoc +4 -3
  11. data/data/locale/attributes-be.adoc +23 -0
  12. data/data/locale/attributes-bg.adoc +4 -3
  13. data/data/locale/attributes-ca.adoc +6 -5
  14. data/data/locale/attributes-cs.adoc +4 -3
  15. data/data/locale/attributes-da.adoc +6 -5
  16. data/data/locale/attributes-de.adoc +4 -4
  17. data/data/locale/attributes-en.adoc +4 -4
  18. data/data/locale/attributes-es.adoc +6 -5
  19. data/data/locale/attributes-fa.adoc +4 -3
  20. data/data/locale/attributes-fi.adoc +4 -3
  21. data/data/locale/attributes-fr.adoc +6 -5
  22. data/data/locale/attributes-hu.adoc +4 -3
  23. data/data/locale/attributes-id.adoc +4 -3
  24. data/data/locale/attributes-it.adoc +6 -5
  25. data/data/locale/attributes-ja.adoc +4 -3
  26. data/data/locale/{attributes-kr.adoc → attributes-ko.adoc} +4 -3
  27. data/data/locale/attributes-nb.adoc +4 -3
  28. data/data/locale/attributes-nl.adoc +6 -5
  29. data/data/locale/attributes-nn.adoc +4 -3
  30. data/data/locale/attributes-pl.adoc +8 -7
  31. data/data/locale/attributes-pt.adoc +6 -5
  32. data/data/locale/attributes-pt_BR.adoc +6 -5
  33. data/data/locale/attributes-ro.adoc +4 -3
  34. data/data/locale/attributes-ru.adoc +6 -5
  35. data/data/locale/attributes-sr.adoc +4 -4
  36. data/data/locale/attributes-sr_Latn.adoc +4 -4
  37. data/data/locale/attributes-sv.adoc +4 -4
  38. data/data/locale/attributes-tr.adoc +4 -3
  39. data/data/locale/attributes-uk.adoc +6 -5
  40. data/data/locale/attributes-zh_CN.adoc +4 -3
  41. data/data/locale/attributes-zh_TW.adoc +4 -3
  42. data/data/reference/syntax.adoc +14 -7
  43. data/data/stylesheets/asciidoctor-default.css +27 -28
  44. data/lib/asciidoctor.rb +38 -12
  45. data/lib/asciidoctor/abstract_block.rb +9 -4
  46. data/lib/asciidoctor/abstract_node.rb +16 -6
  47. data/lib/asciidoctor/attribute_list.rb +64 -72
  48. data/lib/asciidoctor/cli/invoker.rb +2 -0
  49. data/lib/asciidoctor/cli/options.rb +10 -9
  50. data/lib/asciidoctor/convert.rb +167 -162
  51. data/lib/asciidoctor/converter.rb +13 -12
  52. data/lib/asciidoctor/converter/docbook5.rb +29 -12
  53. data/lib/asciidoctor/converter/html5.rb +69 -46
  54. data/lib/asciidoctor/converter/manpage.rb +68 -49
  55. data/lib/asciidoctor/converter/template.rb +3 -0
  56. data/lib/asciidoctor/document.rb +43 -50
  57. data/lib/asciidoctor/extensions.rb +2 -4
  58. data/lib/asciidoctor/helpers.rb +20 -15
  59. data/lib/asciidoctor/load.rb +102 -101
  60. data/lib/asciidoctor/parser.rb +40 -32
  61. data/lib/asciidoctor/path_resolver.rb +14 -12
  62. data/lib/asciidoctor/reader.rb +22 -13
  63. data/lib/asciidoctor/rx.rb +7 -6
  64. data/lib/asciidoctor/substitutors.rb +78 -57
  65. data/lib/asciidoctor/syntax_highlighter.rb +8 -5
  66. data/lib/asciidoctor/syntax_highlighter/coderay.rb +1 -1
  67. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +12 -4
  68. data/lib/asciidoctor/syntax_highlighter/prettify.rb +7 -4
  69. data/lib/asciidoctor/syntax_highlighter/pygments.rb +6 -7
  70. data/lib/asciidoctor/syntax_highlighter/rouge.rb +33 -19
  71. data/lib/asciidoctor/table.rb +52 -23
  72. data/lib/asciidoctor/version.rb +1 -1
  73. data/man/asciidoctor.1 +8 -8
  74. data/man/asciidoctor.adoc +4 -4
  75. 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 Python.
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[^>+]+|\/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\b)/
29
- SvgStartTagRx = /^<svg[^>]*>/
28
+ SvgPreambleRx = /^#{CC_ALL}*?(?=<svg[\s>])/
29
+ SvgStartTagRx = /^<svg(?:\s[^>]*)?>/
30
30
  else
31
- SvgPreambleRx = /\A.*?(?=<svg\b)/m
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.read_asset node.normalize_system_path((node.attr 'stylesheet'), (node.attr 'stylesdir', '')), warn_on_failure: true, label: 'stylesheet'}
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) && (syntax_hl.docinfo? :head)
156
- result << (syntax_hl.docinfo :head, node, cdn_base_url: cdn_base_url, linkcss: linkcss, self_closing_tag_slash: slash)
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
- body_attrs = node.id ? [%(id="#{node.id}")] : []
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
- body_attrs << %(class="#{classes.join ' '}")
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 << '<div id="header">'
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
- #{convert_outline node}
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
- #{convert_outline node}
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 << '<div id="footer">'
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 && (syntax_hl.docinfo? :footer)
254
- result << (syntax_hl.docinfo :footer, node, cdn_base_url: cdn_base_url, linkcss: linkcss, self_closing_tag_slash: slash)
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('stemblock')) {
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
- #{convert_outline node}
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}>#{LF})
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
- #{convert_outline doc}
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
- classes = ['tableblock', %(frame-#{node.attr 'frame', 'all', 'table-frame'}), %(grid-#{node.attr 'grid', 'all', 'table-grid'})]
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
- #{convert_outline doc, toclevels: levels}
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
- if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid])
1111
- text = (ref.xreftext node.attr('xrefstyle', nil, true)) || %([#{refid}])
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
+ %(&lt;!--<b class="conum">(#{node.text})</b>--&gt;)
1147
1162
  else
1148
- %(#{node.attributes['guard']}<b class="conum">(#{node.text})</b>)
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
- if node.attr? dim
1261
- new_start_tag = (old_start_tag = (svg.match SvgStartTagRx)[0]).gsub DimensionAttributeRx, '' unless new_start_tag
1262
- # QUESTION should we add px since it's already the default?
1263
- new_start_tag = %(#{new_start_tag.chop} #{dim}="#{node.attr dim}px">)
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 'manname'} - #{node.attr 'manpurpose'}</p>
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 follows the groff man page definition while also trying to be
6
- # consistent with the output produced by the a2x tool from AsciiDoc Python.
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 = /(?:\A|[^#{ESC}])\\/
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
- MockBoundaryRx = /<\/?BOUNDARY>/
25
+ MalformedEscapedMacroRx = /(#{ESC}\\c) (#{ESC}\.(?:URL|MTO) )/
26
+ MockMacroRx = /<\/?(#{ESC}\\[^>]+)>/
22
27
  EmDashCharRefRx = /&#8212;(?:&#8203;)?/
23
28
  EllipsisCharRefRx = /&#8230;(?:&#8203;)?/
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
- if node.footnotes? && !(node.attr? 'nofootnotes')
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
- if node.footnotes? && !(node.attr? 'nofootnotes')
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.upcase
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 + 1}.\\h'+01'\\c
297
+ \\h'-04' #{numeral = idx + start}.\\h'+01'\\c
292
298
  .\\}
293
299
  .el \\{\\
294
300
  . sp -1
295
- . IP " #{idx + 1}." 4.2
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
- # TODO use Page Control https://www.gnu.org/software/groff/manual/html_node/Page-Control.html#Page-Control
314
- alias convert_page_break skip
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
- result << (node.title? ? %(.sp
538
+ if node.title?
539
+ result << %(.sp
533
540
  .B #{manify node.title}
534
- .br) : '.sp')
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
- if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid])
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
- %(#{ESC_BS}fB[#{ESC_BS}0#{node.text}#{ESC_BS}0]#{ESC_BS}fP)
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
- %(#{ESC_BS}fB(#{node.text})#{ESC_BS}fP)
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
- if (keys = node.attr 'keys').size == 1
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| %(#{ESC_BS}fI#{item}#{ESC_BS}fP) }.join caret
641
- %(#{ESC_BS}fI#{menu}#{ESC_BS}fP#{caret}#{submenu_path}#{caret}#{ESC_BS}fI#{node.attr 'menuitem'}#{ESC_BS}fP)
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
- %(#{ESC_BS}fI#{menu}#{caret}#{menuitem}#{ESC_BS}fP)
642
+ %(<#{ESC_BS}fI>#{menu}#{caret}#{menuitem}</#{ESC_BS}fP>)
644
643
  else
645
- %(#{ESC_BS}fI#{menu}#{ESC_BS}fP)
644
+ %(<#{ESC_BS}fI>#{menu}</#{ESC_BS}fP>)
646
645
  end
647
646
  end
648
647
 
649
- # NOTE use fake <BOUNDARY> element to prevent creating artificial word boundaries
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
- %(#{ESC_BS}fI<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP)
652
+ %(<#{ESC_BS}fI>#{node.text}</#{ESC_BS}fP>)
654
653
  when :strong
655
- %(#{ESC_BS}fB<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP)
654
+ %(<#{ESC_BS}fB>#{node.text}</#{ESC_BS}fP>)
656
655
  when :monospaced
657
- %[#{ESC_BS}f(CR<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP]
656
+ %[<#{ESC_BS}f(CR>#{node.text}</#{ESC_BS}fP>]
658
657
  when :single
659
- %[#{ESC_BS}(oq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(cq]
658
+ %[<#{ESC_BS}(oq>#{node.text}</#{ESC_BS}(cq>]
660
659
  when :double
661
- %[#{ESC_BS}(lq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(rq]
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, '\&(rs'). # literal backslash (not a troff escape sequence)
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"#$2"#{LF}#{rest}) }.
721
+ gsub(EscapedMacroRx) { (rest = $3.lstrip).empty? ? %(.#$1"#$2") : %(.#$1"#{$2.rstrip}"#{LF}#{rest}) }.
706
722
  gsub('-', '\-').
707
723
  gsub('&lt;', '<').
708
724
  gsub('&gt;', '>').
@@ -717,21 +733,24 @@ allbox tab(:);'
717
733
  gsub('&#8217;', '\(cq'). # right single quotation mark
718
734
  gsub('&#8220;', '\(lq'). # left double quotation mark
719
735
  gsub('&#8221;', '\(rq'). # right double quotation mark
720
- gsub(EllipsisCharRefRx, '...'). # horizontal ellipsis
721
736
  gsub('&#8592;', '\(<-'). # leftwards arrow
722
737
  gsub('&#8594;', '\(->'). # rightwards arrow
723
738
  gsub('&#8656;', '\(lA'). # leftwards double arrow
724
739
  gsub('&#8658;', '\(rA'). # rightwards double arrow
725
740
  gsub('&#8203;', '\:'). # zero width space
726
- gsub('&amp;','&'). # literal ampersand (NOTE must take place after any other replacement that includes &)
741
+ gsub('&amp;', '&'). # literal ampersand (NOTE must take place after any other replacement that includes &)
727
742
  gsub('\'', '\(aq'). # apostrophe-quote
728
- gsub(MockBoundaryRx, ''). # mock boundary
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