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
@@ -90,7 +90,7 @@ class AbstractNode
90
90
  # return the value of the attribute or the default value if the attribute
91
91
  # is not found in the attributes of this node or the document node
92
92
  def attr(name, default_value = nil, inherit = true)
93
- name = name.to_s if name.is_a?(::Symbol)
93
+ name = name.to_s if ::Symbol === name
94
94
  inherit = false if self == @document
95
95
  if inherit
96
96
  @attributes[name] || @document.attributes[name] || default_value
@@ -117,7 +117,7 @@ class AbstractNode
117
117
  # comparison value is specified, whether the value of the attribute matches
118
118
  # the comparison value
119
119
  def attr?(name, expect = nil, inherit = true)
120
- name = name.to_s if name.is_a?(::Symbol)
120
+ name = name.to_s if ::Symbol === name
121
121
  inherit = false if self == @document
122
122
  if expect.nil?
123
123
  @attributes.has_key?(name) || (inherit && @document.attributes.has_key?(name))
@@ -130,23 +130,18 @@ class AbstractNode
130
130
 
131
131
  # Public: Assign the value to the attribute name for the current node.
132
132
  #
133
- # name - The String attribute name
134
- # value - The Object value to assign to the attribute name
133
+ # name - The String attribute name to assign
134
+ # value - The Object value to assign to the attribute
135
135
  # overwrite - A Boolean indicating whether to assign the attribute
136
- # if currently present in the attributes Hash
136
+ # if currently present in the attributes Hash (default: true)
137
137
  #
138
138
  # Returns a [Boolean] indicating whether the assignment was performed
139
- def set_attr name, value, overwrite = nil
140
- if overwrite.nil?
139
+ def set_attr name, value, overwrite = true
140
+ if overwrite == false && (@attributes.key? name)
141
+ false
142
+ else
141
143
  @attributes[name] = value
142
144
  true
143
- else
144
- if overwrite || !(@attributes.key? name)
145
- @attributes[name] = value
146
- true
147
- else
148
- false
149
- end
150
145
  end
151
146
  end
152
147
 
@@ -226,6 +221,21 @@ class AbstractNode
226
221
  end
227
222
  end
228
223
 
224
+ # Public: A convenience method that adds the given role directly to this node
225
+ def add_role(name)
226
+ unless (roles = (@attributes['role'] || '').split(' ')).include? name
227
+ @attributes['role'] = roles.push(name) * ' '
228
+ end
229
+ end
230
+
231
+ # Public: A convenience method that removes the given role directly from this node
232
+ def remove_role(name)
233
+ if (roles = (@attributes['role'] || '').split(' ')).include? name
234
+ roles.delete name
235
+ @attributes['role'] = roles * ' '
236
+ end
237
+ end
238
+
229
239
  # Public: A convenience method that checks if the reftext attribute is specified
230
240
  def reftext?
231
241
  @attributes.has_key?('reftext') || @document.attributes.has_key?('reftext')
@@ -301,8 +311,8 @@ class AbstractNode
301
311
  # Returns A String reference or data URI for the target image
302
312
  def image_uri(target_image, asset_dir_key = 'imagesdir')
303
313
  if (doc = @document).safe < SafeMode::SECURE && doc.attr?('data-uri')
