asciidoctor 2.0.9 → 2.0.14

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 +193 -16
  3. data/LICENSE +1 -1
  4. data/README-de.adoc +12 -13
  5. data/README-fr.adoc +11 -15
  6. data/README-jp.adoc +242 -185
  7. data/README-zh_CN.adoc +17 -18
  8. data/README.adoc +133 -131
  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 +30 -30
  44. data/lib/asciidoctor.rb +40 -14
  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 +63 -71
  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 +5 -9
  53. data/lib/asciidoctor/converter/html5.rb +58 -45
  54. data/lib/asciidoctor/converter/manpage.rb +61 -38
  55. data/lib/asciidoctor/converter/template.rb +3 -0
  56. data/lib/asciidoctor/document.rb +44 -51
  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 +20 -13
  63. data/lib/asciidoctor/rx.rb +7 -6
  64. data/lib/asciidoctor/substitutors.rb +69 -50
  65. data/lib/asciidoctor/syntax_highlighter.rb +15 -7
  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 DocBook 5 output. The output is inspired by the output produced
4
- # by the docbook45 backend from AsciiDoc Python, except it has been migrated to the DocBook 5 specification.
4
+ # by the docbook45 backend from AsciiDoc.py, except it has been migrated to the DocBook 5 specification.
5
5
  class Converter::DocBook5Converter < Converter::Base
6
6
  register_for 'docbook5'
7
7
 
@@ -371,9 +371,7 @@ class Converter::DocBook5Converter < Converter::Base
371
371
  has_body = false
372
372
  result = []
373
373
  pgwide_attribute = (node.option? 'pgwide') ? ' pgwide="1"' : ''
374
- if (frame = node.attr 'frame', 'all', 'table-frame') == 'ends'
375
- frame = 'topbot'
376
- end
374
+ frame = 'topbot' if (frame = node.attr 'frame', 'all', 'table-frame') == 'ends'
377
375
  grid = node.attr 'grid', nil, 'table-grid'
378
376
  result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext}#{pgwide_attribute} frame="#{frame}" rowsep="#{['none', 'cols'].include?(grid) ? 0 : 1}" colsep="#{['none', 'rows'].include?(grid) ? 0 : 1}"#{(node.attr? 'orientation', 'landscape', 'table-orientation') ? ' orient="land"' : ''}>)
379
377
  if (node.option? 'unbreakable')
@@ -401,12 +399,10 @@ class Converter::DocBook5Converter < Converter::Base
401
399
  rows.each do |row|
402
400
  result << '<row>'
403
401
  row.each do |cell|
404
- halign_attribute = (cell.attr? 'halign') ? %( align="#{cell.attr 'halign'}") : ''
405
- valign_attribute = (cell.attr? 'valign') ? %( valign="#{cell.attr 'valign'}") : ''
406
402
  colspan_attribute = cell.colspan ? %( namest="col_#{colnum = cell.column.attr 'colnumber'}" nameend="col_#{colnum + cell.colspan - 1}") : ''
407
403
  rowspan_attribute = cell.rowspan ? %( morerows="#{cell.rowspan - 1}") : ''
408
404
  # NOTE <entry> may not have whitespace (e.g., line breaks) as a direct descendant according to DocBook rules
