asciidoctor 2.0.6 → 2.0.11

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