asciidoctor 1.5.2 → 1.5.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of asciidoctor might be problematic. Click here for more details.

Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +107 -1
  3. data/LICENSE.adoc +1 -1
  4. data/README.adoc +155 -230
  5. data/Rakefile +2 -1
  6. data/bin/asciidoctor +5 -1
  7. data/data/stylesheets/asciidoctor-default.css +37 -29
  8. data/data/stylesheets/coderay-asciidoctor.css +3 -3
  9. data/features/text_formatting.feature +2 -0
  10. data/lib/asciidoctor.rb +46 -21
  11. data/lib/asciidoctor/abstract_block.rb +14 -8
  12. data/lib/asciidoctor/abstract_node.rb +77 -24
  13. data/lib/asciidoctor/attribute_list.rb +1 -1
  14. data/lib/asciidoctor/block.rb +2 -3
  15. data/lib/asciidoctor/cli/options.rb +14 -15
  16. data/lib/asciidoctor/converter/docbook45.rb +8 -8
  17. data/lib/asciidoctor/converter/docbook5.rb +25 -17
  18. data/lib/asciidoctor/converter/factory.rb +6 -1
  19. data/lib/asciidoctor/converter/html5.rb +159 -117
  20. data/lib/asciidoctor/converter/manpage.rb +671 -0
  21. data/lib/asciidoctor/converter/template.rb +24 -17
  22. data/lib/asciidoctor/document.rb +89 -47
  23. data/lib/asciidoctor/extensions.rb +22 -21
  24. data/lib/asciidoctor/helpers.rb +73 -16
  25. data/lib/asciidoctor/list.rb +26 -5
  26. data/lib/asciidoctor/parser.rb +179 -122
  27. data/lib/asciidoctor/path_resolver.rb +6 -10
  28. data/lib/asciidoctor/reader.rb +37 -34
  29. data/lib/asciidoctor/stylesheets.rb +16 -10
  30. data/lib/asciidoctor/substitutors.rb +98 -21
  31. data/lib/asciidoctor/table.rb +21 -17
  32. data/lib/asciidoctor/timings.rb +3 -3
  33. data/lib/asciidoctor/version.rb +1 -1
  34. data/man/asciidoctor.1 +155 -89
  35. data/man/asciidoctor.adoc +19 -11
  36. data/test/attributes_test.rb +86 -0
  37. data/test/blocks_test.rb +203 -15
  38. data/test/converter_test.rb +15 -2
  39. data/test/document_test.rb +290 -36
  40. data/test/extensions_test.rb +22 -3
  41. data/test/fixtures/circle.svg +8 -0
  42. data/test/fixtures/subs-docinfo.html +2 -0
  43. data/test/fixtures/subs.adoc +7 -0
  44. data/test/invoker_test.rb +25 -0
  45. data/test/links_test.rb +17 -0
  46. data/test/lists_test.rb +173 -0
  47. data/test/options_test.rb +2 -2
  48. data/test/paragraphs_test.rb +2 -2
  49. data/test/parser_test.rb +56 -13
  50. data/test/reader_test.rb +35 -3
  51. data/test/sections_test.rb +59 -0
  52. data/test/substitutions_test.rb +53 -14
  53. data/test/tables_test.rb +158 -2
  54. data/test/test_helper.rb +7 -2
  55. metadata +22 -11
  56. data/benchmark/benchmark.rb +0 -129
  57. data/benchmark/sample-data/mdbasics.adoc +0 -334
  58. data/lib/asciidoctor/opal_ext.rb +0 -26
  59. data/lib/asciidoctor/opal_ext/comparable.rb +0 -38
  60. data/lib/asciidoctor/opal_ext/dir.rb +0 -13
  61. data/lib/asciidoctor/opal_ext/error.rb +0 -2
  62. data/lib/asciidoctor/opal_ext/file.rb +0 -145
