asciidoctor 2.0.6 → 2.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +159 -6
  3. data/LICENSE +2 -1
  4. data/README-de.adoc +5 -5
  5. data/README-fr.adoc +4 -4
  6. data/README-jp.adoc +248 -183
  7. data/README-zh_CN.adoc +6 -6
  8. data/README.adoc +17 -11
  9. data/asciidoctor.gemspec +8 -8
  10. data/data/locale/attributes-ar.adoc +4 -3
  11. data/data/locale/attributes-bg.adoc +4 -3
  12. data/data/locale/attributes-ca.adoc +6 -5
  13. data/data/locale/attributes-cs.adoc +4 -3
  14. data/data/locale/attributes-da.adoc +6 -5
  15. data/data/locale/attributes-de.adoc +4 -4
  16. data/data/locale/attributes-en.adoc +4 -4
  17. data/data/locale/attributes-es.adoc +6 -5
  18. data/data/locale/attributes-fa.adoc +4 -3
  19. data/data/locale/attributes-fi.adoc +4 -3
  20. data/data/locale/attributes-fr.adoc +6 -5
  21. data/data/locale/attributes-hu.adoc +4 -3
  22. data/data/locale/attributes-id.adoc +4 -3
  23. data/data/locale/attributes-it.adoc +4 -3
  24. data/data/locale/attributes-ja.adoc +4 -3
  25. data/data/locale/{attributes-kr.adoc → attributes-ko.adoc} +4 -3
  26. data/data/locale/attributes-nb.adoc +4 -3
  27. data/data/locale/attributes-nl.adoc +4 -3
  28. data/data/locale/attributes-nn.adoc +4 -3
  29. data/data/locale/attributes-pl.adoc +8 -7
  30. data/data/locale/attributes-pt.adoc +6 -5
  31. data/data/locale/attributes-pt_BR.adoc +6 -5
  32. data/data/locale/attributes-ro.adoc +4 -3
  33. data/data/locale/attributes-ru.adoc +6 -5
  34. data/data/locale/attributes-sr.adoc +4 -4
  35. data/data/locale/attributes-sr_Latn.adoc +4 -4
  36. data/data/locale/attributes-sv.adoc +4 -4
  37. data/data/locale/attributes-tr.adoc +4 -3
  38. data/data/locale/attributes-uk.adoc +6 -5
  39. data/data/locale/attributes-zh_CN.adoc +4 -3
  40. data/data/locale/attributes-zh_TW.adoc +4 -3
  41. data/data/stylesheets/asciidoctor-default.css +29 -26
  42. data/lib/asciidoctor.rb +94 -1098
  43. data/lib/asciidoctor/abstract_block.rb +19 -11
  44. data/lib/asciidoctor/abstract_node.rb +21 -15
  45. data/lib/asciidoctor/attribute_list.rb +59 -67
  46. data/lib/asciidoctor/cli/invoker.rb +2 -0
  47. data/lib/asciidoctor/cli/options.rb +8 -8
  48. data/lib/asciidoctor/convert.rb +198 -0
  49. data/lib/asciidoctor/converter.rb +14 -13
  50. data/lib/asciidoctor/converter/docbook5.rb +9 -25
  51. data/lib/asciidoctor/converter/html5.rb +65 -42
  52. data/lib/asciidoctor/converter/manpage.rb +13 -12
  53. data/lib/asciidoctor/converter/template.rb +6 -3
  54. data/lib/asciidoctor/document.rb +40 -48
  55. data/lib/asciidoctor/extensions.rb +3 -3
  56. data/lib/asciidoctor/helpers.rb +38 -39
  57. data/lib/asciidoctor/inline.rb +1 -1
  58. data/lib/asciidoctor/load.rb +117 -0
  59. data/lib/asciidoctor/parser.rb +29 -25
  60. data/lib/asciidoctor/path_resolver.rb +35 -25
  61. data/lib/asciidoctor/reader.rb +14 -7
  62. data/lib/asciidoctor/rx.rb +722 -0
  63. data/lib/asciidoctor/substitutors.rb +62 -40
  64. data/lib/asciidoctor/syntax_highlighter.rb +22 -8
  65. data/lib/asciidoctor/syntax_highlighter/coderay.rb +1 -1
  66. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +12 -4
  67. data/lib/asciidoctor/syntax_highlighter/prettify.rb +7 -4
  68. data/lib/asciidoctor/syntax_highlighter/pygments.rb +2 -3
  69. data/lib/asciidoctor/syntax_highlighter/rouge.rb +18 -11
  70. data/lib/asciidoctor/table.rb +49 -20
  71. data/lib/asciidoctor/version.rb +1 -1
  72. data/man/asciidoctor.1 +17 -17
  73. data/man/asciidoctor.adoc +15 -14
  74. metadata +12 -9
