asciidoctor 2.0.12 → 2.0.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +142 -22
  3. data/LICENSE +1 -1
  4. data/README-de.adoc +15 -6
  5. data/README-fr.adoc +14 -8
  6. data/README-jp.adoc +15 -6
  7. data/README-zh_CN.adoc +14 -5
  8. data/README.adoc +135 -125
  9. data/asciidoctor.gemspec +4 -11
  10. data/data/locale/attributes-be.adoc +23 -0
  11. data/data/locale/attributes-it.adoc +4 -4
  12. data/data/locale/attributes-nl.adoc +6 -6
  13. data/data/locale/attributes-th.adoc +23 -0
  14. data/data/locale/attributes-vi.adoc +23 -0
  15. data/data/reference/syntax.adoc +14 -7
  16. data/data/stylesheets/asciidoctor-default.css +51 -52
  17. data/lib/asciidoctor.rb +12 -12
  18. data/lib/asciidoctor/abstract_block.rb +4 -4
  19. data/lib/asciidoctor/abstract_node.rb +10 -9
  20. data/lib/asciidoctor/attribute_list.rb +6 -6
  21. data/lib/asciidoctor/block.rb +6 -6
  22. data/lib/asciidoctor/cli/invoker.rb +0 -1
  23. data/lib/asciidoctor/cli/options.rb +27 -26
  24. data/lib/asciidoctor/convert.rb +1 -0
  25. data/lib/asciidoctor/converter.rb +5 -3
  26. data/lib/asciidoctor/converter/docbook5.rb +45 -26
  27. data/lib/asciidoctor/converter/html5.rb +89 -69
  28. data/lib/asciidoctor/converter/manpage.rb +113 -86
  29. data/lib/asciidoctor/converter/template.rb +11 -12
  30. data/lib/asciidoctor/document.rb +44 -51
  31. data/lib/asciidoctor/extensions.rb +10 -12
  32. data/lib/asciidoctor/helpers.rb +3 -6
  33. data/lib/asciidoctor/list.rb +2 -6
  34. data/lib/asciidoctor/load.rb +13 -11
  35. data/lib/asciidoctor/logging.rb +10 -8
  36. data/lib/asciidoctor/parser.rb +135 -150
  37. data/lib/asciidoctor/path_resolver.rb +3 -3
  38. data/lib/asciidoctor/reader.rb +72 -71
  39. data/lib/asciidoctor/rx.rb +4 -3
  40. data/lib/asciidoctor/substitutors.rb +117 -117
  41. data/lib/asciidoctor/syntax_highlighter.rb +8 -11
  42. data/lib/asciidoctor/syntax_highlighter/coderay.rb +2 -1
  43. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +1 -1
  44. data/lib/asciidoctor/syntax_highlighter/pygments.rb +6 -5
  45. data/lib/asciidoctor/syntax_highlighter/rouge.rb +33 -26
  46. data/lib/asciidoctor/table.rb +17 -19
  47. data/lib/asciidoctor/timings.rb +3 -3
  48. data/lib/asciidoctor/version.rb +1 -1
  49. data/man/asciidoctor.1 +10 -11
  50. data/man/asciidoctor.adoc +8 -7
  51. metadata +14 -67
@@ -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,7 +21,7 @@ class Converter::Html5Converter < Converter::Base
21
21
  #latexmath: INLINE_MATH_DELIMITERS[:latexmath] + [false],
22
22
  }).default = ['', '']
23
23
 