304
- if is_uri?(target_image) ||
305
- (asset_dir_key && (images_base = doc.attr(asset_dir_key)) && is_uri?(images_base) &&
314
+ if (Helpers.uriish? target_image) ||
315
+ (asset_dir_key && (images_base = doc.attr(asset_dir_key)) && (Helpers.uriish? images_base) &&
306
316
  (target_image = normalize_web_path(target_image, images_base, false)))
307
317
  if doc.attr?('allow-uri-read')
308
318
  generate_data_uri_from_uri target_image, doc.attr?('cache-uri')
@@ -330,8 +340,9 @@ class AbstractNode
330
340
  #
331
341
  # Returns A String data URI containing the content of the target image
332
342
  def generate_data_uri(target_image, asset_dir_key = nil)
333
- ext = ::File.extname(target_image)[1..-1]
334
- mimetype = (ext == 'svg' ? 'image/svg+xml' : %(image/#{ext}))
343
+ ext = ::File.extname target_image
344
+ # QUESTION what if ext is empty?
345
+ mimetype = (ext == '.svg' ? 'image/svg+xml' : %(image/#{ext[1..-1]}))
335
346
  if asset_dir_key
336
347
  image_path = normalize_system_path(target_image, @document.attr(asset_dir_key), nil, :target_name => 'image')
337
348
  else
@@ -352,6 +363,7 @@ class AbstractNode
352
363
  else
353
364
  bindata = ::File.open(image_path, 'rb') {|file| file.read }
354
365
  end
366
+ # NOTE base64 is autoloaded by reference to ::Base64
355
367
  %(data:#{mimetype};base64,#{::Base64.encode64(bindata).delete EOL})
356
368
  end
357
369
 
@@ -368,7 +380,6 @@ class AbstractNode
368
380
  # Returns A data URI string built from Base64 encoded data read from the URI
369
381
  # and the mime type specified in the Content Type header.
370
382
  def generate_data_uri_from_uri image_uri, cache_uri = false
371
- Helpers.require_library 'base64'
372
383
  if cache_uri
373
384
  # caching requires the open-uri-cached gem to be installed
374
385
  # processing will be automatically aborted if these libraries can't be opened
@@ -384,7 +395,8 @@ class AbstractNode
384
395
  mimetype = file.content_type
385
396
  file.read
386
397
  }
387
- %(data:#{mimetype};base64,#{Base64.encode64(bindata).delete EOL})
398
+ # NOTE base64 is autoloaded by reference to ::Base64
399
+ %(data:#{mimetype};base64,#{::Base64.encode64(bindata).delete EOL})
388
400
  rescue
389
401
  warn %(asciidoctor: WARNING: could not retrieve image data from URI: #{image_uri})
390
402
  image_uri
@@ -395,6 +407,46 @@ class AbstractNode
395
407
  end
396
408
  end
397
409
 
410
+ # Public: Resolve the URI or system path to the specified target, then read and return its contents
411
+ #
412
+ # The URI or system path of the target is first resolved. If the resolved path is a URI, read the
413
+ # contents from the URI if the allow-uri-read attribute is set, enabling caching if the cache-uri
414
+ # attribute is also set. If the resolved path is not a URI, read the contents of the file from the
415
+ # file system. If the normalize option is set, the data will be normalized.
416
+ #
417
+ # target - The URI or local path from which to read the data.
418
+ # opts - a Hash of options to control processing (default: {})
419
+ # * :label the String label of the target to use in warning messages (default: 'asset')
420
+ # * :normalize a Boolean that indicates whether the data should be normalized (default: false)
421
+ # * :start the String relative base path to use when resolving the target (default: nil)
422
+ # * :warn_on_failure a Boolean that indicates whether warnings are issued if the target cannot be read (default: true)
423
+ # Returns the contents of the resolved target or nil if the resolved target cannot be read
424
+ # --
425
+ # TODO refactor other methods in this class to use this method were possible (repurposing if necessary)
426
+ def read_contents target, opts = {}
427
+ doc = @document
428
+ if (Helpers.uriish? target) || ((start = opts[:start]) && (Helpers.uriish? start) &&
429
+ (target = (@path_resolver ||= PathResolver.new).web_path target, start))
430
+ if doc.attr? 'allow-uri-read'
431
+ Helpers.require_library 'open-uri/cached', 'open-uri-cached' if doc.attr? 'cache-uri'
432
+ begin
433
+ data = ::OpenURI.open_uri(target) {|fd| fd.read }
434
+ data = (Helpers.normalize_lines_from_string data) * EOL if opts[:normalize]
435
+ rescue
436
+ warn %(asciidoctor: WARNING: could not retrieve contents of #{opts[:label] || 'asset'} at URI: #{target}) if opts.fetch :warn_on_failure, true
437
+ data = nil
438
+ end
439
+ else
440
+ warn %(asciidoctor: WARNING: cannot retrieve contents of #{opts[:label] || 'asset'} at URI: #{target} (allow-uri-read attribute not enabled)) if opts.fetch :warn_on_failure, true
441
+ data = nil
442
+ end
443
+ else
444
+ target = normalize_system_path target, opts[:start], nil, :target_name => (opts[:label] || 'asset')
445
+ data = read_asset target, :normalize => opts[:normalize], :warn_on_failure => (opts.fetch :warn_on_failure, true)
446
+ end
447
+ data
448
+ end
449
+
398
450
  # Public: Read the contents of the file at the specified path.
399
451
  # This method assumes that the path is safe to read. It checks
400
452
  # that the file is readable before attempting to read it.
@@ -413,9 +465,9 @@ class AbstractNode
413
465
  opts = { :warn_on_failure => (opts != false) } unless ::Hash === opts
414
466
  if ::File.readable? path
415
467
  if opts[:normalize]
416
- # QUESTION should we strip content?
417
468
  Helpers.normalize_lines_from_string(::IO.read(path)) * EOL
418
469
  else
470
+ # QUESTION should we chomp or rstrip content?
419
471
  ::IO.read(path)
420
472
  end
421
473
  else
@@ -434,7 +486,7 @@ class AbstractNode
434
486
  #
435
487
  # Returns the resolved [String] path
436
488
  def normalize_web_path(target, start = nil, preserve_uri_target = true)
437
- if preserve_uri_target && is_uri?(target)
489
+ if preserve_uri_target && (Helpers.uriish? target)
438
490
  target
439
491
  else
440
492
  (@path_resolver ||= PathResolver.new).web_path target, start
@@ -497,8 +549,10 @@ class AbstractNode
497
549
 
498
550
  # Public: Check whether the specified String is a URI by
499
551
  # matching it against the Asciidoctor::UriSniffRx regex.
552
+ #
553
+ # @deprecated Use Helpers.uriish? instead
500
554
  def is_uri? str
501
- str.include?(':') && UriSniffRx =~ str
555
+ Helpers.uriish? str
502
556
  end
503
557
 
504
558
  # Public: Retrieve the list marker keyword for the specified list type.
@@ -511,6 +565,5 @@ class AbstractNode
511
565
  def list_marker_keyword(list_type = nil)
512
566
  ORDERED_LIST_KEYWORDS[list_type || @style]
513
567
  end
514
-
515
568
  end
516
569
  end
@@ -163,7 +163,7 @@ class AttributeList
163
163
  case name
164
164
  when 'options', 'opts'
165
165
  name = 'options'
166
- value.split(',').each {|o| @attributes[%(#{o.strip}-option)] = '' }
166
+ value.tr(' ', '').split(',').each {|opt| @attributes[%(#{opt}-option)] = '' }
167
167
  @attributes[name] = value
168
168
  when 'title'
169
169
  @attributes[name] = value
@@ -9,7 +9,7 @@ module Asciidoctor
9
9
  # => "<em>This</em> is a &lt;test&gt;"
10
10
  class Block < AbstractBlock
11
11
 
12
- DEFAULT_CONTENT_MODEL = {
12
+ (DEFAULT_CONTENT_MODEL = {
13
13
  # TODO should probably fill in all known blocks
14
14
  :audio => :empty,
15
15
  :image => :empty,
@@ -21,8 +21,7 @@ class Block < AbstractBlock
21
21
  :pass => :raw,
22
22
  :thematic_break => :empty,
23
23
  :video => :empty
24
- }
25
- DEFAULT_CONTENT_MODEL.default = :simple
24
+ }).default = :simple
26
25
 
27
26
  # Public: Create alias for context to be consistent w/ AsciiDoc
28
27
  alias :blockname :context
@@ -43,7 +43,8 @@ Example: asciidoctor -b html5 source.asciidoc
43
43
 
44
44
  EOS
45
45
 
46
- opts.on('-b', '--backend BACKEND', 'set output format backend (default: html5)') do |backend|
46
+ opts.on('-b', '--backend BACKEND', 'set output format backend: [html5, xhtml5, docbook5, docbook45, manpage] (default: html5)',
47
+ 'additional backends are supported via extensions (e.g., pdf, latex)') do |backend|
47
48
  self[:attributes]['backend'] = backend
48
49
  end
49
50
  opts.on('-d', '--doctype DOCTYPE', ['article', 'book', 'manpage', 'inline'],
@@ -76,24 +77,22 @@ Example: asciidoctor -b html5 source.asciidoc
76
77
  end
77
78
  opts.on('-C', '--compact', 'compact the output by removing blank lines. (No longer in use)') do
78
79
  end
79
- opts.on('-a', '--attribute key[=value],key2[=value2],...', ::Array,
80
- 'a list of document attributes to set in the form of key, key! or key=value pair',
81
- 'unless @ is appended to the value, these attributes take precedence over attributes',
82
- 'defined in the source document') do |attribs|
83
- attribs.each do |attrib|
84
- key, val = attrib.split '=', 2
85
- # move leading ! to end for internal processing
86
- #if val.nil? && key.start_with?('!')
87
- # key = "#{key[1..-1]}!"
88
- #end
89
- self[:attributes][key] = val || ''
90
- end
80
+ opts.on('-a', '--attribute key[=value]', 'a document attribute to set in the form of key, key! or key=value pair',
81
+ 'unless @ is appended to the value, this attributes takes precedence over attributes',
82
+ 'defined in the source document') do |attr|
83
+ key, val = attr.split '=', 2
84
+ val = val ? (FORCE_ENCODING ? (val.force_encoding ::Encoding::UTF_8) : val) : ''
85
+ # move leading ! to end for internal processing
86
+ #if !val && key.start_with?('!')
87
+ # key = "#{key[1..-1]}!"
88
+ #end
89
+ self[:attributes][key] = val
91
90
  end
92
91
  opts.on('-T', '--template-dir DIR', 'a directory containing custom converter templates that override the built-in converter (requires tilt gem)',
93
92
  'may be specified multiple times') do |template_dir|
94
93
  if self[:template_dirs].nil?
95
94
  self[:template_dirs] = [template_dir]
96
- elsif self[:template_dirs].is_a? ::Array
95
+ elsif ::Array === self[:template_dirs]
97
96
  self[:template_dirs].push template_dir
98
97
  else
99
98
  self[:template_dirs] = [self[:template_dirs], template_dir]
@@ -108,7 +107,7 @@ Example: asciidoctor -b html5 source.asciidoc
108
107
  opts.on('-D', '--destination-dir DIR', 'destination output directory (default: directory of source file)') do |dest_dir|
109
108
  self[:destination_dir] = dest_dir
110
109
  end
111
- opts.on('-IDIRECTORY', '--load-path LIBRARY', 'add a directory to the $LOAD_PATH',
110
+ opts.on('-IDIRECTORY', '--load-path DIRECTORY', 'add a directory to the $LOAD_PATH',
112
111
  'may be specified more than once') do |path|
113
112
  (self[:load_paths] ||= []).concat(path.split ::File::PATH_SEPARATOR)
114
113
  end
@@ -33,21 +33,21 @@ module Asciidoctor
33
33
  end
34
34
 
35
35
  def inline_anchor node
36
- target = node.target
37
36
  case node.type
38
37
  when :ref
39
- %(<anchor#{common_attributes target, nil, node.text}/>)
38
+ %(<anchor#{common_attributes node.target, nil, node.text}/>)
40
39
  when :xref
41
- if node.attr? 'path', nil
42
- linkend = (node.attr 'fragment') || target
43
- (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
40
+ if (path = node.attributes['path'])
41
+ # QUESTION should we use refid as fallback text instead? (like the html5 backend?)
42
+ %(<ulink url="#{node.target}">#{node.text || path}</ulink>)
44
43
  else
45
- text = node.text || (node.attr 'path')
46
- %(<ulink url="#{target}">#{text}</ulink>)
44
+ linkend = node.attributes['fragment'] || node.target
45
+ (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
47
46
  end
48
47
  when :link
49
- %(<ulink url="#{target}">#{node.text}</ulink>)
48
+ %(<ulink url="#{node.target}">#{node.text}</ulink>)
50
49
  when :bibref
50
+ target = node.target
51
51
  %(<anchor#{common_attributes target, nil, "[#{target}]"}/>[#{target}])
52
52
  end
53
53
  end
@@ -83,7 +83,7 @@ module Asciidoctor
83
83
  result * EOL
84
84
  end
85
85
 
86
- DLIST_TAGS = {
86
+ (DLIST_TAGS = {
87
87
  'labeled' => {
88
88
  :list => 'variablelist',
89
89
  :entry => 'varlistentry',
@@ -103,8 +103,7 @@ module Asciidoctor
103
103
  :term => 'glossterm',
104
104
  :item => 'glossdef'
105
105
  }
106
- }
107
- DLIST_TAGS.default = DLIST_TAGS['labeled']
106
+ }).default = DLIST_TAGS['labeled']
108
107
 
109
108
  def dlist node
110
109
  result = []
@@ -188,6 +187,8 @@ module Asciidoctor
188
187
  def image node
189
188
  width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : nil
190
189
  depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : nil
190
+ # FIXME if scaledwidth is set, we should remove width & depth
191
+ # See http://www.docbook.org/tdg/en/html/imagedata.html#d0e92271 for details
191
192
  swidth_attribute = (node.attr? 'scaledwidth') ? %( width="#{node.attr 'scaledwidth'}" scalefit="1") : nil
192
193
  scale_attribute = (node.attr? 'scale') ? %( scale="#{node.attr 'scale'}") : nil
193
194
  align_attribute = (node.attr? 'align') ? %( align="#{node.attr 'align'}") : nil
@@ -254,9 +255,11 @@ module Asciidoctor
254
255
  if node.style == 'latexmath'
255
256
  equation_data = %(<alt><![CDATA[#{equation}]]></alt>
256
257
  <mediaobject><textobject><phrase></phrase></textobject></mediaobject>)
257
- # asciimath
258
+ elsif node.style == 'asciimath' && ((defined? ::AsciiMath) || (!(defined? @asciimath_loaded) ?
259
+ (@asciimath_loaded = Helpers.require_library 'asciimath', true, :warn) : @asciimath_loaded))
260
+ equation_data = ::AsciiMath.parse(equation).to_mathml 'mml:', 'xmlns:mml' => 'http://www.w3.org/1998/Math/MathML'
258
261
  else
259
- # DocBook backends can't handle AsciiMath, so output raw expression in text object
262
+ # Unsupported math style, so output raw expression in text object
260
263
  equation_data = %(<mediaobject><textobject><phrase><![CDATA[#{equation}]]></phrase></textobject></mediaobject>)
261
264
  end
262
265
  if node.title?
@@ -374,6 +377,11 @@ module Asciidoctor
374
377
  result = []
375
378
  pgwide_attribute = (node.option? 'pgwide') ? ' pgwide="1"' : nil
376
379
  result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext}#{pgwide_attribute} frame="#{node.attr 'frame', 'all'}" rowsep="#{['none', 'cols'].include?(node.attr 'grid') ? 0 : 1}" colsep="#{['none', 'rows'].include?(node.attr 'grid') ? 0 : 1}">)
380
+ if (node.option? 'unbreakable')
381
+ result << '<?dbfo keep-together="always"?>'
382
+ elsif (node.option? 'breakable')
383
+ result << '<?dbfo keep-together="auto"?>'
384
+ end
377
385
  result << %(<title>#{node.title}</title>) if tag_name == 'table'
378
386
  if (width = (node.attr? 'width') ? (node.attr 'width') : nil)
379
387
  TABLE_PI_NAMES.each do |pi_name|
@@ -488,16 +496,18 @@ module Asciidoctor
488
496
  when :ref
489
497
  %(<anchor#{common_attributes node.target, nil, node.text}/>)
490
498
  when :xref
491
- if node.attr? 'path', nil
492
- linkend = (node.attr 'fragment') || node.target
493
- (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
499
+ if (path = node.attributes['path'])
500
+ # QUESTION should we use refid as fallback text instead? (like the html5 backend?)
501
+ %(<link xlink:href="#{node.target}">#{node.text || path}</link>)
494
502
  else
495
- %(<link xlink:href="#{target}">#{node.text || (node.attr 'path')}</link>)
503
+ linkend = node.attributes['fragment'] || node.target
504
+ (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
496
505
  end
497
506
  when :link
498
507
  %(<link xlink:href="#{node.target}">#{node.text}</link>)
499
508
  when :bibref
500
- %(<anchor#{common_attributes node.target, nil, "[#{node.target}]"}/>[#{node.target}])
509
+ target = node.target
510
+ %(<anchor#{common_attributes target, nil, "[#{target}]"}/>[#{target}])
501
511
  else
502
512
  warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect})
503
513
  end
@@ -561,8 +571,7 @@ module Asciidoctor
561
571
  if (keys = node.attr 'keys').size == 1
562
572
  %(<keycap>#{keys[0]}</keycap>)
563
573
  else
564
- key_combo = keys.map {|key| %(<keycap>#{key}</keycap>) }.join
565
- %(<keycombo>#{key_combo}</keycombo>)
574
+ %(<keycombo>#{keys.map {|key| "<keycap>#{key}</keycap>" }.join}</keycombo>)
566
575
  end
567
576
  end
568
577
 
@@ -578,7 +587,7 @@ module Asciidoctor
578
587
  end
579
588
  end
580
589
 
581
- QUOTE_TAGS = {
590
+ (QUOTE_TAGS = {
582
591
  :emphasis => ['<emphasis>', '</emphasis>', true],
583
592
  :strong => ['<emphasis role="strong">', '</emphasis>', true],
584
593
  :monospaced => ['<literal>', '</literal>', false],
@@ -587,8 +596,7 @@ module Asciidoctor
587
596
  :double => ['&#8220;', '&#8221;', true],
588
597
  :single => ['&#8216;', '&#8217;', true],
589
598
  :mark => ['<emphasis role="marked">', '</emphasis>', false]
590
- }
591
- QUOTE_TAGS.default = [nil, nil, true]
599
+ }).default = [nil, nil, true]
592
600
 
593
601
  def inline_quoted node
594
602
  if (type = node.type) == :latexmath
@@ -672,8 +680,8 @@ module Asciidoctor
672
680
  result << %(</revision>
673
681
  </revhistory>)
674
682
  end
675
- unless (header_docinfo = doc.docinfo :header).empty?
676
- result << header_docinfo
683
+ unless (head_docinfo = doc.docinfo).empty?
684
+ result << head_docinfo
677
685
  end
678
686
  result << %(<orgname>#{doc.attr 'orgname'}</orgname>) if doc.attr? 'orgname'
679
687
  end
@@ -49,7 +49,7 @@ module Asciidoctor
49
49
  require 'thread_safe'.to_s unless defined? ::ThreadSafe
50
50
  new ::ThreadSafe::Cache.new
51
51
  rescue ::LoadError
52
- warn 'asciidoctor: WARNING: gem \'thread_safe\' is not installed. This gem recommended when registering custom converters.'
52
+ warn 'asciidoctor: WARNING: gem \'thread_safe\' is not installed. This gem is recommended when registering custom converters.'
53
53
  new
54
54
  end
55
55
  end
@@ -207,6 +207,11 @@ module Asciidoctor
207
207
  require 'asciidoctor/converter/docbook45'.to_s
208
208
  end
209
209
  DocBook45Converter.new backend, opts
210
+ when 'manpage'
211
+ unless defined? ::Asciidoctor::Converter::ManPageConverter
212
+ require 'asciidoctor/converter/manpage'.to_s
213
+ end
214
+ ManPageConverter.new backend, opts
210
215
  end
211
216
 
212
217
  return base_converter unless opts.key? :template_dirs
@@ -3,7 +3,7 @@ module Asciidoctor
3
3
  # A built-in {Converter} implementation that generates HTML 5 output
4
4
  # consistent with the html5 backend from AsciiDoc Python.
5
5
  class Converter::Html5Converter < Converter::BuiltIn
6
- QUOTE_TAGS = {
6
+ (QUOTE_TAGS = {
7
7
  :emphasis => ['<em>', '</em>', true],
8
8
  :strong => ['<strong>', '</strong>', true],
9
9
  :monospaced => ['<code>', '</code>', true],
@@ -17,8 +17,11 @@ module Asciidoctor
17
17
  # Opal can't resolve these constants when referenced here
18
18
  #:asciimath => INLINE_MATH_DELIMITERS[:asciimath] + [false],
19
19
  #:latexmath => INLINE_MATH_DELIMITERS[:latexmath] + [false]
20
- }
21
- QUOTE_TAGS.default = [nil, nil, nil]
20
+ }).default = [nil, nil, nil]
21
+
22
+ SvgPreambleRx = /\A.*?(?=<svg[ >])/m
23
+ SvgStartTagRx = /\A<svg[^>]*>/
24
+ DimensionAttributeRx = /\s(?:width|height|style)=(["']).*?\1/
22
25
 
23
26
  def initialize backend, opts = {}
24
27
  @xml_mode = opts[:htmlsyntax] == 'xml'
@@ -30,8 +33,9 @@ module Asciidoctor
30
33
  result = []
31
34
  slash = @void_element_slash
32
35
  br = %(<br#{slash}>)
33
- asset_uri_scheme = (node.attr 'asset-uri-scheme', 'https')
34
- asset_uri_scheme = %(#{asset_uri_scheme}:) unless asset_uri_scheme.empty?
36
+ unless (asset_uri_scheme = (node.attr 'asset-uri-scheme', 'https')).empty?
37
+ asset_uri_scheme = %(#{asset_uri_scheme}:)
38
+ end
35
39
  cdn_base = %(#{asset_uri_scheme}//cdnjs.cloudflare.com/ajax/libs)
36
40
  linkcss = node.safe >= SafeMode::SECURE || (node.attr? 'linkcss')
37
41
  result << '<!DOCTYPE html>'
@@ -52,7 +56,7 @@ module Asciidoctor
52
56
  result << %(<title>#{node.doctitle :sanitize => true, :use_fallback => true}</title>)
53
57
  if DEFAULT_STYLESHEET_KEYS.include?(node.attr 'stylesheet')
54
58
  if (webfonts = node.attr 'webfonts')
55
- result << %(<link rel="stylesheet" href="#{asset_uri_scheme}//fonts.googleapis.com/css?family=#{webfonts.empty? ? 'Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400' : webfonts}"#{slash}>)
59
+ result << %(<link rel="stylesheet" href="#{asset_uri_scheme}//fonts.googleapis.com/css?family=#{webfonts.empty? ? 'Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700' : webfonts}"#{slash}>)
56
60
  end
57
61
  if linkcss
58
62
  result << %(<link rel="stylesheet" href="#{node.normalize_web_path DEFAULT_STYLESHEET_NAME, (node.attr 'stylesdir', ''), false}"#{slash}>)
@@ -71,14 +75,14 @@ module Asciidoctor
71
75
 
72
76
  if node.attr? 'icons', 'font'
73
77
  if node.attr? 'iconfont-remote'
74
- result << %(<link rel="stylesheet" href="#{node.attr 'iconfont-cdn', %[#{cdn_base}/font-awesome/4.2.0/css/font-awesome.min.css]}"#{slash}>)
78
+ result << %(<link rel="stylesheet" href="#{node.attr 'iconfont-cdn', %[#{cdn_base}/font-awesome/4.4.0/css/font-awesome.min.css]}"#{slash}>)
75
79
  else
76
80
  iconfont_stylesheet = %(#{node.attr 'iconfont-name', 'font-awesome'}.css)
77
81
  result << %(<link rel="stylesheet" href="#{node.normalize_web_path iconfont_stylesheet, (node.attr 'stylesdir', ''), false}"#{slash}>)
78
82
  end
79
83
  end
80
84
 
81
- case node.attr 'source-highlighter'
85
+ case (highlighter = node.attr 'source-highlighter')
82
86
  when 'coderay'
83
87
  if (node.attr 'coderay-css', 'class') == 'class'
84
88
  if linkcss
@@ -96,38 +100,6 @@ module Asciidoctor
96
100
  result << (@stylesheets.embed_pygments_stylesheet pygments_style)
97
101
  end
98
102
  end
99
- when 'highlightjs', 'highlight.js'
100
- highlightjs_path = node.attr 'highlightjsdir', %(#{cdn_base}/highlight.js/8.4)
101
- result << %(<link rel="stylesheet" href="#{highlightjs_path}/styles/#{node.attr 'highlightjs-theme', 'github'}.min.css"#{slash}>
102
- <script src="#{highlightjs_path}/highlight.min.js"></script>
103
- <script>hljs.initHighlightingOnLoad()</script>)
104
- when 'prettify'
105
- prettify_path = node.attr 'prettifydir', %(#{cdn_base}/prettify/r298)
106
- result << %(<link rel="stylesheet" href="#{prettify_path}/#{node.attr 'prettify-theme', 'prettify'}.min.css"#{slash}>
107
- <script src="#{prettify_path}/prettify.min.js"></script>
108
- <script>document.addEventListener('DOMContentLoaded', prettyPrint)</script>)
109
- end
110
-
111
- if node.attr? 'stem'
112
- # IMPORTANT to_s calls on delimiter arrays are intentional for JavaScript compat (emulates JSON.stringify)
113
- eqnums_val = node.attr 'eqnums', 'none'
114
- eqnums_val = 'AMS' if eqnums_val == ''
115
- eqnums_opt = %( equationNumbers: { autoNumber: "#{eqnums_val}" } )
116
- result << %(<script type="text/x-mathjax-config">
117
- MathJax.Hub.Config({
118
- tex2jax: {
119
- inlineMath: [#{INLINE_MATH_DELIMITERS[:latexmath].to_s}],
120
- displayMath: [#{BLOCK_MATH_DELIMITERS[:latexmath].to_s}],
121
- ignoreClass: "nostem|nolatexmath"
122
- },
123
- asciimath2jax: {
124
- delimiters: [#{BLOCK_MATH_DELIMITERS[:asciimath].to_s}],
125
- ignoreClass: "nostem|noasciimath"
126
- },
127
- TeX: {#{eqnums_opt}}
128
- });
129
- </script>
130
- <script src="#{cdn_base}/mathjax/2.4.0/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script>)
131
103
  end
132
104
 
133
105
  unless (docinfo_content = node.docinfo).empty?
@@ -159,6 +131,7 @@ MathJax.Hub.Config({
159
131
  #{outline node}
160
132
  </div>)
161
133
  end
134
+ # QUESTION should this h2 have an auto-generated id?
162
135
  result << %(<h2>#{node.attr 'manname-title'}</h2>
163
136
  <div class="sectionbody">
164
137
  <p>#{node.attr 'manname'} - #{node.attr 'manpurpose'}</p>
@@ -221,22 +194,57 @@ MathJax.Hub.Config({
221
194
  end
222
195
  result << '</div>'
223
196
  end
197
+
224
198
  unless node.nofooter
225
199
  result << '<div id="footer">'
226
200
  result << '<div id="footer-text">'
227
- if node.attr? 'revnumber'
228
- result << %(#{node.attr 'version-label'} #{node.attr 'revnumber'}#{br})
229
- end
230
- if node.attr? 'last-update-label'
231
- result << %(#{node.attr 'last-update-label'} #{node.attr 'docdatetime'})
232
- end
201
+ result << %(#{node.attr 'version-label'} #{node.attr 'revnumber'}#{br}) if node.attr? 'revnumber'
202
+ result << %(#{node.attr 'last-update-label'} #{node.attr 'docdatetime'}) if node.attr? 'last-update-label'
233
203
  result << '</div>'
234
- unless (docinfo_content = node.docinfo :footer).empty?
235
- result << docinfo_content
236
- end
237
204
  result << '</div>'
238
205
  end
239
206
 
207
+ unless (docinfo_content = node.docinfo :footer).empty?
208
+ result << docinfo_content
209
+ end
210
+
211
+ # Load Javascript at the end of body for performance
212
+ # See http://www.html5rocks.com/en/tutorials/speed/script-loading/
213
+ case highlighter
214
+ when 'highlightjs', 'highlight.js'
215
+ highlightjs_path = node.attr 'highlightjsdir', %(#{cdn_base}/highlight.js/8.9.1)
216
+ result << %(<link rel="stylesheet" href="#{highlightjs_path}/styles/#{node.attr 'highlightjs-theme', 'github'}.min.css"#{slash}>)
217
+ result << %(<script src="#{highlightjs_path}/highlight.min.js"></script>
218
+ <script>hljs.initHighlighting()</script>)
219
+ when 'prettify'
220
+ prettify_path = node.attr 'prettifydir', %(#{cdn_base}/prettify/r298)
221
+ result << %(<link rel="stylesheet" href="#{prettify_path}/#{node.attr 'prettify-theme', 'prettify'}.min.css"#{slash}>)
222
+ result << %(<script src="#{prettify_path}/prettify.min.js"></script>
223
+ <script>prettyPrint()</script>)
224
+ end
225
+
226
+ if node.attr? 'stem'
227
+ # IMPORTANT to_s calls on delimiter arrays are intentional for JavaScript compat (emulates JSON.stringify)
228
+ eqnums_val = node.attr 'eqnums', 'none'
229
+ eqnums_val = 'AMS' if eqnums_val == ''
230
+ eqnums_opt = %( equationNumbers: { autoNumber: "#{eqnums_val}" } )
231
+ result << %(<script type="text/x-mathjax-config">
232
+ MathJax.Hub.Config({
233
+ tex2jax: {
234
+ inlineMath: [#{INLINE_MATH_DELIMITERS[:latexmath].inspect}],
235
+ displayMath: [#{BLOCK_MATH_DELIMITERS[:latexmath].inspect}],
236
+ ignoreClass: "nostem|nolatexmath"
237
+ },
238
+ asciimath2jax: {
239
+ delimiters: [#{BLOCK_MATH_DELIMITERS[:asciimath].inspect}],
240
+ ignoreClass: "nostem|noasciimath"
241
+ },
242
+ TeX: {#{eqnums_opt}}
243
+ });
244
+ </script>
245
+ <script src="#{cdn_base}/mathjax/2.5.3/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script>)
246
+ end
247
+
240
248
  result << '</body>'
241
249
  result << '</html>'
242
250
  result * EOL
@@ -244,9 +252,29 @@ MathJax.Hub.Config({
244
252
 
245
253
  def embedded node
246
254
  result = []
247
- if !node.notitle && node.has_header?
248
- id_attr = node.id ? %( id="#{node.id}") : nil
249
- result << %(<h1#{id_attr}>#{node.header.title}</h1>)
255
+ if node.doctype == 'manpage'
256
+ # QUESTION should notitle control the manual page title?
257
+ unless node.notitle
258
+ id_attr = node.id ? %( id="#{node.id}") : nil
259
+ result << %(<h1#{id_attr}>#{node.doctitle} Manual Page</h1>)
260
+ end
261
+ # QUESTION should this h2 have an auto-generated id?
262
+ result << %(<h2>#{node.attr 'manname-title'}</h2>
263
+ <div class="sectionbody">
264
+ <p>#{node.attr 'manname'} - #{node.attr 'manpurpose'}</p>
265
+ </div>)
266
+ else
267
+ if node.has_header? && !node.notitle
268
+ id_attr = node.id ? %( id="#{node.id}") : nil
269
+ result << %(<h1#{id_attr}>#{node.header.title}</h1>)
270
+ end
271
+ end
272
+
273
+ if (node.attr? 'toc') && !['macro', 'preamble'].include?(node.attr 'toc-placement')
274
+ result << %(<div id="toc" class="toc">
275
+ <div id="toctitle">#{node.attr 'toc-title'}</div>
276
+ #{outline node}
277
+ </div>)
250
278
  end
251
279
 
252
280
  result << node.content
@@ -256,10 +284,9 @@ MathJax.Hub.Config({
256
284
  <hr#{@void_element_slash}>)
257
285
  node.footnotes.each do |footnote|
258
286
  result << %(<div class="footnote" id="_footnote_#{footnote.index}">
259
- <a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a> #{footnote.text}
287
+ <a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a>. #{footnote.text}
260
288
  </div>)
261
289
  end
262
-
263
290
  result << '</div>'
264
291
  end
265
292
 
@@ -510,29 +537,34 @@ Your browser does not support the audio tag.
510
537
  end
511
538
 
512
539
  def image node
513
- align = (node.attr? 'align') ? (node.attr 'align') : nil
514
- float = (node.attr? 'float') ? (node.attr 'float') : nil
515
- style_attribute = if align || float
516
- styles = [align ? %(text-align: #{align}) : nil, float ? %(float: #{float}) : nil].compact
517
- %( style="#{styles * ';'}")
540
+ target = node.attr 'target'
541
+ width_attr = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
542
+ height_attr = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
543
+ if ((node.attr? 'format', 'svg', false) || (target.include? '.svg')) && node.document.safe < SafeMode::SECURE
544
+ ((svg = (node.option? 'inline')) || (obj = (node.option? 'interactive')))
545
+ if svg
546
+ img = (read_svg_contents node, target) || %(<span class="alt">#{node.attr 'alt'}</span>)
547
+ elsif obj
548
+ fallback = (node.attr? 'fallback') ? %(<img src="#{node.image_uri(node.attr 'fallback')}" alt="#{node.attr 'alt'}"#{width_attr}#{height_attr}#{@void_element_slash}>) : %(<span class="alt">#{node.attr 'alt'}</span>)
549
+ img = %(<object type="image/svg+xml" data="#{node.image_uri target}"#{width_attr}#{height_attr}>#{fallback}</object>)
550
+ end
518
551
  end
519
-
520
- width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
521
- height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
522
-
523
- img_element = %(<img src="#{node.image_uri node.attr('target')}" alt="#{node.attr 'alt'}"#{width_attribute}#{height_attribute}#{@void_element_slash}>)
552
+ img ||= %(<img src="#{node.image_uri target}" alt="#{node.attr 'alt'}"#{width_attr}#{height_attr}#{@void_element_slash}>)
524
553
  if (link = node.attr 'link')
525
- img_element = %(<a class="image" href="#{link}">#{img_element}</a>)
554
+ img = %(<a class="image" href="#{link}">#{img}</a>)
526
555
  end
527
- id_attribute = node.id ? %( id="#{node.id}") : nil
556
+ id_attr = node.id ? %( id="#{node.id}") : nil
528
557
  classes = ['imageblock', node.style, node.role].compact
529
- class_attribute = %( class="#{classes * ' '}")
530
- title_element = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : nil
531
-
532
- %(<div#{id_attribute}#{class_attribute}#{style_attribute}>
558
+ class_attr = %( class="#{classes * ' '}")
559
+ styles = []
560
+ styles << %(text-align: #{node.attr 'align'}) if node.attr? 'align'
561
+ styles << %(float: #{node.attr 'float'}) if node.attr? 'float'
562
+ style_attr = styles.empty? ? nil : %( style="#{styles * ';'}")
563
+ title_el = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : nil
564
+ %(<div#{id_attr}#{class_attr}#{style_attr}>
533
565
  <div class="content">
534
- #{img_element}
535
- </div>#{title_element}
566
+ #{img}
567
+ </div>#{title_el}
536
568
  </div>)
537
569
  end
538
570
 
@@ -663,25 +695,16 @@ Your browser does not support the audio tag.
663
695
  end
664
696
 
665
697
  def paragraph node
666
- attributes = if node.id
667
- if node.role
668
- %( id="#{node.id}" class="paragraph #{node.role}")
669
- else
670
- %( id="#{node.id}" class="paragraph")
671
- end
672
- elsif node.role
673
- %( class="paragraph #{node.role}")
674
- else
675
- ' class="paragraph"'
676
- end
698
+ class_attribute = node.role ? %(class="paragraph #{node.role}") : 'class="paragraph"'
699
+ attributes = node.id ? %(id="#{node.id}" #{class_attribute}) : class_attribute
677
700
 
678
701
  if node.title?
679
- %(<div#{attributes}>
702
+ %(<div #{attributes}>
680
703
  <div class="title">#{node.title}</div>
681
704
  <p>#{node.content}</p>
682
705
  </div>)
683
706
  else
684
- %(<div#{attributes}>
707
+ %(<div #{attributes}>
685
708
  <p>#{node.content}</p>
686
709
  </div>)
687
710
  end
@@ -911,6 +934,9 @@ Your browser does not support the audio tag.
911
934
  height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
912
935
  case node.attr 'poster'
913
936
  when 'vimeo'
937
+ unless (asset_uri_scheme = (node.document.attr 'asset-uri-scheme', 'https')).empty?
938
+ asset_uri_scheme = %(#{asset_uri_scheme}:)
939
+ end
914
940
  start_anchor = (node.attr? 'start', nil, false) ? %(#at=#{node.attr 'start'}) : nil
915
941
  delimiter = '?'
916
942
  autoplay_param = (node.option? 'autoplay') ? %(#{delimiter}autoplay=1) : nil
@@ -918,10 +944,13 @@ Your browser does not support the audio tag.
918
944
  loop_param = (node.option? 'loop') ? %(#{delimiter}loop=1) : nil
919
945
  %(<div#{id_attribute}#{class_attribute}>#{title_element}
920
946
  <div class="content">
921
- <iframe#{width_attribute}#{height_attribute} src="//player.vimeo.com/video/#{node.attr 'target'}#{start_anchor}#{autoplay_param}#{loop_param}" frameborder="0"#{(node.option? 'nofullscreen') ? nil : (append_boolean_attribute 'allowfullscreen', xml)}></iframe>
947
+ <iframe#{width_attribute}#{height_attribute} src="#{asset_uri_scheme}//player.vimeo.com/video/#{node.attr 'target'}#{start_anchor}#{autoplay_param}#{loop_param}" frameborder="0"#{(node.option? 'nofullscreen') ? nil : (append_boolean_attribute 'allowfullscreen', xml)}></iframe>
922
948
  </div>
923
949
  </div>)
924
950
  when 'youtube'
951
+ unless (asset_uri_scheme = (node.document.attr 'asset-uri-scheme', 'https')).empty?
952
+ asset_uri_scheme = %(#{asset_uri_scheme}:)
953
+ end
925
954
  rel_param_val = (node.option? 'related') ? 1 : 0
926
955
  start_param = (node.attr? 'start', nil, false) ? %(&amp;start=#{node.attr 'start'}) : nil
927
956
  end_param = (node.attr? 'end', nil, false) ? %(&amp;end=#{node.attr 'end'}) : nil
@@ -951,13 +980,14 @@ Your browser does not support the audio tag.
951
980
  # INFO playlist bar doesn't appear in Firefox unless showinfo=1 and modestbranding=1
952
981
  list_param = %(&amp;playlist=#{playlist})
953
982
  else
954
- list_param = nil
983
+ # NOTE for loop to work, playlist must be specified; use VIDEO_ID if there's no explicit playlist
984
+ list_param = loop_param ? %(&amp;playlist=#{target}) : nil
955
985
  end
956
986
  end
957
987
 
958
988
  %(<div#{id_attribute}#{class_attribute}>#{title_element}
959
989
  <div class="content">
960
- <iframe#{width_attribute}#{height_attribute} src="//www.youtube.com/embed/#{target}?rel=#{rel_param_val}#{start_param}#{end_param}#{autoplay_param}#{loop_param}#{controls_param}#{list_param}#{fs_param}#{modest_param}#{theme_param}#{hl_param}" frameborder="0"#{fs_attribute}></iframe>
990
+ <iframe#{width_attribute}#{height_attribute} src="#{asset_uri_scheme}//www.youtube.com/embed/#{target}?rel=#{rel_param_val}#{start_param}#{end_param}#{autoplay_param}#{loop_param}#{controls_param}#{list_param}#{fs_param}#{modest_param}#{theme_param}#{hl_param}" frameborder="0"#{fs_attribute}></iframe>
961
991
  </div>
962
992
  </div>)
963
993
  else
@@ -979,7 +1009,7 @@ Your browser does not support the video tag.
979
1009
  target = node.target
980
1010
  case node.type
981
1011
  when :xref
982
- refid = (node.attr 'refid') || target
1012
+ refid = node.attributes['refid'] || target
983
1013
  # NOTE we lookup text in converter because DocBook doesn't need this logic
984
1014
  text = node.text || (node.document.references[:ids][refid] || %([#{refid}]))
985
1015
  # FIXME shouldn't target be refid? logic seems confused here
@@ -992,8 +1022,8 @@ Your browser does not support the video tag.
992
1022
  if (role = node.role)
993
1023
  attrs << %( class="#{role}")
994
1024
  end
995
- attrs << %( title="#{node.attr 'title'}") if node.attr? 'title'
996
- attrs << %( target="#{node.attr 'window'}") if node.attr? 'window'
1025
+ attrs << %( title="#{node.attr 'title'}") if node.attr? 'title', nil, false
1026
+ attrs << %( target="#{node.attr 'window'}") if node.attr? 'window', nil, false
997
1027
  %(<a href="#{target}"#{attrs.join}>#{node.text}</a>)
998
1028
  when :bibref
999
1029
  %(<a id="#{target}"></a>[#{target}])
@@ -1024,51 +1054,47 @@ Your browser does not support the video tag.
1024
1054
  def inline_footnote node
1025
1055
  if (index = node.attr 'index')
1026
1056
  if node.type == :xref
1027
- %(<span class="footnoteref">[<a class="footnote" href="#_footnote_#{index}" title="View footnote.">#{index}</a>]</span>)
1057
+ %(<sup class="footnoteref">[<a class="footnote" href="#_footnote_#{index}" title="View footnote.">#{index}</a>]</sup>)
1028
1058
  else
1029
1059
  id_attr = node.id ? %( id="_footnote_#{node.id}") : nil
1030
- %(<span class="footnote"#{id_attr}>[<a id="_footnoteref_#{index}" class="footnote" href="#_footnote_#{index}" title="View footnote.">#{index}</a>]</span>)
1060
+ %(<sup class="footnote"#{id_attr}>[<a id="_footnoteref_#{index}" class="footnote" href="#_footnote_#{index}" title="View footnote.">#{index}</a>]</sup>)
1031
1061
  end
1032
1062
  elsif node.type == :xref
1033
- %(<span class="footnoteref red" title="Unresolved footnote reference.">[#{node.text}]</span>)
1063
+ %(<sup class="footnoteref red" title="Unresolved footnote reference.">[#{node.text}]</sup>)
1034
1064
  end
1035
1065
  end
1036
1066
 
1037
1067
  def inline_image node
1038
1068
  if (type = node.type) == 'icon' && (node.document.attr? 'icons', 'font')
1039
- style_class = %(fa fa-#{node.target})
1040
- if node.attr? 'size'
1041
- style_class = %(#{style_class} fa-#{node.attr 'size'})
1042
- end
1043
- if node.attr? 'rotate'
1044
- style_class = %(#{style_class} fa-rotate-#{node.attr 'rotate'})
1045
- end
1046
- if node.attr? 'flip'
1047
- style_class = %(#{style_class} fa-flip-#{node.attr 'flip'})
1069
+ class_attr_val = %(fa fa-#{node.target})
1070
+ {'size' => 'fa-', 'rotate' => 'fa-rotate-', 'flip' => 'fa-flip-'}.each do |(key, prefix)|
1071
+ class_attr_val = %(#{class_attr_val} #{prefix}#{node.attr key}) if node.attr? key
1048
1072
  end
1049
- title_attribute = (node.attr? 'title') ? %( title="#{node.attr 'title'}") : nil
1050
- img = %(<i class="#{style_class}"#{title_attribute}></i>)
1073
+ title_attr = (node.attr? 'title') ? %( title="#{node.attr 'title'}") : nil
1074
+ img = %(<i class="#{class_attr_val}"#{title_attr}></i>)
1051
1075
  elsif type == 'icon' && !(node.document.attr? 'icons')
1052
1076
  img = %([#{node.attr 'alt'}])
1053
1077
  else
1054
- resolved_target = (type == 'icon') ? (node.icon_uri node.target) : (node.image_uri node.target)
1055
-
1056
- attrs = ['alt', 'width', 'height', 'title'].map {|name|
1057
- (node.attr? name) ? %( #{name}="#{node.attr name}") : nil
1058
- }.join
1059
-
1060
- img = %(<img src="#{resolved_target}"#{attrs}#{@void_element_slash}>)
1078
+ target = node.target
1079
+ attrs = ['width', 'height', 'title'].map {|name| (node.attr? name) ? %( #{name}="#{node.attr name}") : nil }.join
1080
+ if type != 'icon' && ((node.attr? 'format', 'svg', false) || (target.include? '.svg')) &&
1081
+ node.document.safe < SafeMode::SECURE && ((svg = (node.option? 'inline')) || (obj = (node.option? 'interactive')))
1082
+ if svg
1083
+ img = (read_svg_contents node, target) || %(<span class="alt">#{node.attr 'alt'}</span>)
1084
+ elsif obj
1085
+ fallback = (node.attr? 'fallback') ? %(<img src="#{node.image_uri(node.attr 'fallback')}" alt="#{node.attr 'alt'}"#{attrs}#{@void_element_slash}>) : %(<span class="alt">#{node.attr 'alt'}</span>)
1086
+ img = %(<object type="image/svg+xml" data="#{node.image_uri target}"#{attrs}>#{fallback}</object>)
1087
+ end
1088
+ end
1089
+ img ||= %(<img src="#{type == 'icon' ? (node.icon_uri target) : (node.image_uri target)}" alt="#{node.attr 'alt'}"#{attrs}#{@void_element_slash}>)
1061
1090
  end
1062
-
1063
1091
  if node.attr? 'link'
1064
1092
  window_attr = (node.attr? 'window') ? %( target="#{node.attr 'window'}") : nil
1065
1093
  img = %(<a class="image" href="#{node.attr 'link'}"#{window_attr}>#{img}</a>)
1066
1094
  end
1067
-
1068
- style_classes = (role = node.role) ? %(#{type} #{role}) : type
1095
+ class_attr_val = (role = node.role) ? %(#{type} #{role}) : type
1069
1096
  style_attr = (node.attr? 'float') ? %( style="float: #{node.attr 'float'}") : nil
1070
-
1071
- %(<span class="#{style_classes}"#{style_attr}>#{img}</span>)
1097
+ %(<span class="#{class_attr_val}"#{style_attr}>#{img}</span>)
1072
1098
  end
1073
1099
 
1074
1100
  def inline_indexterm node
@@ -1114,5 +1140,21 @@ Your browser does not support the video tag.
1114
1140
  def append_boolean_attribute name, xml
1115
1141
  xml ? %( #{name}="#{name}") : %( #{name})
1116
1142
  end
1143
+
1144
+ def read_svg_contents node, target
1145
+ if (svg = node.read_contents target, :start => (node.document.attr 'imagesdir'), :normalize => true, :label => 'SVG')
1146
+ svg = svg.sub SvgPreambleRx, ''
1147
+ start_tag = nil
1148
+ ['width', 'height'].each do |dim|
1149
+ if node.attr? dim
1150
+ # NOTE width, height and style attributes are removed if either width or height is specified
1151
+ start_tag ||= (svg.match SvgStartTagRx)[0].gsub DimensionAttributeRx, ''
1152
+ start_tag = %(#{start_tag.chop} #{dim}="#{node.attr dim}px">)
1153
+ end
1154
+ end
1155
+ svg = svg.sub SvgStartTagRx, start_tag if start_tag
1156
+ end
1157
+ svg
1158
+ end
1117
1159
  end
1118
1160
  end