asciidoctor 2.0.9 → 2.0.14

Sign up to get free protection for your applications and to get access to all the features.
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