@@ -34,8 +34,8 @@ class Converter::TemplateConverter < Converter::Base
34
34
  }
35
35
 
36
36
  begin
37
- require 'concurrent/hash' unless defined? ::Concurrent::Hash
38
- @caches = { scans: ::Concurrent::Hash.new, templates: ::Concurrent::Hash.new }
37
+ require 'concurrent/map' unless defined? ::Concurrent::Map
38
+ @caches = { scans: ::Concurrent::Map.new, templates: ::Concurrent::Map.new }
39
39
  rescue ::LoadError
40
40
  @caches = { scans: {}, templates: {} }
41
41
  end
@@ -71,7 +71,7 @@ class Converter::TemplateConverter < Converter::Base
71
71
  end
72
72
  case opts[:template_cache]
73
73
  when true
74
- logger.warn 'optional gem \'concurrent-ruby\' is not available. This gem is recommended when using the default template cache.' unless defined? ::Concurrent::Hash
74
+ logger.warn 'optional gem \'concurrent-ruby\' is not available. This gem is recommended when using the default template cache.' unless defined? ::Concurrent::Map
75
75
  @caches = self.class.caches
76
76
  when ::Hash
77
77
  @caches = opts[:template_cache]
@@ -257,6 +257,9 @@ class Converter::TemplateConverter < Converter::Base
257
257
  if !name || name == 'erb'
258
258
  require 'erb' unless defined? ::ERB.version
259
259
  [::Tilt::ERBTemplate, {}]
260
+ elsif name == 'erubi'
261
+ Helpers.require_library 'erubi' unless defined? ::Erubis::Engine
262
+ [::Tilt::ErubiTemplate, {}]
260
263
  elsif name == 'erubis'
261
264
  Helpers.require_library 'erubis' unless defined? ::Erubis::FastEruby
262
265
  [::Tilt::ErubisTemplate, { engine_class: ::Erubis::FastEruby }]
@@ -326,13 +326,12 @@ class Document < AbstractBlock
326
326
  @sourcemap = options[:sourcemap]
327
327
  @timings = options.delete :timings
328
328
  @path_resolver = PathResolver.new
329
- initialize_extensions = (defined? ::Asciidoctor::Extensions) ? true : nil
329
+ initialize_extensions = (defined? ::Asciidoctor::Extensions) || (options.key? :extensions) ? ::Asciidoctor::Extensions : nil
330
330
  @extensions = nil # initialize furthur down if initialize_extensions is true
331
331
  options[:standalone] = options[:header_footer] if (options.key? :header_footer) && !(options.key? :standalone)
332
332
  end
333
333
 
334
- @parsed = false
335
- @header = @header_attributes = nil
334
+ @parsed = @reftexts = @header = @header_attributes = nil
336
335
  @counters = {}
337
336
  @attributes_modified = ::Set.new
338
337
  @docinfo_processor_extensions = {}
@@ -340,51 +339,36 @@ class Document < AbstractBlock
340
339
  (@options = options).freeze
341
340
 
342
341
  attrs = @attributes
343
- #attrs['encoding'] = 'UTF-8'
344
- attrs['sectids'] = ''
345
- attrs['toc-placement'] = 'auto'
342
+ attrs['attribute-undefined'] = Compliance.attribute_undefined
343
+ attrs['attribute-missing'] = Compliance.attribute_missing
344
+ attrs.update DEFAULT_ATTRIBUTES
345
+ # TODO if lang attribute is set, @safe mode < SafeMode::SERVER, and !parent_doc,
346
+ # load attributes from data/locale/attributes-<lang>.adoc
347
+
346
348
  if standalone
347
- attrs['copycss'] = ''
348
349
  # sync embedded attribute with :standalone option value
349
350
  attr_overrides['embedded'] = nil