409
- entry_start = %(<entry#{halign_attribute}#{valign_attribute}#{colspan_attribute}#{rowspan_attribute}>)
405
+ entry_start = %(<entry align="#{cell.attr 'halign'}" valign="#{cell.attr 'valign'}"#{colspan_attribute}#{rowspan_attribute}>)
410
406
  if tsec == :head
411
407
  cell_content = cell.text
412
408
  else
@@ -487,7 +483,7 @@ class Converter::DocBook5Converter < Converter::Base
487
483
  when :link
488
484
  %(<link xl:href="#{node.target}">#{node.text}</link>)
489
485
  when :bibref
490
- %(<anchor#{common_attributes node.id, nil, "[#{node.reftext || node.id}]"}/>#{text})
486
+ %(<anchor#{common_attributes node.id, nil, (text = "[#{node.reftext || node.id}]")}/>#{text})
491
487
  else
492
488
  logger.warn %(unknown anchor type: #{node.type.inspect})
493
489
  nil
@@ -517,7 +513,7 @@ class Converter::DocBook5Converter < Converter::Base
517
513
  def convert_inline_image node
518
514
  width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : ''
519
515
  depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : ''
520
- %(<inlinemediaobject>
516
+ %(<inlinemediaobject#{common_attributes nil, node.role}>
521
517
  <imageobject>
522
518
  <imagedata fileref="#{node.type == 'icon' ? (node.icon_uri node.target) : (node.image_uri node.target)}"#{width_attribute}#{depth_attribute}/>
523
519
  </imageobject>
@@ -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, linkcss: linkcss)
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>
@@ -1107,8 +1112,13 @@ Your browser does not support the video tag.
1107
1112
  attrs = node.role ? %( class="#{node.role}") : ''
1108
1113
  unless (text = node.text)
1109
1114
  refid = node.attributes['refid']
1110
- if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid])
1111
- text = (ref.xreftext node.attr('xrefstyle', nil, true)) || %([#{refid}])
1115
+ if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid]) && (@resolving_xref ||= (outer = true)) && outer
1116
+ if !(text = ref.xreftext node.attr 'xrefstyle', nil, true)
1117
+ text = %([#{refid}])
1118
+ elsif text.include? '<a'
1119
+ text = text.gsub DropAnchorRx, ''
1120
+ end
1121
+ @resolving_xref = nil
1112
1122
  else
1113
1123
  text = %([#{refid}])
1114
1124
  end
@@ -1144,8 +1154,10 @@ Your browser does not support the video tag.
1144
1154
  elsif node.document.attr? 'icons'
1145
1155
  src = node.icon_uri("callouts/#{node.text}")
1146
1156
  %(<img src="#{src}" alt="#{node.text}"#{@void_element_slash}>)
1157
+ elsif ::Array === (guard = node.attributes['guard'])
1158
+ %(&lt;!--<b class="conum">(#{node.text})</b>--&gt;)
1147
1159
  else
1148
- %(#{node.attributes['guard']}<b class="conum">(#{node.text})</b>)
1160
+ %(#{guard}<b class="conum">(#{node.text})</b>)
1149
1161
  end
1150
1162
  end
1151
1163
 
@@ -1186,9 +1198,7 @@ Your browser does not support the video tag.
1186
1198
  end
1187
1199
  img ||= %(<img src="#{type == 'icon' ? (node.icon_uri target) : (node.image_uri target)}" alt="#{encode_attribute_value node.alt}"#{attrs}#{@void_element_slash}>)
1188
1200
  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
1201
+ img = %(<a class="image" href="#{node.attr 'link'}"#{(append_link_constraint_attrs node).join}>#{img}</a>) if node.attr? 'link'
1192
1202
  if (role = node.role)
1193
1203
  if node.attr? 'float'
1194
1204
  class_attr_val = %(#{type} #{node.attr 'float'} #{role})
@@ -1252,16 +1262,19 @@ Your browser does not support the video tag.
1252
1262
 
1253
1263
  # NOTE expose read_svg_contents for Bespoke converter
1254
1264
  def read_svg_contents node, target
1255
- if (svg = node.read_contents target, start: (node.document.attr 'imagesdir'), normalize: true, label: 'SVG')
1265
+ if (svg = node.read_contents target, start: (node.document.attr 'imagesdir'), normalize: true, label: 'SVG', warn_if_empty: true)
1266
+ return if svg.empty?
1256
1267
  svg = svg.sub SvgPreambleRx, '' unless svg.start_with? '<svg'
1257
- old_start_tag = new_start_tag = nil
1268
+ old_start_tag = new_start_tag = start_tag_match = nil
1258
1269
  # NOTE width, height and style attributes are removed if either width or height is specified
1259
1270
  ['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">)
1271
+ next unless node.attr? dim
1272
+ unless new_start_tag
1273
+ next if (start_tag_match ||= (svg.match SvgStartTagRx) || :no_match) == :no_match
1274
+ new_start_tag = (old_start_tag = start_tag_match[0]).gsub DimensionAttributeRx, ''
1264
1275
  end
1276
+ # NOTE a unitless value in HTML is assumed to be px, so we can pass the value straight through
1277
+ new_start_tag = %(#{new_start_tag.chop} #{dim}="#{node.attr dim}">)
1265
1278
  end
1266
1279
  svg = %(#{new_start_tag}#{svg[old_start_tag.length..-1]}) if new_start_tag
1267
1280
  end
@@ -1297,7 +1310,7 @@ Your browser does not support the video tag.
1297
1310
  manname_id_attr = (manname_id = node.attr 'manname-id') ? %( id="#{manname_id}") : ''
1298
1311
  %(<h2#{manname_id_attr}>#{manname_title}</h2>
1299
1312
  <div class="sectionbody">
1300
- <p>#{node.attr 'manname'} - #{node.attr 'manpurpose'}</p>
1313
+ <p>#{(node.attr 'mannames').join ', '} - #{node.attr 'manpurpose'}</p>
1301
1314
  </div>)
1302
1315
  end
1303
1316
 
@@ -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
@@ -608,7 +612,6 @@ allbox tab(:);'
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}])
@@ -646,19 +649,19 @@ allbox tab(:);'
646
649
  end
647
650
  end
648
651
 
649
- # NOTE use fake <BOUNDARY> element to prevent creating artificial word boundaries
652
+ # NOTE use fake XML elements to prevent creating artificial word boundaries
650
653
  def convert_inline_quoted node
651
654
  case node.type
652
655
  when :emphasis
653
- %(#{ESC_BS}fI<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP)
656
+ %(<#{ESC_BS}fI>#{node.text}</#{ESC_BS}fP>)
654
657
  when :strong
655
- %(#{ESC_BS}fB<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP)
658
+ %(<#{ESC_BS}fB>#{node.text}</#{ESC_BS}fP>)
656
659
  when :monospaced
657
- %[#{ESC_BS}f(CR<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP]
660
+ %[<#{ESC_BS}f(CR>#{node.text}</#{ESC_BS}fP>]
658
661
  when :single
659
- %[#{ESC_BS}(oq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(cq]
662
+ %[<#{ESC_BS}(oq>#{node.text}</#{ESC_BS}(cq>]
660
663
  when :double
661
- %[#{ESC_BS}(lq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(rq]
664
+ %[<#{ESC_BS}(lq>#{node.text}</#{ESC_BS}(rq>]
662
665
  else
663
666
  node.text
664
667
  end
@@ -677,6 +680,22 @@ allbox tab(:);'
677
680
 
678
681
  private
679
682
 
683
+ def append_footnotes result, node
684
+ if node.footnotes? && !(node.attr? 'nofootnotes')
685
+ result << '.SH "NOTES"'
686
+ node.footnotes.each_with_index do |fn, idx|
687
+ result << %(.IP [#{fn.index}])
688
+ # NOTE restore newline in escaped macro that gets removed by normalize_text in substitutor
689
+ if (text = fn.text).include? %(#{ESC}\\c #{ESC}.)
690
+ text = (manify %(#{text.gsub MalformedEscapedMacroRx, %(\\1#{LF}\\2)} ), whitespace: :normalize).chomp ' '
691
+ else
692
+ text = manify text, whitespace: :normalize
693
+ end
694
+ result << text
695
+ end
696
+ end
697
+ end
698
+
680
699
  # Converts HTML entity references back to their original form, escapes
681
700
  # special man characters and strips trailing whitespace.
682
701
  #
@@ -699,10 +718,11 @@ allbox tab(:);'
699
718
  str = str.tr_s WHITESPACE, ' '
700
719
  end
701
720
  str = str.
702
- gsub(LiteralBackslashRx, '\&(rs'). # literal backslash (not a troff escape sequence)
721
+ gsub(LiteralBackslashRx) { $1 ? $& : '\\(rs' }. # literal backslash (not a troff escape sequence)
722
+ gsub(EllipsisCharRefRx, '...'). # horizontal ellipsis
703
723
  gsub(LeadingPeriodRx, '\\\&.'). # leading . is used in troff for macro call or other formatting; replace with \&.
704
724
  # 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}) }.
725
+ gsub(EscapedMacroRx) { (rest = $3.lstrip).empty? ? %(.#$1"#$2") : %(.#$1"#{$2.rstrip}"#{LF}#{rest}) }.
706
726
  gsub('-', '\-').
707
727
  gsub('&lt;', '<').
708
728
  gsub('&gt;', '>').
@@ -717,21 +737,24 @@ allbox tab(:);'
717
737
  gsub('&#8217;', '\(cq'). # right single quotation mark
718
738
  gsub('&#8220;', '\(lq'). # left double quotation mark
719
739
  gsub('&#8221;', '\(rq'). # right double quotation mark
720
- gsub(EllipsisCharRefRx, '...'). # horizontal ellipsis
721
740
  gsub('&#8592;', '\(<-'). # leftwards arrow
722
741
  gsub('&#8594;', '\(->'). # rightwards arrow
723
742
  gsub('&#8656;', '\(lA'). # leftwards double arrow
724
743
  gsub('&#8658;', '\(rA'). # rightwards double arrow
725
744
  gsub('&#8203;', '\:'). # zero width space
726
- gsub('&amp;','&'). # literal ampersand (NOTE must take place after any other replacement that includes &)
745
+ gsub('&amp;', '&'). # literal ampersand (NOTE must take place after any other replacement that includes &)
727
746
  gsub('\'', '\(aq'). # apostrophe-quote
728
- gsub(MockBoundaryRx, ''). # mock boundary
747
+ gsub(MockMacroRx, '\1'). # mock boundary
729
748
  gsub(ESC_BS, '\\'). # unescape troff backslash (NOTE update if more escapes are added)
730
749
  gsub(ESC_FS, '.'). # unescape full stop in troff commands (NOTE must take place after gsub(LeadingPeriodRx))
731
750
  rstrip # strip trailing space
732
751
  opts[:append_newline] ? %(#{str}#{LF}) : str
733
752
  end
734
753
 
754
+ def uppercase_pcdata string
755
+ (XMLMarkupRx.match? string) ? string.gsub(PCDATAFilterRx) { $2 ? $2.upcase : $1 } : string.upcase
756
+ end
757
+
735
758
  def enclose_content node
736
759
  node.content_model == :compound ? node.content : %(.sp#{LF}#{manify node.content, whitespace: :normalize})
737
760
  end