24
- DropAnchorRx = /<(?:a\b[^>]*|\/a)>/
24
+ DropAnchorRx = %r(<(?: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
@@ -48,44 +48,45 @@ class Converter::Html5Converter < Converter::Base
48
48
  end
49
49
 
50
50
  def convert node, transform = node.node_name, opts = nil
51
- if transform == 'inline_quoted'; return convert_inline_quoted node
52
- elsif transform == 'paragraph'; return convert_paragraph node
53
- elsif transform == 'inline_anchor'; return convert_inline_anchor node
54
- elsif transform == 'section'; return convert_section node
55
- elsif transform == 'listing'; return convert_listing node
56
- elsif transform == 'literal'; return convert_literal node
57
- elsif transform == 'ulist'; return convert_ulist node
58
- elsif transform == 'olist'; return convert_olist node
59
- elsif transform == 'dlist'; return convert_dlist node
60
- elsif transform == 'admonition'; return convert_admonition node
61
- elsif transform == 'colist'; return convert_colist node
62
- elsif transform == 'embedded'; return convert_embedded node
63
- elsif transform == 'example'; return convert_example node
64
- elsif transform == 'floating_title'; return convert_floating_title node
65
- elsif transform == 'image'; return convert_image node
66
- elsif transform == 'inline_break'; return convert_inline_break node
67
- elsif transform == 'inline_button'; return convert_inline_button node
68
- elsif transform == 'inline_callout'; return convert_inline_callout node
69
- elsif transform == 'inline_footnote'; return convert_inline_footnote node
70
- elsif transform == 'inline_image'; return convert_inline_image node
71
- elsif transform == 'inline_indexterm'; return convert_inline_indexterm node
72
- elsif transform == 'inline_kbd'; return convert_inline_kbd node
73
- elsif transform == 'inline_menu'; return convert_inline_menu node
74
- elsif transform == 'open'; return convert_open node
75
- elsif transform == 'page_break'; return convert_page_break node
76
- elsif transform == 'preamble'; return convert_preamble node
77
- elsif transform == 'quote'; return convert_quote node
78
- elsif transform == 'sidebar'; return convert_sidebar node
79
- elsif transform == 'stem'; return convert_stem node
80
- elsif transform == 'table'; return convert_table node
81
- elsif transform == 'thematic_break'; return convert_thematic_break node
82
- elsif transform == 'verse'; return convert_verse node
83
- elsif transform == 'video'; return convert_video node
84
- elsif transform == 'document'; return convert_document node
85
- elsif transform == 'toc'; return convert_toc node
86
- elsif transform == 'pass'; return convert_pass node
87
- elsif transform == 'audio'; return convert_audio node
88
- else; return super
51
+ case transform
52
+ when 'inline_quoted' then convert_inline_quoted node
53
+ when 'paragraph' then convert_paragraph node
54
+ when 'inline_anchor' then convert_inline_anchor node
55
+ when 'section' then convert_section node
56
+ when 'listing' then convert_listing node
57
+ when 'literal' then convert_literal node
58
+ when 'ulist' then convert_ulist node
59
+ when 'olist' then convert_olist node
60
+ when 'dlist' then convert_dlist node
61
+ when 'admonition' then convert_admonition node
62
+ when 'colist' then convert_colist node
63
+ when 'embedded' then convert_embedded node
64
+ when 'example' then convert_example node
65
+ when 'floating_title' then convert_floating_title node
66
+ when 'image' then convert_image node
67
+ when 'inline_break' then convert_inline_break node
68
+ when 'inline_button' then convert_inline_button node
69
+ when 'inline_callout' then convert_inline_callout node
70
+ when 'inline_footnote' then convert_inline_footnote node
71
+ when 'inline_image' then convert_inline_image node
72
+ when 'inline_indexterm' then convert_inline_indexterm node
73
+ when 'inline_kbd' then convert_inline_kbd node
74
+ when 'inline_menu' then convert_inline_menu node
75
+ when 'open' then convert_open node
76
+ when 'page_break' then convert_page_break node
77
+ when 'preamble' then convert_preamble node
78
+ when 'quote' then convert_quote node
79
+ when 'sidebar' then convert_sidebar node
80
+ when 'stem' then convert_stem node
81
+ when 'table' then convert_table node
82
+ when 'thematic_break' then convert_thematic_break node
83
+ when 'verse' then convert_verse node
84
+ when 'video' then convert_video node
85
+ when 'document' then convert_document node
86
+ when 'toc' then convert_toc node
87
+ when 'pass' then convert_pass node
88
+ when 'audio' then convert_audio node
89
+ else; super
89
90
  end
90
91
  end
91
92
 
@@ -350,9 +351,10 @@ MathJax.Hub.Register.StartupHook("AsciiMath Jax Ready", function () {
350
351
  stitle = section.captioned_title
351
352
  elsif section.numbered && slevel <= sectnumlevels
352
353
  if slevel < 2 && node.document.doctype == 'book'
353
- if section.sectname == 'chapter'
354
+ case section.sectname
355
+ when 'chapter'
354
356
  stitle = %(#{(signifier = node.document.attributes['chapter-signifier']) ? "#{signifier} " : ''}#{section.sectnum} #{section.title})
355
- elsif section.sectname == 'part'
357
+ when 'part'
356
358
  stitle = %(#{(signifier = node.document.attributes['part-signifier']) ? "#{signifier} " : ''}#{section.sectnum nil, ':'} #{section.title})
357
359
  else
358
360
  stitle = %(#{section.sectnum} #{section.title})
@@ -383,9 +385,10 @@ MathJax.Hub.Register.StartupHook("AsciiMath Jax Ready", function () {
383
385
  title = node.captioned_title
384
386
  elsif node.numbered && level <= (doc_attrs['sectnumlevels'] || 3).to_i
385
387
  if level < 2 && node.document.doctype == 'book'
386
- if node.sectname == 'chapter'
388
+ case node.sectname
389
+ when 'chapter'
387
390
  title = %(#{(signifier = doc_attrs['chapter-signifier']) ? "#{signifier} " : ''}#{node.sectnum} #{node.title})
388
- elsif node.sectname == 'part'
391
+ when 'part'
389
392
  title = %(#{(signifier = doc_attrs['part-signifier']) ? "#{signifier} " : ''}#{node.sectnum nil, ':'} #{node.title})
390
393
  else
391
394
  title = %(#{node.sectnum} #{node.title})
@@ -513,16 +516,16 @@ Your browser does not support the audio tag.
513
516
  result = []
514
517
  id_attribute = node.id ? %( id="#{node.id}") : ''
515
518
 
516
- classes = case node.style
519
+ case node.style
517
520
  when 'qanda'
518
- ['qlist', 'qanda', node.role]
521
+ classes = ['qlist', 'qanda', node.role]
519
522
  when 'horizontal'
520
- ['hdlist', node.role]
523
+ classes = ['hdlist', node.role]
521
524
  else
522
- ['dlist', node.style, node.role]
523
- end.compact
525
+ classes = ['dlist', node.style, node.role]
526
+ end
524
527
 
525
- class_attribute = %( class="#{classes.join ' '}")
528
+ class_attribute = %( class="#{classes.compact.join ' '}")
526
529
 
527
530
  result << %(<div#{id_attribute}#{class_attribute}>)
528
531
  result << %(<div class="title">#{node.title}</div>) if node.title?
@@ -578,12 +581,11 @@ Your browser does not support the audio tag.
578
581
  terms.each do |dt|
579
582
  result << %(<dt#{dt_style_attribute}>#{dt.text}</dt>)
580
583
  end
581
- if dd
582
- result << '<dd>'
583
- result << %(<p>#{dd.text}</p>) if dd.text?
584
- result << dd.content if dd.blocks?
585
- result << '</dd>'
586
- end
584
+ next unless dd
585
+ result << '<dd>'
586
+ result << %(<p>#{dd.text}</p>) if dd.text?
587
+ result << dd.content if dd.blocks?
588
+ result << '</dd>'
587
589
  end
588
590
  result << '</dl>'
589
591
  end
@@ -759,8 +761,8 @@ Your browser does not support the audio tag.
759
761
  logger.error 'partintro block can only be used when doctype is book and must be a child of a book part. Excluding block content.'
760
762
  ''
761
763
  else
762
- id_attr = node.id ? %( id="#{node.id}") : ''
763
- title_el = node.title? ? %(<div class="title">#{node.title}</div>\n) : ''
764
+ id_attr = node.id ? %( id="#{node.id}") : ''
765
+ title_el = node.title? ? %(<div class="title">#{node.title}</div>\n) : ''
764
766
  %(<div#{id_attr} class="openblock#{style && style != 'open' ? " #{style}" : ''}#{(role = node.role) ? " #{role}" : ''}">
765
767
  #{title_el}<div class="content">
766
768
  #{node.content}
@@ -1111,14 +1113,17 @@ Your browser does not support the video tag.
1111
1113
  else
1112
1114
  attrs = node.role ? %( class="#{node.role}") : ''
1113
1115
  unless (text = node.text)
1114
- refid = node.attributes['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, ''
1116
+ if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid = node.attributes['refid']] || (refid.nil_or_empty? ? (top = get_root_document node) : nil))
1117
+ if (@resolving_xref ||= (outer = true)) && outer
1118
+ if (text = ref.xreftext node.attr 'xrefstyle', nil, true)
1119
+ text = text.gsub DropAnchorRx, '' if text.include? '<a'
1120
+ else
1121
+ text = top ? '[^top]' : %([#{refid}])
1122
+ end
1123
+ @resolving_xref = nil
1124
+ else
1125
+ text = top ? '[^top]' : %([#{refid}])
1120
1126
  end
1121
- @resolving_xref = nil
1122
1127
  else
1123
1128
  text = %([#{refid}])
1124
1129
  end
@@ -1186,7 +1191,11 @@ Your browser does not support the video tag.
1186
1191
  img = %([#{node.alt}])
1187
1192
  else
1188
1193
  target = node.target
1189
- attrs = ['width', 'height', 'title'].map {|name| (node.attr? name) ? %( #{name}="#{node.attr name}") : '' }.join
1194
+ attrs = []
1195
+ attrs << %( width="#{node.attr 'width'}") if node.attr? 'width'
1196
+ attrs << %( height="#{node.attr 'height'}") if node.attr? 'height'
1197
+ attrs << %( title="#{node.attr 'title'}") if node.attr? 'title'
1198
+ attrs = attrs.empty? ? '' : attrs.join
1190
1199
  if type != 'icon' && ((node.attr? 'format', 'svg') || (target.include? '.svg')) &&
1191
1200
  node.document.safe < SafeMode::SECURE && ((svg = (node.option? 'inline')) || (obj = (node.option? 'interactive')))
1192
1201
  if svg
@@ -1310,13 +1319,24 @@ Your browser does not support the video tag.
1310
1319
  manname_id_attr = (manname_id = node.attr 'manname-id') ? %( id="#{manname_id}") : ''
1311
1320
  %(<h2#{manname_id_attr}>#{manname_title}</h2>
1312
1321
  <div class="sectionbody">
1313
- <p>#{node.attr 'manname'} - #{node.attr 'manpurpose'}</p>
1322
+ <p>#{(node.attr 'mannames').join ', '} - #{node.attr 'manpurpose'}</p>
1314
1323
  </div>)
1315
1324
  end
1316
1325
 
1326
+ def get_root_document node
1327
+ while (node = node.document).nested?
1328
+ node = node.parent_document
1329
+ end
1330
+ node
1331
+ end
1332
+
1317
1333
  # NOTE adapt to older converters that relied on unprefixed method names
1318
- def method_missing id, *params
1319
- !((name = id.to_s).start_with? 'convert_') && (handles? name) ? (send %(convert_#{name}), *params) : super
1334
+ def method_missing id, *args
1335
+ !((name = id.to_s).start_with? 'convert_') && (handles? name) ? (send %(convert_#{name}), *args) : super
1336
+ end
1337
+
1338
+ def respond_to_missing? id, *options
1339
+ !((name = id.to_s).start_with? 'convert_') && (handles? name)
1320
1340
  end
1321
1341
  end
1322
1342
  end
@@ -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
@@ -18,10 +22,13 @@ class Converter::ManPageConverter < Converter::Base
18
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 = %r(</?(#{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})
@@ -315,8 +316,9 @@ r lw(\n(.lu*75u/100u).'
315
316
  end
316
317
  end
317
318
 
318
- # TODO use Page Control https://www.gnu.org/software/groff/manual/html_node/Page-Control.html#Page-Control
319
- alias convert_page_break skip
319
+ def convert_page_break node
320
+ '.bp'
321
+ end
320
322
 
321
323
  def convert_paragraph node
322
324
  if node.title?
@@ -417,15 +419,7 @@ allbox tab(:);'
417
419
  end
418
420
  row_text[row_index] << %(T{#{LF}.sp#{LF})
419
421
  cell_halign = (cell.attr 'halign', 'left').chr
420
- if tsec == :head
421
- if row_header[row_index].empty? || row_header[row_index][cell_index].empty?
422
- row_header[row_index][cell_index] << %(#{cell_halign}tB)
423
- else
424
- row_header[row_index][cell_index + 1] ||= []
425
- row_header[row_index][cell_index + 1] << %(#{cell_halign}tB)
426
- end
427
- row_text[row_index] << %(#{manify cell.text, whitespace: :normalize}#{LF})
428
- elsif tsec == :body
422
+ if tsec == :body
429
423
  if row_header[row_index].empty? || row_header[row_index][cell_index].empty?
430
424
  row_header[row_index][cell_index] << %(#{cell_halign}t)
431
425
  else
@@ -441,7 +435,7 @@ allbox tab(:);'
441
435
  cell_content = manify cell.content.join, whitespace: :normalize
442
436
  end
443
437
  row_text[row_index] << %(#{cell_content}#{LF})
444
- elsif tsec == :foot
438
+ else # tsec == :head || tsec == :foot
445
439
  if row_header[row_index].empty? || row_header[row_index][cell_index].empty?
446
440
  row_header[row_index][cell_index] << %(#{cell_halign}tB)
447
441
  else
@@ -531,12 +525,13 @@ allbox tab(:);'
531
525
  result.join LF
532
526
  end
533
527
 
534
- # FIXME git uses [verse] for the synopsis; detect this special case
535
528
  def convert_verse node
536
529
  result = []
537
- result << (node.title? ? %(.sp
530
+ if node.title?
531
+ result << %(.sp
538
532
  .B #{manify node.title}
539
- .br) : '.sp')
533
+ .br)
534
+ end
540
535
  attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil
541
536
  attribution_line = (node.attr? 'attribution') ? %[#{attribution_line}\\(em #{node.attr 'attribution'}] : nil
542
537
  result << %(.sp
@@ -584,8 +579,16 @@ allbox tab(:);'
584
579
  %(#{ESC_BS}c#{LF}#{ESC_FS}#{macro} "#{target}" "#{text}" )
585
580
  when :xref
586
581
  unless (text = node.text)
587
- refid = node.attributes['refid']
588
- text = %([#{refid}]) unless AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid]) && (@resolving_xref ||= outer = true) && outer && (text = ref.xreftext node.attr 'xrefstyle', nil, true)
582
+ if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid = node.attributes['refid']] || (refid.nil_or_empty? ? (top = get_root_document node) : nil))
583
+ if (@resolving_xref ||= (outer = true)) && outer && (text = ref.xreftext node.attr 'xrefstyle', nil, true)
584
+ text = uppercase_pcdata text if ref.context === :section && ref.level < 2 && text == ref.title
585
+ else
586
+ text = top ? '[^top]' : %([#{refid}])
587
+ end
588
+ @resolving_xref = nil if outer
589
+ else
590
+ text = %([#{refid}])
591
+ end
589
592
  end
590
593
  text
591
594
  when :ref, :bibref
@@ -602,14 +605,13 @@ allbox tab(:);'
602
605
  end
603
606
 
604
607
  def convert_inline_button node
605
- %(#{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>)
606
609
  end
607
610
 
608
611
  def convert_inline_callout node
609
- %(#{ESC_BS}fB(#{node.text})#{ESC_BS}fP)
612
+ %(<#{ESC_BS}fB>(#{node.text})<#{ESC_BS}fP>)
610
613
  end
611
614
 
612
- # TODO supposedly groff has footnotes, but we're in search of an example
613
615
  def convert_inline_footnote node
614
616
  if (index = node.attr 'index')
615
617
  %([#{index}])
@@ -627,57 +629,68 @@ allbox tab(:);'
627
629
  end
628
630
 
629
631
  def convert_inline_kbd node
630
- if (keys = node.attr 'keys').size == 1
631
- keys[0]
632
- else
633
- keys.join %(#{ESC_BS}0+#{ESC_BS}0)
634
- 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>]
635
633
  end
636
634
 
637
635
  def convert_inline_menu node
638
636
  caret = %[#{ESC_BS}0#{ESC_BS}(fc#{ESC_BS}0]
639
637
  menu = node.attr 'menu'
640
638
  if !(submenus = node.attr 'submenus').empty?
641
- submenu_path = submenus.map {|item| %(#{ESC_BS}fI#{item}#{ESC_BS}fP) }.join caret
642
- %(#{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>)
643
641
  elsif (menuitem = node.attr 'menuitem')
644
- %(#{ESC_BS}fI#{menu}#{caret}#{menuitem}#{ESC_BS}fP)
642
+ %(<#{ESC_BS}fI>#{menu}#{caret}#{menuitem}</#{ESC_BS}fP>)
645
643
  else
646
- %(#{ESC_BS}fI#{menu}#{ESC_BS}fP)
644
+ %(<#{ESC_BS}fI>#{menu}</#{ESC_BS}fP>)
647
645
  end
648
646
  end
649
647
 
650
- # NOTE use fake <BOUNDARY> element to prevent creating artificial word boundaries
648
+ # NOTE use fake XML elements to prevent creating artificial word boundaries
651
649
  def convert_inline_quoted node
652
650
  case node.type
653
651
  when :emphasis
654
- %(#{ESC_BS}fI<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP)
652
+ %(<#{ESC_BS}fI>#{node.text}</#{ESC_BS}fP>)
655
653
  when :strong
656
- %(#{ESC_BS}fB<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP)
654
+ %(<#{ESC_BS}fB>#{node.text}</#{ESC_BS}fP>)
657
655
  when :monospaced
658
- %[#{ESC_BS}f(CR<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP]
656
+ %[<#{ESC_BS}f(CR>#{node.text}</#{ESC_BS}fP>]
659
657
  when :single
660
- %[#{ESC_BS}(oq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(cq]
658
+ %[<#{ESC_BS}(oq>#{node.text}</#{ESC_BS}(cq>]
661
659
  when :double
662
- %[#{ESC_BS}(lq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(rq]
660
+ %[<#{ESC_BS}(lq>#{node.text}</#{ESC_BS}(rq>]
663
661
  else
664
662
  node.text
665
663
  end
666
664
  end
667
665
 
668
666
  def self.write_alternate_pages mannames, manvolnum, target
669
- if mannames && mannames.size > 1
670
- mannames.shift
671
- manvolext = %(.#{manvolnum})
672
- dir, basename = ::File.split target
673
- mannames.each do |manname|
674
- ::File.write ::File.join(dir, %(#{manname}#{manvolext})), %(.so #{basename}), mode: FILE_WRITE_MODE
675
- end
667
+ return unless mannames && mannames.size > 1
668
+ mannames.shift
669
+ manvolext = %(.#{manvolnum})
670
+ dir, basename = ::File.split target
671
+ mannames.each do |manname|
672
+ ::File.write ::File.join(dir, %(#{manname}#{manvolext})), %(.so #{basename}), mode: FILE_WRITE_MODE
676
673
  end
677
674
  end
678
675
 
679
676
  private
680
677
 
678
+ def append_footnotes result, node
679
+ if node.footnotes? && !(node.attr? 'nofootnotes')
680
+ result << '.SH "NOTES"'
681
+ node.footnotes.each do |fn|
682
+ result << %(.IP [#{fn.index}])
683
+ # NOTE restore newline in escaped macro that gets removed by normalize_text in substitutor
684
+ if (text = fn.text).include? %(#{ESC}\\c #{ESC}.)
685
+ text = (manify %(#{text.gsub MalformedEscapedMacroRx, %(\\1#{LF}\\2)} ), whitespace: :normalize).chomp ' '
686
+ else
687
+ text = manify text, whitespace: :normalize
688
+ end
689
+ result << text
690
+ end
691
+ end
692
+ end
693
+
681
694
  # Converts HTML entity references back to their original form, escapes
682
695
  # special man characters and strips trailing whitespace.
683
696
  #
@@ -699,42 +712,56 @@ allbox tab(:);'
699
712
  else
700
713
  str = str.tr_s WHITESPACE, ' '
701
714
  end
702
- str = str.
703
- gsub(LiteralBackslashRx) { $1 ? $& : '\\(rs' }. # literal backslash (not a troff escape sequence)
704
- gsub(EllipsisCharRefRx, '...'). # horizontal ellipsis
705
- gsub(LeadingPeriodRx, '\\\&.'). # leading . is used in troff for macro call or other formatting; replace with \&.
706
- # drop orphaned \c escape lines, unescape troff macro, quote adjacent character, isolate macro line
707
- gsub(EscapedMacroRx) { (rest = $3.lstrip).empty? ? %(.#$1"#$2") : %(.#$1"#$2"#{LF}#{rest}) }.
708
- gsub('-', '\-').
709
- gsub('&lt;', '<').
710
- gsub('&gt;', '>').
711
- gsub('&#160;', '\~'). # non-breaking space
712
- gsub('&#169;', '\(co'). # copyright sign
713
- gsub('&#174;', '\(rg'). # registered sign
714
- gsub('&#8482;', '\(tm'). # trademark sign
715
- gsub('&#8201;', ' '). # thin space
716
- gsub('&#8211;', '\(en'). # en dash
717
- gsub(EmDashCharRefRx, '\(em'). # em dash
718
- gsub('&#8216;', '\(oq'). # left single quotation mark
719
- gsub('&#8217;', '\(cq'). # right single quotation mark
720
- gsub('&#8220;', '\(lq'). # left double quotation mark
721
- gsub('&#8221;', '\(rq'). # right double quotation mark
722
- gsub('&#8592;', '\(<-'). # leftwards arrow
723
- gsub('&#8594;', '\(->'). # rightwards arrow
724
- gsub('&#8656;', '\(lA'). # leftwards double arrow
725
- gsub('&#8658;', '\(rA'). # rightwards double arrow
726
- gsub('&#8203;', '\:'). # zero width space
727
- gsub('&amp;', '&'). # literal ampersand (NOTE must take place after any other replacement that includes &)
728
- gsub('\'', '\(aq'). # apostrophe-quote
729
- gsub(MockBoundaryRx, ''). # mock boundary
730
- gsub(ESC_BS, '\\'). # unescape troff backslash (NOTE update if more escapes are added)
731
- gsub(ESC_FS, '.'). # unescape full stop in troff commands (NOTE must take place after gsub(LeadingPeriodRx))
732
- rstrip # strip trailing space
715
+ str = str
716
+ .gsub(LiteralBackslashRx) { $1 ? $& : '\\(rs' } # literal backslash (not a troff escape sequence)
717
+ .gsub(EllipsisCharRefRx, '...') # horizontal ellipsis
718
+ .gsub(LeadingPeriodRx, '\\\&.') # leading . is used in troff for macro call or other formatting; replace with \&.
719
+ .gsub(EscapedMacroRx) do # drop orphaned \c escape lines, unescape troff macro, quote adjacent character, isolate macro line
720
+ (rest = $3.lstrip).empty? ? %(.#{$1}"#{$2}") : %(.#{$1}"#{$2.rstrip}"#{LF}#{rest})
721
+ end
722
+ .gsub('-', '\-')
723
+ .gsub('&lt;', '<')
724
+ .gsub('&gt;', '>')
725
+ .gsub('&#43;', '+') # plus sign; alternately could use \c(pl
726
+ .gsub('&#160;', '\~') # non-breaking space
727
+ .gsub('&#169;', '\(co') # copyright sign
728
+ .gsub('&#174;', '\(rg') # registered sign
729
+ .gsub('&#8482;', '\(tm') # trademark sign
730
+ .gsub('&#176;', '\(de') # degree sign
731
+ .gsub('&#8201;', ' ') # thin space
732
+ .gsub('&#8211;', '\(en') # en dash
733
+ .gsub(EmDashCharRefRx, '\(em') # em dash
734
+ .gsub('&#8216;', '\(oq') # left single quotation mark
735
+ .gsub('&#8217;', '\(cq') # right single quotation mark
736
+ .gsub('&#8220;', '\(lq') # left double quotation mark
737
+ .gsub('&#8221;', '\(rq') # right double quotation mark
738
+ .gsub('&#8592;', '\(<-') # leftwards arrow
739
+ .gsub('&#8594;', '\(->') # rightwards arrow
740
+ .gsub('&#8656;', '\(lA') # leftwards double arrow
741
+ .gsub('&#8658;', '\(rA') # rightwards double arrow
742
+ .gsub('&#8203;', '\:') # zero width space
743
+ .gsub('&amp;', '&') # literal ampersand (NOTE must take place after any other replacement that includes &)
744
+ .gsub('\'', '\*(Aq') # apostrophe / neutral single quote
745
+ .gsub(MockMacroRx, '\1') # mock boundary
746
+ .gsub(ESC_BS, '\\') # unescape troff backslash (NOTE update if more escapes are added)
747
+ .gsub(ESC_FS, '.') # unescape full stop in troff commands (NOTE must take place after gsub(LeadingPeriodRx))
748
+ .rstrip # strip trailing space
733
749
  opts[:append_newline] ? %(#{str}#{LF}) : str
734
750
  end
735
751
 
752
+ def uppercase_pcdata string
753
+ (XMLMarkupRx.match? string) ? string.gsub(PCDATAFilterRx) { $2 ? $2.upcase : $1 } : string.upcase
754
+ end
755
+
736
756
  def enclose_content node
737
757
  node.content_model == :compound ? node.content : %(.sp#{LF}#{manify node.content, whitespace: :normalize})
738
758
  end
759
+
760
+ def get_root_document node
761
+ while (node = node.document).nested?
762
+ node = node.parent_document
763
+ end
764
+ node
765
+ end
739
766
  end
740
767
  end