351
+ attrs['copycss'] = ''
352
+ attrs['iconfont-remote'] = ''
353
+ attrs['stylesheet'] = ''
354
+ attrs['webfonts'] = ''
350
355
  else
351
- attrs['notitle'] = ''
352
356
  # sync embedded attribute with :standalone option value
353
357
  attr_overrides['embedded'] = ''
358
+ if (attr_overrides.key? 'showtitle') && (attr_overrides.keys & %w(notitle showtitle))[-1] == 'showtitle'
359
+ attr_overrides['notitle'] = { nil => '', false => '@', '@' => false}[attr_overrides['showtitle']]
360
+ elsif attr_overrides.key? 'notitle'
361
+ attr_overrides['showtitle'] = { nil => '', false => '@', '@' => false}[attr_overrides['notitle']]
362
+ else
363
+ attrs['notitle'] = ''
364
+ end
354
365
  end
355
- attrs['stylesheet'] = ''
356
- attrs['webfonts'] = ''
357
- attrs['prewrap'] = ''
358
- attrs['attribute-undefined'] = Compliance.attribute_undefined
359
- attrs['attribute-missing'] = Compliance.attribute_missing
360
- attrs['iconfont-remote'] = ''
361
-
362
- # language strings
363
- # TODO load these based on language settings
364
- attrs['caution-caption'] = 'Caution'
365
- attrs['important-caption'] = 'Important'
366
- attrs['note-caption'] = 'Note'
367
- attrs['tip-caption'] = 'Tip'
368
- attrs['warning-caption'] = 'Warning'
369
- attrs['example-caption'] = 'Example'
370
- attrs['figure-caption'] = 'Figure'
371
- #attrs['listing-caption'] = 'Listing'
372
- attrs['table-caption'] = 'Table'
373
- attrs['toc-title'] = 'Table of Contents'
374
- #attrs['preface-title'] = 'Preface'
375
- attrs['section-refsig'] = 'Section'
376
- attrs['part-refsig'] = 'Part'
377
- attrs['chapter-refsig'] = 'Chapter'
378
- attrs['appendix-caption'] = attrs['appendix-refsig'] = 'Appendix'
379
- attrs['untitled-label'] = 'Untitled'
380
- attrs['version-label'] = 'Version'
381
- attrs['last-update-label'] = 'Last updated'
382
366
 
383
367
  attr_overrides['asciidoctor'] = ''
384
368
  attr_overrides['asciidoctor-version'] = ::Asciidoctor::VERSION
385
369
 
386
370
  attr_overrides['safe-mode-name'] = (safe_mode_name = SafeMode.name_for_value @safe)