@@ -0,0 +1,671 @@
1
+ module Asciidoctor
2
+ # A built-in {Converter} implementation that generates the man page (troff) format.
3
+ #
4
+ # The output follows the groff man page definition while also trying to be
5
+ # consistent with the output produced by the a2x tool from AsciiDoc Python.
6
+ #
7
+ # See http://www.gnu.org/software/groff/manual/html_node/Man-usage.html#Man-usage
8
+ class Converter::ManPageConverter < Converter::BuiltIn
9
+ LF = "\n"
10
+ TAB = "\t"
11
+ ETAB = ' ' * 8
12
+
13
+ # Converts HTML entity references back to their original form, escapes
14
+ # special man characters and strips trailing whitespace.
15
+ #
16
+ # Optional features:
17
+ # * fold each endline into a single space
18
+ # * append a newline
19
+ def manify str, opts = {}
20
+ append_newline = opts[:append_newline]
21
+ preserve_space = opts.fetch :preserve_space, true
22
+ str = preserve_space ? str.gsub(TAB, ETAB) : str.tr_s(%(#{LF}#{TAB} ), ' ')
23
+ str = str.
24
+ gsub(/^\.$/, '\\\&.'). # a lone . is also used in troff to indicate paragraph continuation with visual separator
25
+ gsub(/\\$/, '\\(rs'). # a literal backslash at the end of a line
26
+ gsub(/^\.((?:URL|MTO) ".*?" ".*?" )( |[^\s]*)(.*?)( *)$/, ".\\1\"\\2\"#{LF}\\3"). # quote last URL argument
27
+ gsub(/(?:\A\n|\n *(\n))^\.(URL|MTO) /, "\\1\.\\2 "). # strip blank lines in source that precede a URL
28
+ gsub('-', '\\-').
29
+ gsub('&lt;', '<').
30
+ gsub('&gt;', '>').
31
+ gsub('&#169;', '\\(co'). # copyright sign
32
+ gsub('&#174;', '\\(rg'). # registered sign
33
+ gsub('&#8482;', '\\(tm'). # trademark sign
34
+ gsub('&#8201;', ' '). # thin space
35
+ gsub('&#8211;', '\\(en'). # en-dash
36
+ gsub(/&#8212(?:;&#8203;)?/, '\\(em'). # em-dash
37
+ gsub('&#8216;', '\\(oq'). # left single quotation mark
38
+ gsub('&#8217;', '\\(cq'). # right single quotation mark
39
+ gsub('&#8220;', '\\(lq'). # left double quotation mark
40
+ gsub('&#8221;', '\\(rq'). # right double quotation mark
41
+ gsub(/&#8230;(?:&#8203;)?/, '...'). # horizontal ellipsis
42
+ gsub('&#8592;', '\\(<-'). # leftwards arrow
43
+ gsub('&#8594;', '\\(->'). # rightwards arrow
44
+ gsub('&#8656;', '\\(lA'). # leftwards double arrow
45
+ gsub('&#8658;', '\\(rA'). # rightwards double arrow
46
+ gsub('&#8203;', '\:'). # zero width space
47
+ gsub('\'', '\\(aq'). # apostrophe-quote
48
+ gsub(/<\/?BOUNDARY>/, '').# artificial boundary
49
+ rstrip # strip trailing space
50
+ append_newline ? %(#{str}#{LF}) : str
51
+ end
52
+
53
+ def skip_with_warning node, name = nil
54
+ warn %(asciidoctor: WARNING: converter missing for #{name || node.node_name} node in manpage backend)
55
+ nil
56
+ end
57
+
58
+ def document node
59
+ unless node.attr? 'mantitle'
60
+ raise 'asciidoctor: ERROR: doctype must be set to manpage when using manpage backend'
61
+ end
62
+ mantitle = node.attr 'mantitle'
63
+ manvolnum = node.attr 'manvolnum', '1'
64
+ manname = node.attr 'manname', mantitle
65
+ result = [%('\\" t
66
+ .\\" Title: #{mantitle}
67
+ .\\" Author: #{(node.attr? 'authors') ? (node.attr 'authors') : '[see the "AUTHORS" section]'}
68
+ .\\" Generator: Asciidoctor #{node.attr 'asciidoctor-version'}
69
+ .\\" Date: #{docdate = node.attr 'docdate'}
70
+ .\\" Manual: #{manual = (node.attr? 'manmanual') ? (node.attr 'manmanual') : '\ \&'}
71
+ .\\" Source: #{source = (node.attr? 'mansource') ? (node.attr 'mansource') : '\ \&'}
72
+ .\\" Language: English
73
+ .\\")]
74
+ # TODO add document-level setting to disable capitalization of manname
75
+ result << %(.TH "#{manify manname.upcase}" "#{manvolnum}" "#{docdate}" "#{manify source}" "#{manify manual}")
76
+ # define portability settings
77
+ # see http://bugs.debian.org/507673
78
+ # see http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
79
+ result << '.ie \n(.g .ds Aq \(aq'
80
+ result << '.el .ds Aq \''
81
+ # set sentence_space_size to 0 to prevent extra space between sentences separated by a newline
82
+ # the alternative is to add \& at the end of the line
83
+ result << '.ss \n[.ss] 0'
84
+ # disable hyphenation
85
+ result << '.nh'
86
+ # disable justification (adjust text to left margin only)
87
+ result << '.ad l'
88
+ # define URL macro for portability
89
+ # see http://web.archive.org/web/20060102165607/http://people.debian.org/~branden/talks/wtfm/wtfm.pdf
90
+ #
91
+ # Use: .URL "http://www.debian.org" "Debian" "."
92
+ #
93
+ # * First argument: the URL
94
+ # * Second argument: text to be hyperlinked
95
+ # * Third (optional) argument: text that needs to immediately trail
96
+ # the hyperlink without intervening whitespace
97
+ result << '.de URL
98
+ \\\\$2 \\(laURL: \\\\$1 \\(ra\\\\$3
99
+ ..
100
+ .if \n[.g] .mso www.tmac
101
+ .LINKSTYLE blue R < >'
102
+
103
+ unless node.noheader
104
+ if node.attr? 'manpurpose'
105
+ result << %(.SH "#{node.attr 'manname-title'}"
106
+ #{manify mantitle} \\- #{manify node.attr 'manpurpose'})
107
+ end
108
+ end
109
+
110
+ result << node.content
111
+
112
+ # QUESTION should NOTES come after AUTHOR(S)?
113
+ if node.footnotes? && !(node.attr? 'nofootnotes')
114
+ result << '.SH "NOTES"'
115
+ result.concat(node.footnotes.map {|fn| %(#{fn.index}. #{fn.text}) })
116
+ end
117
+
118
+ # FIXME detect single author and use appropriate heading; itemize the authors if multiple
119
+ if node.attr? 'authors'
120
+ result << %(.SH "AUTHOR(S)"
121
+ .sp
122
+ \\fB#{node.attr 'authors'}\\fP
123
+ .RS 4
124
+ Author(s).
125
+ .RE)
126
+ end
127
+
128
+ result * LF
129
+ end
130
+
131
+ # NOTE embedded doesn't really make sense in the manpage backend
132
+ def embedded node
133
+ result = [node.content]
134
+
135
+ if node.footnotes? && !(node.attr? 'nofootnotes')
136
+ result << '.SH "NOTES"'
137
+ result.concat(node.footnotes.map {|fn| %(#{fn.index}. #{fn.text}) })
138
+ end
139
+
140
+ # QUESTION should we add an AUTHOR(S) section?
141
+
142
+ result * LF
143
+ end
144
+
145
+ def section node
146
+ slevel = node.level
147
+ # QUESTION should the check for slevel be done in section?
148
+ slevel = 1 if slevel == 0 && node.special
149
+ result = []
150
+ if slevel > 1
151
+ macro = 'SS'
152
+ # QUESTION why captioned title? why not for slevel == 1?
153
+ stitle = node.captioned_title
154
+ else
155
+ macro = 'SH'
156
+ stitle = node.title.upcase
157
+ end
158
+ result << %(.#{macro} "#{manify stitle}"
159
+ #{node.content})
160
+ result * LF
161
+ end
162
+
163
+ def admonition node
164
+ result = []
165
+ result << %(.if n \\{\\
166
+ .sp
167
+ .\\}
168
+ .RS 4
169
+ .it 1 an-trap
170
+ .nr an-no-space-flag 1
171
+ .nr an-break-flag 1
172
+ .br
173
+ .ps +1
174
+ .B #{node.caption}#{node.title? ? "\\fP #{manify node.title}" : nil}
175
+ .ps -1
176
+ .br
177
+ #{resolve_content node}
178
+ .sp .5v
179
+ .RE)
180
+ result * LF
181
+ end
182
+
183
+ alias :audio :skip_with_warning
184
+
185
+ def colist node
186
+ result = []
187
+ result << %(.sp
188
+ .B #{manify node.title}
189
+ .br) if node.title?
190
+ result << %(.TS
191
+ tab\(:\);
192
+ r lw\(\\n\(.lu*75u/100u\).)
193
+
194
+ node.items.each_with_index do |item, index|
195
+ result << %(\\fB(#{index + 1})\\fP\\h'-2n':T{
196
+ #{manify item.text}
197
+ T})
198
+ end
199
+ result << '.TE'
200
+ result * LF
201
+ end
202
+
203
+ # TODO implement title for dlist
204
+ # TODO implement horizontal (if it makes sense)
205
+ def dlist node
206
+ result = []
207
+ counter = 0
208
+ node.items.each do |terms, dd|
209
+ counter += 1
210
+ case node.style
211
+ when 'qanda'
212
+ result << %(.sp
213
+ #{counter}. #{manify([*terms].map {|dt| dt.text }.join ' ')}
214
+ .RS 4)
215
+ else
216
+ result << %(.sp
217
+ #{manify([*terms].map {|dt| dt.text }.join ', ')}
218
+ .RS 4)
219
+ end
220
+ if dd
221
+ result << (manify dd.text) if dd.text?
222
+ result << dd.content if dd.blocks?
223
+ end
224
+ result << '.RE'
225
+ end
226
+ result * LF
227
+ end
228
+
229
+ def example node
230
+ result = []
231
+ result << %(.sp
232
+ .B #{manify node.captioned_title}
233
+ .br) if node.title?
234
+ result << %(.RS 4
235
+ #{resolve_content node}
236
+ .RE)
237
+ result * LF
238
+ end
239
+
240
+ def floating_title node
241
+ %(.SS "#{manify node.title}")
242
+ end
243
+
244
+ alias :image :skip_with_warning
245
+
246
+ def listing node
247
+ result = []
248
+ result << %(.sp
249
+ .B #{manify node.captioned_title}
250
+ .br) if node.title?
251
+ result << %(.sp
252
+ .if n \\{\\
253
+ .RS 4
254
+ .\\}
255
+ .nf
256
+ #{manify node.content}
257
+ .fi
258
+ .if n \\{\\
259
+ .RE
260
+ .\\})
261
+ result * LF
262
+ end
263
+
264
+ def literal node
265
+ result = []
266
+ result << %(.sp
267
+ .B #{manify node.title}
268
+ .br) if node.title?
269
+ result << %(.sp
270
+ .if n \\{\\
271
+ .RS 4
272
+ .\\}
273
+ .nf
274
+ #{manify node.content}
275
+ .fi
276
+ .if n \\{\\
277
+ .RE
278
+ .\\})
279
+ result * LF
280
+ end
281
+
282
+ def olist node
283
+ result = []
284
+ result << %(.sp
285
+ .B #{manify node.title}
286
+ .br) if node.title?
287
+
288
+ node.items.each_with_index do |item, idx|
289
+ result << %(.sp
290
+ .RS 4
291
+ .ie n \\{\\
292
+ \\h'-04' #{idx + 1}.\\h'+01'\\c
293
+ .\\}
294
+ .el \\{\\
295
+ .sp -1
296
+ .IP " #{idx + 1}." 4.2
297
+ .\\}
298
+ #{manify item.text})
299
+ result << item.content if item.blocks?
300
+ result << '.RE'
301
+ end
302
+ result * LF
303
+ end
304
+
305
+ def open node
306
+ case node.style
307
+ when 'abstract', 'partintro'
308
+ resolve_content node
309
+ else
310
+ node.content
311
+ end
312
+ end
313
+
314
+ # TODO use Page Control https://www.gnu.org/software/groff/manual/html_node/Page-Control.html#Page-Control
315
+ alias :page_break :skip
316
+
317
+ def paragraph node
318
+ if node.title?
319
+ %(.sp
320
+ .B #{manify node.title}
321
+ .br
322
+ #{manify node.content})
323
+ else
324
+ %(.sp
325
+ #{manify node.content})
326
+ end
327
+ end
328
+
329
+ alias :preamble :content
330
+
331
+ def quote node
332
+ result = []
333
+ if node.title?
334
+ result << %(.sp
335
+ .in +.3i
336
+ .B #{manify node.title}
337
+ .br
338
+ .in)
339
+ end
340
+ attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil
341
+ attribution_line = (node.attr? 'attribution') ? %(#{attribution_line}\\\(em #{node.attr 'attribution'}) : nil
342
+ result << %(.in +.3i
343
+ .ll -.3i
344
+ .nf
345
+ #{resolve_content node}
346
+ .fi
347
+ .br
348
+ .in
349
+ .ll)
350
+ if attribution_line
351
+ result << %(.in +.5i
352
+ .ll -.5i
353
+ #{attribution_line}
354
+ .in
355
+ .ll)
356
+ end
357
+ result * LF
358
+ end
359
+
360
+ alias :sidebar :skip_with_warning
361
+
362
+ def stem node
363
+ title_element = node.title? ? %(.sp
364
+ .B #{manify node.title}
365
+ .br) : nil
366
+ open, close = BLOCK_MATH_DELIMITERS[node.style.to_sym]
367
+
368
+ unless ((equation = node.content).start_with? open) && (equation.end_with? close)
369
+ equation = %(#{open}#{equation}#{close})
370
+ end
371
+
372
+ %(#{title_element}#{equation})
373
+ end
374
+
375
+ # FIXME: The reason this method is so complicated is because we are not
376
+ # receiving empty(marked) cells when there are colspans or rowspans. This
377
+ # method has to create a map of all cells and in the case of rowspans
378
+ # create empty cells as placeholders of the span.
379
+ # To fix this, asciidoctor needs to provide an API to tell the user if a
380
+ # given cell is being used as a colspan or rowspan.
381
+ def table node
382
+ result = []
383
+ if node.title?
384
+ result << %(.sp
385
+ .it 1 an-trap
386
+ .nr an-no-space-flag 1
387
+ .nr an-break-flag 1
388
+ .br
389
+ .B #{manify node.captioned_title})
390
+ end
391
+ result << '.TS
392
+ allbox tab(:);'
393
+ row_header = []
394
+ row_text = []
395
+ row_index = 0
396
+ [:head, :body, :foot].each do |tsec|
397
+ node.rows[tsec].each do |row|
398
+ row_header[row_index] ||= []
399
+ row_text[row_index] ||= []
400
+ # result << LF
401
+ # l left-adjusted
402
+ # r right-adjusted
403
+ # c centered-adjusted
404
+ # n numerical align
405
+ # a alphabetic align
406
+ # s spanned
407
+ # ^ vertically spanned
408
+ remaining_cells = row.size
409
+ row.each_with_index do |cell, cell_index|
410
+ remaining_cells -= 1
411
+ row_header[row_index][cell_index] ||= []
412
+ # Add an empty cell if this is a rowspan cell
413
+ if row_header[row_index][cell_index] == ['^t']
414
+ row_text[row_index] << %(T{#{LF}.sp#{LF}T}:)
415
+ end
416
+ row_text[row_index] << %(T{#{LF}.sp#{LF})
417
+ cell_halign = (cell.attr 'halign', 'left')[0..0]
418
+ if tsec == :head
419
+ if row_header[row_index].empty? ||
420
+ row_header[row_index][cell_index].empty?
421
+ row_header[row_index][cell_index] << %(#{cell_halign}tB)
422
+ else
423
+ row_header[row_index][cell_index + 1] ||= []
424
+ row_header[row_index][cell_index + 1] << %(#{cell_halign}tB)
425
+ end
426
+ row_text[row_index] << %(#{cell.text}#{LF})
427
+ elsif tsec == :body
428
+ if row_header[row_index].empty? ||
429
+ row_header[row_index][cell_index].empty?
430
+ row_header[row_index][cell_index] << %(#{cell_halign}t)
431
+ else
432
+ row_header[row_index][cell_index + 1] ||= []
433
+ row_header[row_index][cell_index + 1] << %(#{cell_halign}t)
434
+ end
435
+ case cell.style
436
+ when :asciidoc
437
+ cell_content = cell.content
438
+ when :verse, :literal
439
+ cell_content = cell.text
440
+ else
441
+ cell_content = cell.content.join
442
+ end
443
+ row_text[row_index] << %(#{cell_content}#{LF})
444
+ elsif tsec == :foot
445
+ if row_header[row_index].empty? ||
446
+ row_header[row_index][cell_index].empty?
447
+ row_header[row_index][cell_index] << %(#{cell_halign}tB)
448
+ else
449
+ row_header[row_index][cell_index + 1] ||= []
450
+ row_header[row_index][cell_index + 1] << %(#{cell_halign}tB)
451
+ end
452
+ row_text[row_index] << %(#{cell.text}#{LF})
453
+ end
454
+ if cell.colspan && cell.colspan > 1
455
+ (cell.colspan - 1).times do |i|
456
+ if row_header[row_index].empty? ||
457
+ row_header[row_index][cell_index].empty?
458
+ row_header[row_index][cell_index + i] << 'st'
459
+ else
460
+ row_header[row_index][cell_index + 1 + i] ||= []
461
+ row_header[row_index][cell_index + 1 + i] << 'st'
462
+ end
463
+ end
464
+ end
465
+ if cell.rowspan && cell.rowspan > 1
466
+ (cell.rowspan - 1).times do |i|
467
+ row_header[row_index + 1 + i] ||= []
468
+ if row_header[row_index + 1 + i].empty? ||
469
+ row_header[row_index + 1 + i][cell_index].empty?
470
+ row_header[row_index + 1 + i][cell_index] ||= []
471
+ row_header[row_index + 1 + i][cell_index] << '^t'
472
+ else
473
+ row_header[row_index + 1 + i][cell_index + 1] ||= []
474
+ row_header[row_index + 1 + i][cell_index + 1] << '^t'
475
+ end
476
+ end
477
+ end
478
+ if remaining_cells >= 1
479
+ row_text[row_index] << 'T}:'
480
+ else
481
+ row_text[row_index] << %(T}#{LF})
482
+ end
483
+ end
484
+ row_index += 1
485
+ end
486
+ end
487
+
488
+ #row_header.each do |row|
489
+ # result << LF
490
+ # row.each_with_index do |cell, i|
491
+ # result << (cell.join ' ')
492
+ # result << ' ' if row.size > i + 1
493
+ # end
494
+ #end
495
+ # FIXME temporary fix to get basic table to display
496
+ result << LF
497
+ result << row_header.first.map {|r| 'lt'}.join(' ')
498
+
499
+ result << %(.#{LF})
500
+ row_text.each do |row|
501
+ result << row.join
502
+ end
503
+ result << %(.TE#{LF}.sp)
504
+ result.join
505
+ end
506
+
507
+ def thematic_break node
508
+ '.sp
509
+ .ce
510
+ \l\'\n(.lu*25u/100u\(ap\''
511
+ end
512
+
513
+ alias :toc :skip
514
+
515
+ def ulist node
516
+ result = []
517
+ result << %(.sp
518
+ .B #{manify node.title}
519
+ .br) if node.title?
520
+ node.items.map {|item|
521
+ result << %[.sp
522
+ .RS 4
523
+ .ie n \\{\\
524
+ \\h'-04'\\(bu\\h'+03'\\c
525
+ .\\}
526
+ .el \\{\\
527
+ .sp -1
528
+ .IP \\(bu 2.3
529
+ .\\}
530
+ #{manify item.text}]
531
+ result << item.content if item.blocks?
532
+ result << '.RE'
533
+ }
534
+ result * LF
535
+ end
536
+
537
+ # FIXME git uses [verse] for the synopsis; detect this special case
538
+ def verse node
539
+ result = []
540
+ if node.title?
541
+ result << %(.sp
542
+ .B #{manify node.title}
543
+ .br)
544
+ end
545
+ attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil
546
+ attribution_line = (node.attr? 'attribution') ? %(#{attribution_line}\\\(em #{node.attr 'attribution'}) : nil
547
+ result << %(.sp
548
+ .nf
549
+ #{manify node.content}
550
+ .fi
551
+ .br)
552
+ if attribution_line
553
+ result << %(.in +.5i
554
+ .ll -.5i
555
+ #{attribution_line}
556
+ .in
557
+ .ll)
558
+ end
559
+ result * LF
560
+ end
561
+
562
+ def video node
563
+ start_param = (node.attr? 'start', nil, false) ? %(&start=#{node.attr 'start'}) : nil
564
+ end_param = (node.attr? 'end', nil, false) ? %(&end=#{node.attr 'end'}) : nil
565
+ %(.sp
566
+ #{manify node.captioned_title} (video) <#{node.media_uri(node.attr 'target')}#{start_param}#{end_param}>)
567
+ end
568
+
569
+ def inline_anchor node
570
+ target = node.target
571
+ case node.type
572
+ when :link
573
+ if (text = node.text) == target
574
+ text = nil
575
+ else
576
+ text = text.gsub '"', '\\(dq'
577
+ end
578
+ if target.start_with? 'mailto:'
579
+ macro = 'MTO'
580
+ target = target[7..-1].sub('@', '\\(at')
581
+ else
582
+ macro = 'URL'
583
+ end
584
+ %(#{LF}.#{macro} "#{target}" "#{text}" )
585
+ when :xref
586
+ refid = (node.attr 'refid') || target
587
+ node.text || (node.document.references[:ids][refid] || %([#{refid}]))
588
+ when :ref, :bibref
589
+ # These are anchor points, which shouldn't be visual
590
+ ''
591
+ else
592
+ warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect})
593
+ end
594
+ end
595
+
596
+ def inline_break node
597
+ %(#{node.text}
598
+ .br)
599
+ end
600
+
601
+ def inline_button node
602
+ %(\\fB[\\ #{node.text}\\ ]\\fP)
603
+ end
604
+
605
+ def inline_callout node
606
+ %[\\fB(#{node.text})\\fP]
607
+ end
608
+
609
+ # TODO supposedly groff has footnotes, but we're in search of an example
610
+ def inline_footnote node
611
+ if (index = node.attr 'index')
612
+ %([#{index}])
613
+ elsif node.type == :xref
614
+ %([#{node.text}])
615
+ end
616
+ end
617
+
618
+ def inline_image node
619
+ # NOTE alt should always be set
620
+ alt_text = (node.attr? 'alt') ? (node.attr 'alt') : node.target
621
+ (node.attr? 'link') ? %([#{alt_text}] <#{node.attr 'link'}>) : %([#{alt_text}])
622
+ end
623
+
624
+ def inline_indexterm node
625
+ node.type == :visible ? node.text : ''
626
+ end
627
+
628
+ def inline_kbd node
629
+ if (keys = node.attr 'keys').size == 1
630
+ keys[0]
631
+ else
632
+ keys.join '\ +\ '
633
+ end
634
+ end
635
+
636
+ def inline_menu node
637
+ caret = '\ \(fc\ '
638
+ menu = node.attr 'menu'
639
+ if !(submenus = node.attr 'submenus').empty?
640
+ submenu_path = submenus.map {|item| %(\\fI#{item}\\fP) }.join caret
641
+ %(\\fI#{menu}\\fP#{caret}#{submenu_path}#{caret}\\fI#{node.attr 'menuitem'}\\fP)
642
+ elsif (menuitem = node.attr 'menuitem')
643
+ %(\\fI#{menu}#{caret}#{menuitem}\\fP)
644
+ else
645
+ %(\\fI#{menu}\\fP)
646
+ end
647
+ end
648
+
649
+ # NOTE use fake <BOUNDARY> element to prevent creating artificial word boundaries
650
+ def inline_quoted node
651
+ case node.type
652
+ when :emphasis
653
+ %[\\fI<BOUNDARY>#{node.text}</BOUNDARY>\\fP]
654
+ when :strong
655
+ %[\\fB<BOUNDARY>#{node.text}</BOUNDARY>\\fP]
656
+ when :monospaced
657
+ %[\\f[CR]<BOUNDARY>#{node.text}</BOUNDARY>\\fP]
658
+ when :single
659
+ %[\\(oq<BOUNDARY>#{node.text}</BOUNDARY>\\(cq]
660
+ when :double
661
+ %[\\(lq<BOUNDARY>#{node.text}</BOUNDARY>\\(rq]
662
+ else
663
+ node.text
664
+ end
665
+ end
666
+
667
+ def resolve_content node
668
+ node.content_model == :compound ? node.content : %(.sp#{LF}#{manify node.content})
669
+ end
670
+ end
671
+ end