387
- attr_overrides["safe-mode-#{safe_mode_name}"] = ''
371
+ attr_overrides[%(safe-mode-#{safe_mode_name})] = ''
388
372
  attr_overrides['safe-mode-level'] = @safe
389
373
 
390
374
  # the only way to set the max-include-depth attribute is via the API; default to 64 like AsciiDoc Python
@@ -506,10 +490,10 @@ class Document < AbstractBlock
506
490
  ::AsciidoctorJ::Extensions::ExtensionRegistry === ext_registry)
507
491
  @extensions = ext_registry.activate self
508
492
  end
509
- elsif ::Proc === (ext_block = options[:extensions])
493
+ elsif (ext_block = options[:extensions]).nil?
494
+ @extensions = Extensions::Registry.new.activate self unless Extensions.groups.empty?
495
+ elsif ::Proc === ext_block
510
496
  @extensions = Extensions.create(&ext_block).activate self
511
- elsif !Extensions.groups.empty?
512
- @extensions = Extensions::Registry.new.activate self
513
497
  end
514
498
  end
515
499
 
@@ -607,24 +591,32 @@ class Document < AbstractBlock
607
591
  when :refs
608
592
  @catalog[:refs][value[0]] ||= (ref = value[1])
609
593
  ref
610
- #when :footnotes, :indexterms
611
594
  when :footnotes
612
595
  @catalog[type] << value
613
596
  else
614
- @catalog[type] << (type == :images ? (ImageReference.new value[0], value[1]) : value) if @options[:catalog_assets]
597
+ @catalog[type] << (type == :images ? (ImageReference.new value, @attributes['imagesdir']) : value) if @options[:catalog_assets]
615
598
  end
616
599
  end
617
600
 
618
- # Public: Scan all registered references and return the ID of the reference that matches the specified reference text.
619
- #
620
- # If multiple references in the document have the same reference text, the first match in document order is used.
601
+ # Public: Scan registered references and return the ID of the first reference that matches the specified reference text.
621
602
  #
622
603
  # text - The String reference text to compare to the converted reference text of each registered reference.
623
604
  #
624
605
  # Returns the String ID of the first reference with matching reference text or nothing if no reference is found.
625
606
  def resolve_id text
626
- ((@reftexts ||= @parsed ? {}.tap {|accum| @catalog[:refs].each {|id, ref| accum[ref.xreftext] = id } } : nil) ||
627
- {}.tap {|accum| @catalog[:refs].find {|id, ref| ref.xreftext == text ? accum[text] = id : nil } })[text]
607
+ if @reftexts
608
+ @reftexts[text]
609
+ elsif @parsed
610
+ # @reftexts is set eagerly to prevent nested lazy init
611
+ (@reftexts = {}).tap {|accum| @catalog[:refs].each {|id, ref| accum[ref.xreftext] ||= id } }[text]
612
+ else
613
+ # @reftexts is set eagerly to prevent nested lazy init
614
+ resolved_id = nil
615
+ # NOTE short-circuit early since we're throwing away this table
616
+ (@reftexts = {}).tap {|accum| @catalog[:refs].each {|id, ref| (xreftext = ref.xreftext) == text ? (break (resolved_id = id)) : (accum[xreftext] ||= id) } }
617
+ @reftexts = nil
618
+ resolved_id
619
+ end
628
620
  end
629
621
 
630
622
  def footnotes?
@@ -765,7 +757,7 @@ class Document < AbstractBlock
765
757
  end
766
758
 
767
759
  def notitle
768
- !@attributes.key?('showtitle') && @attributes.key?('notitle')
760
+ @attributes.key? 'notitle'
769
761
  end
770
762
 
771
763
  def noheader
@@ -1210,7 +1202,7 @@ class Document < AbstractBlock
1210
1202
  when '', 'font'
1211
1203
  else
1212
1204
  attrs['icons'] = ''
1213
- attrs['icontype'] = icons_val
1205
+ attrs['icontype'] = icons_val unless icons_val == 'image'
1214
1206
  end
1215
1207
  end
1216
1208
 
@@ -425,7 +425,7 @@ module Extensions
425
425
  # TIP: Postprocessors can also be used to relocate assets needed by the published
426
426
  # document.
427
427
  #
428
- # Postprocessor implementations must Postprocessor.
428
+ # Postprocessor implementations must extend Postprocessor.
429
429
  class Postprocessor < Processor
430
430
  def process document, output
431
431
  raise ::NotImplementedError, %(#{Postprocessor} subclass #{self.class} must implement the ##{__method__} method)
@@ -610,7 +610,7 @@ module Extensions
610
610
  #--
611
611
  # TODO break this out into different pattern types
612
612
  # for example, FullInlineMacro, ShortInlineMacro (no target) and other patterns
613
- # FIXME for inline passthrough, we need to have some way to specify the text as a passthrough
613
+ # FIXME for inline macro, we need to have some way to specify the text as a passthrough
614
614
  class InlineMacroProcessor < MacroProcessor
615
615
  @@rx_cache = {}
616
616
 
@@ -622,7 +622,7 @@ module Extensions
622
622
 
623
623
  def resolve_regexp name, format
624
624
  raise ::ArgumentError, %(invalid name for inline macro: #{name}) unless MacroNameRx.match? name
625
- @@rx_cache[[name, format]] ||= /\\?#{name}:#{format == :short ? '(){0}' : '(\S+?)'}\[(|.*?[^\\])\]/
625
+ @@rx_cache[[name, format]] ||= /\\?#{name}:#{format == :short ? '(){0}' : '(\S+?)'}\[(|#{CC_ANY}*?[^\\])\]/
626
626
  end
627
627
  end
628
628
 
@@ -2,7 +2,9 @@
2
2
  module Asciidoctor
3
3
  # Internal: Except where noted, a module that contains internal helper functions.
4
4
  module Helpers
5
- # Internal: Require the specified library using Kernel#require.
5
+ module_function
6
+
7
+ # Public: Require the specified library using Kernel#require.
6
8
  #
7
9
  # Attempts to load the library specified in the first argument using the
8
10
  # Kernel#require. Rescues the LoadError if the library is not available and
@@ -21,7 +23,7 @@ module Helpers
21
23
  # Otherwise, if on_failure is :abort, Kernel#raise is called with an appropriate message.
22
24
  # Otherwise, if on_failure is :warn, Kernel#warn is called with an appropriate message and nil returned.
23
25
  # Otherwise, nil is returned.
24
- def self.require_library name, gem_name = true, on_failure = :abort
26
+ def require_library name, gem_name = true, on_failure = :abort
25
27
  require name
26
28
  rescue ::LoadError
27
29
  include Logging unless include? Logging
@@ -54,25 +56,27 @@ module Helpers
54
56
  # If a BOM is found at the beginning of the data, a best attempt is made to
55
57
  # encode it to UTF-8 from the specified source encoding.
56
58
  #
57
- # data - the source data Array to prepare (no nil entries allowed)
59
+ # data - the source data Array to prepare (no nil entries allowed)
60
+ # trim_end - whether to trim whitespace from the end of each line;
61
+ # (true cleans all whitespace; false only removes trailing newline) (default: true)
58
62
  #
59
63
  # returns a String Array of prepared lines
60
- def self.prepare_source_array data
64
+ def prepare_source_array data, trim_end = true
61
65
  return [] if data.empty?
62
66
  if (leading_2_bytes = (leading_bytes = (first = data[0]).unpack 'C3').slice 0, 2) == BOM_BYTES_UTF_16LE
63
67
  data[0] = first.byteslice 2, first.bytesize
64
68
  # NOTE you can't split a UTF-16LE string using .lines when encoding is UTF-8; doing so will cause this line to fail
65
- return data.map {|line| (line.encode UTF_8, ::Encoding::UTF_16LE).rstrip }
69
+ return trim_end ? data.map {|line| (line.encode UTF_8, ::Encoding::UTF_16LE).rstrip } : data.map {|line| (line.encode UTF_8, ::Encoding::UTF_16LE).chomp }
66
70
  elsif leading_2_bytes == BOM_BYTES_UTF_16BE
67
71
  data[0] = first.byteslice 2, first.bytesize
68
- return data.map {|line| (line.encode UTF_8, ::Encoding::UTF_16BE).rstrip }
72
+ return trim_end ? data.map {|line| (line.encode UTF_8, ::Encoding::UTF_16BE).rstrip } : data.map {|line| (line.encode UTF_8, ::Encoding::UTF_16BE).chomp }
69
73
  elsif leading_bytes == BOM_BYTES_UTF_8
70
74
  data[0] = first.byteslice 3, first.bytesize
71
75
  end
72
76
  if first.encoding == UTF_8
73
- data.map {|line| line.rstrip }
77
+ trim_end ? data.map {|line| line.rstrip } : data.map {|line| line.chomp }
74
78
  else
75
- data.map {|line| (line.encode UTF_8).rstrip }
79
+ trim_end ? data.map {|line| (line.encode UTF_8).rstrip } : data.map {|line| (line.encode UTF_8).chomp }
76
80
  end
77
81
  end
78
82
 
@@ -84,10 +88,12 @@ module Helpers
84
88
  # If a BOM is found at the beginning of the data, a best attempt is made to
85
89
  # encode it to UTF-8 from the specified source encoding.
86
90
  #
87
- # data - the source data String to prepare
91
+ # data - the source data String to prepare
92
+ # trim_end - whether to trim whitespace from the end of each line;
93
+ # (true cleans all whitespace; false only removes trailing newline) (default: true)
88
94
  #
89
95
  # returns a String Array of prepared lines
90
- def self.prepare_source_string data
96
+ def prepare_source_string data, trim_end = true
91
97
  return [] if data.nil_or_empty?
92
98
  if (leading_2_bytes = (leading_bytes = data.unpack 'C3').slice 0, 2) == BOM_BYTES_UTF_16LE
93
99
  data = (data.byteslice 2, data.bytesize).encode UTF_8, ::Encoding::UTF_16LE
@@ -99,7 +105,11 @@ module Helpers
99
105
  elsif data.encoding != UTF_8
100
106
  data = data.encode UTF_8
101
107
  end
102
- [].tap {|lines| data.each_line {|line| lines << line.rstrip } }
108
+ if trim_end
109
+ [].tap {|lines| data.each_line {|line| lines << line.rstrip } }
110
+ else
111
+ [].tap {|lines| data.each_line {|line| lines << line.chomp } }
112
+ end
103
113
  end
104
114
 
105
115
  # Internal: Efficiently checks whether the specified String resembles a URI
@@ -110,29 +120,17 @@ module Helpers
110
120
  # str - the String to check
111
121
  #
112
122
  # returns true if the String is a URI, false if it is not
113
- def self.uriish? str
123
+ def uriish? str
114
124
  (str.include? ':') && (UriSniffRx.match? str)
115
125
  end
116
126
 
117
- # Internal: Efficiently retrieves the URI prefix of the specified String
118
- #
119
- # Uses the Asciidoctor::UriSniffRx regex to match the URI prefix in the
120
- # specified String (e.g., http://), if present.
121
- #
122
- # str - the String to check
123
- #
124
- # returns the string URI prefix if the string is a URI, otherwise nil
125
- def self.uri_prefix str
126
- (str.include? ':') && UriSniffRx =~ str ? $& : nil
127
- end
128
-
129
127
  # Internal: Encode a URI component String for safe inclusion in a URI.
130
128
  #
131
129
  # str - the URI component String to encode
132
130
  #
133
131
  # Returns the String with all reserved URI characters encoded (e.g., /, &, =, space, etc).
134
132
  if RUBY_ENGINE == 'opal'
135
- def self.encode_uri_component str
133
+ def encode_uri_component str
136
134
  # patch necessary to adhere with RFC-3986 (and thus CGI.escape)
137
135
  # see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Description
138
136
  %x(
@@ -143,17 +141,17 @@ module Helpers
143
141
  end
144
142
  else
145
143
  CGI = ::CGI
146
- def self.encode_uri_component str
144
+ def encode_uri_component str
147
145
  CGI.escape str
148
146
  end
149
147
  end
150
148
 
151
- # Internal: Encode a URI String (namely the path portion).
149
+ # Internal: Apply URI path encoding to spaces in the specified string (i.e., convert spaces to %20).
152
150
  #
153
151
  # str - the String to encode
154
152
  #
155
- # Returns the String with all spaces replaced with %20.
156
- def self.encode_uri str
153
+ # Returns the specified String with all spaces replaced with %20.
154
+ def encode_spaces_in_uri str
157
155
  (str.include? ' ') ? (str.gsub ' ', '%20') : str
158
156
  end
159
157
 
@@ -167,7 +165,7 @@ module Helpers
167
165
  # # => "part1/chapter1"
168
166
  #
169
167
  # Returns the String filename with the file extension removed
170
- def self.rootname filename
168
+ def rootname filename
171
169
  if (last_dot_idx = filename.rindex '.')
172
170
  (filename.index '/', last_dot_idx) ? filename : (filename.slice 0, last_dot_idx)
173
171
  else
@@ -190,7 +188,7 @@ module Helpers
190
188
  # # => "tiger"
191
189
  #
192
190
  # Returns the String filename with leading directories removed and, if specified, the extension removed
193
- def self.basename filename, drop_ext = nil
191
+ def basename filename, drop_ext = nil
194
192
  if drop_ext
195
193
  ::File.basename filename, (drop_ext == true ? (extname filename) : drop_ext)
196
194
  else
@@ -203,7 +201,7 @@ module Helpers
203
201
  # path - The path String to check; expects a posix path
204
202
  #
205
203
  # Returns true if the path has a file extension, false otherwise
206
- def self.extname? path
204
+ def extname? path
207
205
  (last_dot_idx = path.rindex '.') && !(path.index '/', last_dot_idx)
208
206
  end
209
207
 
@@ -217,7 +215,7 @@ module Helpers
217
215
  #
218
216
  # Returns the String file extension (with the leading dot included) or the fallback value if the path has no file extension.
219
217
  if ::File::ALT_SEPARATOR
220
- def self.extname path, fallback = ''
218
+ def extname path, fallback = ''
221
219
  if (last_dot_idx = path.rindex '.')
222
220
  (path.index '/', last_dot_idx) || (path.index ::File::ALT_SEPARATOR, last_dot_idx) ? fallback : (path.slice last_dot_idx, path.length)
223
221
  else
@@ -225,7 +223,7 @@ module Helpers
225
223
  end
226
224
  end
227
225
  else
228
- def self.extname path, fallback = ''
226
+ def extname path, fallback = ''
229
227
  if (last_dot_idx = path.rindex '.')
230
228
  (path.index '/', last_dot_idx) ? fallback : (path.slice last_dot_idx, path.length)
231
229
  else
@@ -235,7 +233,7 @@ module Helpers
235
233
  end
236
234
 
237
235
  # Internal: Make a directory, ensuring all parent directories exist.
238
- def self.mkdir_p dir
236
+ def mkdir_p dir
239
237
  unless ::File.directory? dir
240
238
  unless (parent_dir = ::File.dirname dir) == '.'
241
239
  mkdir_p parent_dir
@@ -252,13 +250,14 @@ module Helpers
252
250
  'M' => 1000, 'CM' => 900, 'D' => 500, 'CD' => 400, 'C' => 100, 'XC' => 90,
253
251
  'L' => 50, 'XL' => 40, 'X' => 10, 'IX' => 9, 'V' => 5, 'IV' => 4, 'I' => 1
254
252
  }
253
+ private_constant :ROMAN_NUMERALS
255
254
 
256
255
  # Internal: Converts an integer to a Roman numeral.
257
256
  #
258
257
  # val - the [Integer] value to convert
259
258
  #
260
259
  # Returns the [String] roman numeral for this integer
261
- def self.int_to_roman val
260
+ def int_to_roman val
262
261
  ROMAN_NUMERALS.map do |l, i|
263
262
  repeat, val = val.divmod i
264
263
  l * repeat
@@ -272,7 +271,7 @@ module Helpers
272
271
  # current - the value to increment as a String or Integer
273
272
  #
274
273
  # returns the next value in the sequence according to the current value's type
275
- def self.nextval current
274
+ def nextval current
276
275
  if ::Integer === current
277
276
  current + 1
278
277
  else
@@ -291,14 +290,14 @@ module Helpers
291
290
  #
292
291
  # Returns a Class if the specified object is a Class (but not a Module) or
293
292
  # a String that resolves to a Class; otherwise, nil
294
- def self.resolve_class object
293
+ def resolve_class object
295
294
  ::Class === object ? object : (::String === object ? (class_for_name object) : nil)
296
295
  end
297
296
 
298
297
  # Internal: Resolves a Class object (not a Module) for the qualified name.
299
298
  #
300
299
  # Returns Class
301
- def self.class_for_name qualified_name
300
+ def class_for_name qualified_name
302
301
  raise unless ::Class === (resolved = ::Object.const_get qualified_name, false)
303
302
  resolved
304
303
  rescue
@@ -39,7 +39,7 @@ class Inline < AbstractNode
39
39
  #
40
40
  # Returns the [String] value of the alt attribute.
41
41
  def alt
42
- attr 'alt'
42
+ (attr 'alt') || ''
43
43
  end
44
44
 
45
45
  # For a reference node (:ref or :bibref), the text is the reftext (and the reftext attribute is not set).
@@ -0,0 +1,117 @@
1
+ module Asciidoctor
2
+ class << self
3
+ # Public: Parse the AsciiDoc source input into a {Document}
4
+ #
5
+ # Accepts input as an IO (or StringIO), String or String Array object. If the
6
+ # input is a File, the object is expected to be opened for reading and is not
7
+ # closed afterwards by this method. Information about the file (filename,
8
+ # directory name, etc) gets assigned to attributes on the Document object.
9
+ #
10
+ # input - the AsciiDoc source as a IO, String or Array.
11
+ # options - a String, Array or Hash of options to control processing (default: {})
12
+ # String and Array values are converted into a Hash.
13
+ # See {Document#initialize} for details about these options.
14
+ #
15
+ # Returns the Document
16
+ def load input, options = {}
17
+ options = options.merge
18
+
19
+ if (timings = options[:timings])
20
+ timings.start :read
21
+ end
22
+
23
+ if (logger = options[:logger]) && logger != LoggerManager.logger
24
+ LoggerManager.logger = logger
25
+ end
26
+
27
+ if !(attrs = options[:attributes])
28
+ attrs = {}
29
+ elsif ::Hash === attrs
30
+ attrs = attrs.merge
31
+ elsif (defined? ::Java::JavaUtil::Map) && ::Java::JavaUtil::Map === attrs
32
+ attrs = attrs.dup
33
+ elsif ::Array === attrs
34
+ attrs = {}.tap do |accum|
35
+ attrs.each do |entry|
36
+ k, _, v = entry.partition '='
37
+ accum[k] = v
38
+ end
39
+ end
40
+ elsif ::String === attrs
41
+ # condense and convert non-escaped spaces to null, unescape escaped spaces, then split on null
42
+ attrs = {}.tap do |accum|
43
+ attrs.gsub(SpaceDelimiterRx, '\1' + NULL).gsub(EscapedSpaceRx, '\1').split(NULL).each do |entry|
44
+ k, _, v = entry.partition '='
45
+ accum[k] = v
46
+ end
47
+ end
48
+ elsif (attrs.respond_to? :keys) && (attrs.respond_to? :[])
49
+ # coerce attrs to a real Hash
50
+ attrs = {}.tap {|accum| attrs.keys.each {|k| accum[k] = attrs[k] } }
51
+ else
52
+ raise ::ArgumentError, %(illegal type for attributes option: #{attrs.class.ancestors.join ' < '})
53
+ end
54
+
55
+ if ::File === input
56
+ options[:input_mtime] = input.mtime
57
+ # NOTE defer setting infile and indir until we get a better sense of their purpose
58
+ # TODO cli checks if input path can be read and is file, but might want to add check to API too
59
+ attrs['docfile'] = input_path = ::File.absolute_path input.path
60
+ attrs['docdir'] = ::File.dirname input_path
61
+ attrs['docname'] = Helpers.basename input_path, (attrs['docfilesuffix'] = Helpers.extname input_path)
62
+ source = input.read
63
+ elsif input.respond_to? :read
64
+ # NOTE tty, pipes & sockets can't be rewound, but can't be sniffed easily either
65
+ # just fail the rewind operation silently to handle all cases
66
+ input.rewind rescue nil
67
+ source = input.read
68
+ elsif ::String === input
69
+ source = input
70
+ elsif ::Array === input
71
+ source = input.drop 0
72
+ elsif input
73
+ raise ::ArgumentError, %(unsupported input type: #{input.class})
74
+ end
75
+
76
+ if timings
77
+ timings.record :read
78
+ timings.start :parse
79
+ end
80
+
81
+ options[:attributes] = attrs
82
+ doc = options[:parse] == false ? (Document.new source, options) : (Document.new source, options).parse
83
+
84
+ timings.record :parse if timings
85
+ doc
86
+ rescue => ex
87
+ begin
88
+ context = %(asciidoctor: FAILED: #{attrs['docfile'] || '<stdin>'}: Failed to load AsciiDoc document)
89
+ if ex.respond_to? :exception
90
+ # The original message must be explicitly preserved when wrapping a Ruby exception
91
+ wrapped_ex = ex.exception %(#{context} - #{ex.message})
92
+ # JRuby automatically sets backtrace; MRI did not until 2.6
93
+ wrapped_ex.set_backtrace ex.backtrace
94
+ else
95
+ # Likely a Java exception class
96
+ wrapped_ex = ex.class.new context, ex
97
+ wrapped_ex.stack_trace = ex.stack_trace
98
+ end
99
+ rescue
100
+ wrapped_ex = ex
101
+ end
102
+ raise wrapped_ex
103
+ end
104
+
105
+ # Public: Parse the contents of the AsciiDoc source file into an Asciidoctor::Document
106
+ #
107
+ # input - the String AsciiDoc source filename
108
+ # options - a String, Array or Hash of options to control processing (default: {})
109
+ # String and Array values are converted into a Hash.
110
+ # See Asciidoctor::Document#initialize for details about options.
111
+ #
112
+ # Returns the Asciidoctor::Document
113
+ def load_file filename, options = {}
114
+ ::File.open(filename, FILE_READ_MODE) {|file| load file, options }
115
+ end
116
+ end
117
+ end