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
@@ -38,12 +38,13 @@ class AbstractBlock < AbstractNode
38
38
  @blocks = []
39
39
  @subs = []
40
40
  @id = @title = @caption = @numeral = @style = @default_subs = @source_location = nil
41
- case context
42
- when :document, :section
41
+ if context == :document || context == :section
43
42
  @level = @next_section_index = 0
44
43
  @next_section_ordinal = 1
44
+ elsif AbstractBlock === parent
45
+ @level = parent.level
45
46
  else
46
- @level = AbstractBlock === parent ? parent.level : nil
47
+ @level = nil
47
48
  end
48
49
  end
49
50
 
@@ -140,6 +141,11 @@ class AbstractBlock < AbstractNode
140
141
  (Integer @numeral) rescue @numeral
141
142
  end
142
143
 
144
+ # Deprecated: Legacy property to set the numeral of this section by coercing the value to a String.
145
+ def number= val
146
+ @numeral = val.to_s
147
+ end
148
+
143
149
  # Public: Walk the document tree and find all block-level nodes that match the specified selector (context, style, id,
144
150
  # role, and/or custom filter).
145
151
  #
@@ -222,6 +228,8 @@ class AbstractBlock < AbstractNode
222
228
  text = sub_specialchars text
223
229
  (ReplaceableTextRx.match? text) ? (sub_replacements text) : text
224
230
  end
231
+ else
232
+ ''
225
233
  end
226
234
  end
227
235
 
@@ -335,17 +343,17 @@ class AbstractBlock < AbstractNode
335
343
  if (val = reftext) && !val.empty?
336
344
  val
337
345
  # NOTE xrefstyle only applies to blocks with a title and a caption or number
338
- elsif xrefstyle && @title && @caption
346
+ elsif xrefstyle && @title && !@caption.nil_or_empty?
339
347
  case xrefstyle
340
348
  when 'full'
341
349
  quoted_title = sub_placeholder (sub_quotes @document.compat_mode ? %q(``%s'') : '"`%s`"'), title
342
- if @numeral && (caption_attr_name = CAPTION_ATTR_NAMES[@context]) && (prefix = @document.attributes[caption_attr_name])
350
+ if @numeral && (caption_attr_name = CAPTION_ATTRIBUTE_NAMES[@context]) && (prefix = @document.attributes[caption_attr_name])
343
351
  %(#{prefix} #{@numeral}, #{quoted_title})
344
352
  else
345
353
  %(#{@caption.chomp '. '}, #{quoted_title})
346
354
  end
347
355
  when 'short'
348
- if @numeral && (caption_attr_name = CAPTION_ATTR_NAMES[@context]) && (prefix = @document.attributes[caption_attr_name])
356
+ if @numeral && (caption_attr_name = CAPTION_ATTRIBUTE_NAMES[@context]) && (prefix = @document.attributes[caption_attr_name])
349
357
  %(#{prefix} #{@numeral})
350
358
  else
351
359
  @caption.chomp '. '
@@ -369,15 +377,15 @@ class AbstractBlock < AbstractNode
369
377
  # The parts of a complete caption are: <prefix> <number>. <title>
370
378
  # This partial caption represents the part the precedes the title.
371
379
  #
372
- # value - The explicit String caption to assign to this block (default: nil).
373
- # caption_context - The Symbol context to use when resolving caption-related attributes.
374
- # If not provided, the name of the context for this block is used.
380
+ # value - The String caption to assign to this block or nil to use document attribute.
381
+ # caption_context - The Symbol context to use when resolving caption-related attributes. If not provided, the name of
382
+ # the context for this block is used. Only certain contexts allow the caption to be looked up.
375
383
  # (default: @context)
376
384
  #
377
385
  # Returns nothing.
378
- def assign_caption value = nil, caption_context = @context
386
+ def assign_caption value, caption_context = @context
379
387
  unless @caption || !@title || (@caption = value || @document.attributes['caption'])
380
- if (attr_name = CAPTION_ATTR_NAMES[caption_context]) && (prefix = @document.attributes[attr_name])
388
+ if (attr_name = CAPTION_ATTRIBUTE_NAMES[caption_context]) && (prefix = @document.attributes[attr_name])
381
389
  @caption = %(#{prefix} #{@numeral = @document.increment_and_store_counter %(#{caption_context}-number), self}. )
382
390
  nil
383
391
  end
@@ -65,7 +65,7 @@ class AbstractNode
65
65
  #
66
66
  # parent - The Block to set as the parent of this Block
67
67
  #
68
- # Returns the new parent Block associated with this Block
68
+ # Returns the value of the parent argument
69
69
  def parent= parent
70
70
  @parent, @document = parent, parent.document
71
71
  end
@@ -161,10 +161,10 @@ class AbstractNode
161
161
  nil
162
162
  end
163
163
 
164
- # Public: Retrieve the Set of option names that are set on this node
164
+ # Public: Retrieve the Set of option names that are enabled on this node
165
165
  #
166
166
  # Returns a [Set] of option names
167
- def options
167
+ def enabled_options
168
168
  ::Set.new.tap {|accum| @attributes.each_key {|k| accum << (k.slice 0, k.length - 7) if k.to_s.end_with? '-option' } }
169
169
  end
170
170
 
@@ -216,6 +216,15 @@ class AbstractNode
216
216
  (val = @attributes['role']) ? (%( #{val} ).include? %( #{name} )) : false
217
217
  end
218
218
 
219
+ # Public: Sets the value of the role attribute on this ndoe.
220
+ #
221
+ # names - A single role name, a space-separated String of role names, or an Array of role names
222
+ #
223
+ # Returns the value of the names argument
224
+ def role= names
225
+ @attributes['role'] = (::Array === names) ? (names.join ' ') : names
226
+ end
227
+
219
228
  # Public: Adds the given role directly to this node.
220
229
  #
221
230
  # Returns a [Boolean] indicating whether the role was added.
@@ -310,14 +319,10 @@ class AbstractNode
310
319
  # Returns A String reference or data URI for the target image
311
320
  def image_uri(target_image, asset_dir_key = 'imagesdir')
312
321
  if (doc = @document).safe < SafeMode::SECURE && (doc.attr? 'data-uri')
313
- if ((Helpers.uriish? target_image) && (target_image = Helpers.encode_uri target_image)) ||
322
+ if ((Helpers.uriish? target_image) && (target_image = Helpers.encode_spaces_in_uri target_image)) ||
314
323
  (asset_dir_key && (images_base = doc.attr asset_dir_key) && (Helpers.uriish? images_base) &&
315
324
  (target_image = normalize_web_path target_image, images_base, false))
316
- if doc.attr? 'allow-uri-read'
317
- generate_data_uri_from_uri target_image, (doc.attr? 'cache-uri')
318
- else
319
- target_image
320
- end
325
+ (doc.attr? 'allow-uri-read') ? (generate_data_uri_from_uri target_image, (doc.attr? 'cache-uri')) : target_image
321
326
  else
322
327
  generate_data_uri target_image, asset_dir_key
323
328
  end
@@ -474,7 +479,7 @@ class AbstractNode
474
479
  # Returns the resolved [String] path
475
480
  def normalize_web_path(target, start = nil, preserve_uri_target = true)
476
481
  if preserve_uri_target && (Helpers.uriish? target)
477
- Helpers.encode_uri target
482
+ Helpers.encode_spaces_in_uri target
478
483
  else
479
484
  @document.path_resolver.web_path target, start
480
485
  end
@@ -518,6 +523,7 @@ class AbstractNode
518
523
  # * :normalize a Boolean that indicates whether the data should be normalized (default: false)
519
524
  # * :start the String relative base path to use when resolving the target (default: nil)
520
525
  # * :warn_on_failure a Boolean that indicates whether warnings are issued if the target cannot be read (default: true)
526
+ # * :warn_if_empty a Boolean that indicates whether a warning is issued if contents of target is empty (default: false)
521
527
  # Returns the contents of the resolved target or nil if the resolved target cannot be read
522
528
  # --
523
529
  # TODO refactor other methods in this class to use this method were possible (repurposing if necessary)
@@ -529,22 +535,22 @@ class AbstractNode
529
535
  Helpers.require_library 'open-uri/cached', 'open-uri-cached' if doc.attr? 'cache-uri'
530
536
  begin
531
537
  if opts[:normalize]
532
- (Helpers.prepare_source_string ::OpenURI.open_uri(target, URI_READ_MODE) {|f| f.read }).join LF
538
+ contents = (Helpers.prepare_source_string ::OpenURI.open_uri(target, URI_READ_MODE) {|f| f.read }).join LF
533
539
  else
534
- ::OpenURI.open_uri(target, URI_READ_MODE) {|f| f.read }
540
+ contents = ::OpenURI.open_uri(target, URI_READ_MODE) {|f| f.read }
535
541
  end
536
542
  rescue
537
543
  logger.warn %(could not retrieve contents of #{opts[:label] || 'asset'} at URI: #{target}) if opts.fetch :warn_on_failure, true
538
- return
539
544
  end
540
545
  else
541
546
  logger.warn %(cannot retrieve contents of #{opts[:label] || 'asset'} at URI: #{target} (allow-uri-read attribute not enabled)) if opts.fetch :warn_on_failure, true
542
- return
543
547
  end
544
548
  else
545
549
  target = normalize_system_path target, opts[:start], nil, target_name: (opts[:label] || 'asset')
546
- read_asset target, normalize: opts[:normalize], warn_on_failure: (opts.fetch :warn_on_failure, true), label: opts[:label]
550
+ contents = read_asset target, normalize: opts[:normalize], warn_on_failure: (opts.fetch :warn_on_failure, true), label: opts[:label]
547
551
  end
552
+ logger.warn %(contents of #{opts[:label] || 'asset'} is empty: #{target}) if contents && opts[:warn_if_empty] && contents.empty?
553
+ contents
548
554
  end
549
555
 
550
556
  # Deprecated: Check whether the specified String is a URI by
@@ -22,19 +22,20 @@ module Asciidoctor
22
22
  # => { 'style' => 'quote', 'attribution' => 'Famous Person', 'citetitle' => 'Famous Book (2001)' }
23
23
  #
24
24
  class AttributeList
25
- BACKSLASH = '\\'
26
25
  APOS = '\''
26
+ BACKSLASH = '\\'
27
+ QUOT = '"'
27
28
 
28
29
  # Public: Regular expressions for detecting the boundary of a value
29
30
  BoundaryRxs = {
30
- '"' => /.*?[^\\](?=")/,
31
+ QUOT => /.*?[^\\](?=")/,
31
32
  APOS => /.*?[^\\](?=')/,
32
33
  ',' => /.*?(?=[ \t]*(,|$))/
33
34
  }
34
35
 
35
36
  # Public: Regular expressions for unescaping quoted characters
36
37
  EscapedQuotes = {
37
- '"' => '\\"',
38
+ QUOT => '\\"',
38
39
  APOS => '\\\''
39
40
  }
40
41
 
@@ -45,7 +46,9 @@ class AttributeList
45
46
  BlankRx = /[ \t]+/
46
47
 
47
48
  # Public: Regular expressions for skipping delimiters
48
- SkipRxs = { ',' => /[ \t]*(,|$)/ }
49
+ SkipRxs = {
50
+ ',' => /[ \t]*(,|$)/
51
+ }
49
52
 
50
53
  def initialize source, block = nil, delimiter = ','
51
54
  @scanner = ::StringScanner.new source
@@ -65,8 +68,6 @@ class AttributeList
65
68
  return @attributes if @attributes
66
69
 
67
70
  @attributes = {}
68
- # QUESTION do we want to store the attribute list as the zero-index attribute?
69
- #attributes[0] = @scanner.string
70
71
  index = 0
71
72
 
72
73
  while parse_attribute index, positional_attrs
@@ -83,75 +84,73 @@ class AttributeList
83
84
  end
84
85
 
85
86
  def self.rekey attributes, positional_attrs
86
- index = 0
87
- positional_attrs.each do |key|
88
- index += 1
89
- if (val = attributes[index])
87
+ positional_attrs.each_with_index do |key, index|
88
+ if key && (val = attributes[index + 1])
90
89
  # QUESTION should we delete the positional key?
91
90
  attributes[key] = val
92
- end if key
91
+ end
93
92
  end
94
93
  attributes
95
94
  end
96
95
 
97
96
  private
98
97
 
99
- def parse_attribute index = 0, positional_attrs = []
100
- single_quoted_value = false
98
+ def parse_attribute index, positional_attrs
99
+ continue = true
101
100
  skip_blank
102
- # example: "quote"
103
- if (first = @scanner.peek(1)) == '"'
101
+ case @scanner.peek 1
102
+ # example: "quote" || "foo
103
+ when QUOT
104
104
  name = parse_attribute_value @scanner.get_byte
105
- value = nil
106
- # example: 'quote'
107
- elsif first == APOS
105
+ # example: 'quote' || 'foo
106
+ when APOS
108
107
  name = parse_attribute_value @scanner.get_byte
109
- value = nil
110
- single_quoted_value = true unless name.start_with? APOS
108
+ single_quoted = true unless name.start_with? APOS
111
109
  else
112
- name = scan_name
113
-
114
- skipped = 0
115
- c = nil
110
+ skipped = ((name = scan_name) && skip_blank) || 0
116
111
  if @scanner.eos?
117
- return false unless name
118
- else
119
- skipped = skip_blank || 0
120
- c = @scanner.get_byte
121
- end
122
-
123
- # example: quote
124
- if !c || c == @delimiter
125
- value = nil
126
- # example: Sherlock Holmes || =foo=
127
- elsif c != '=' || !name
128
- name = %(#{name}#{' ' * skipped}#{c}#{scan_to_delimiter})
129
- value = nil
130
- else
131
- skip_blank
132
- if @scanner.peek(1)
133
- # example: foo="bar" || foo="ba\"zaar"
134
- if (c = @scanner.get_byte) == '"'
112
+ return unless name || (@scanner.string.rstrip.end_with? @delimiter)
113
+ # example: quote (at eos)
114
+ continue = nil
115
+ # example: quote,
116
+ elsif (c = @scanner.get_byte) == @delimiter
117
+ @scanner.unscan
118
+ elsif name
119
+ # example: foo=...
120
+ if c == '='
121
+ skip_blank
122
+ case (c = @scanner.get_byte)
123
+ # example: foo="bar" || foo="ba\"zaar" || foo="bar
124
+ when QUOT
135
125
  value = parse_attribute_value c
136
- # example: foo='bar' || foo='ba\'zaar' || foo='ba"zaar'
137
- elsif c == APOS
126
+ # example: foo='bar' || foo='ba\'zaar' || foo='ba"zaar' || foo='bar
127
+ when APOS
138
128
  value = parse_attribute_value c
139
- single_quoted_value = true unless value.start_with? APOS
129
+ single_quoted = true unless value.start_with? APOS
140
130
  # example: foo=,
141
- elsif c == @delimiter
131
+ when @delimiter
132
+ value = ''
133
+ @scanner.unscan
134
+ # example: foo= (at eos)
135
+ when nil
142
136
  value = ''
143
- # example: foo=bar (all spaces ignored)
137
+ # example: foo=bar || foo=None
144
138
  else
145
139
  value = %(#{c}#{scan_to_delimiter})
146
140
  return true if value == 'None'
147
141
  end
142
+ # example: foo bar
143
+ else
144
+ name = %(#{name}#{' ' * skipped}#{c}#{scan_to_delimiter})
148
145
  end
146
+ # example: =foo= || !foo
147
+ else
148
+ name = %(#{c}#{scan_to_delimiter})
149
149
  end
150
150
  end
151
151
 
152
152
  if value
153
- # example: options="opt1,opt2,opt3"
154
- # opts is an alias for options
153
+ # example: options="opt1,opt2,opt3" || opts="opts1,opt2,opt3"
155
154
  case name
156
155
  when 'options', 'opts'
157
156
  if value.include? ','
@@ -161,7 +160,7 @@ class AttributeList
161
160
  @attributes[%(#{value}-option)] = '' unless value.empty?
162
161
  end
163
162
  else
164
- if single_quoted_value && @block
163
+ if single_quoted && @block
165
164
  case name
166
165
  when 'title', 'reftext'
167
166
  @attributes[name] = value
@@ -173,33 +172,26 @@ class AttributeList
173
172
  end
174
173
  end
175
174
  else
176
- resolved_name = single_quoted_value && @block ? (@block.apply_subs name) : name
175
+ name = @block.apply_subs name if single_quoted && @block
177
176
  if (positional_attr_name = positional_attrs[index])
178
- @attributes[positional_attr_name] = resolved_name
177
+ @attributes[positional_attr_name] = name
179
178
  end
180
- # QUESTION should we always assign the positional key?
181
- @attributes[index + 1] = resolved_name
182
- # QUESTION should we assign the resolved name as an attribute?
183
- #@attributes[resolved_name] = nil
179
+ # QUESTION should we assign the positional key even when it's claimed by a positional attribute?
180
+ @attributes[index + 1] = name
184
181
  end
185
182
 
186
- true
183
+ continue
187
184
  end
188
185
 
189
186
  def parse_attribute_value quote
190
187
  # empty quoted value
191
- if @scanner.peek(1) == quote
188
+ if (@scanner.peek 1) == quote
192
189
  @scanner.get_byte
193
- return ''
194
- end
195
-
196
- if (value = scan_to_quote quote)
190
+ ''
191
+ elsif (value = scan_to_quote quote)
197
192
  @scanner.get_byte
198
- if value.include? BACKSLASH
199
- value.gsub EscapedQuotes[quote], quote
200
- else
201
- value
202
- end
193
+ (value.include? BACKSLASH) ? (value.gsub EscapedQuotes[quote], quote) : value
194
+ # leading quote only
203
195
  else
204
196
  %(#{quote}#{scan_to_delimiter})
205
197
  end
@@ -42,6 +42,8 @@ module Asciidoctor
42
42
  non_posix_env = ::File::ALT_SEPARATOR == RS
43
43
  err = @err || $stderr
44
44
  show_timings = false
45
+ # NOTE in Ruby 2.7, RubyGems sets SOURCE_DATE_EPOCH if it's not set
46
+ ::ENV.delete 'SOURCE_DATE_EPOCH' if (::ENV.key? 'IGNORE_SOURCE_DATE_EPOCH') && (::Gem.respond_to? :source_date_epoch)
45
47
 
46
48
  @options.map do |key, val|
47
49
  case key
@@ -76,17 +76,17 @@ module Asciidoctor
76
76
  opts.on('-n', '--section-numbers', 'auto-number section titles in the HTML backend; disabled by default') do
77
77
  self[:attributes]['sectnums'] = ''
78
78
  end
79
- opts.on('--eruby ERUBY', ['erb', 'erubis'],
80
- 'specify eRuby implementation to use when rendering custom ERB templates: [erb, erubis] (default: erb)') do |eruby|
79
+ opts.on('--eruby ERUBY', ['erb', 'erubi', 'erubis'],
80
+ 'specify eRuby implementation to use when rendering custom ERB templates: [erb, erubi, erubis] (default: erb)') do |eruby|
81
81
  self[:eruby] = eruby
82
82
  end
83
- opts.on('-a', '--attribute key[=value]', 'a document attribute to set in the form of key, key! or key=value pair',
84
- 'unless @ is appended to the value, this attributes takes precedence over attributes',
85
- 'defined in the source document') do |attr|
83
+ opts.on('-a', '--attribute name[=value]', 'a document attribute to set in the form of name, name!, or name=value pair',
84
+ 'this attribute takes precedence over the same attribute defined in the source document',
85
+ 'unless either the name or value ends in @ (i.e., name@=value or name=value@)') do |attr|
86
86
  next if (attr = attr.rstrip).empty? || attr == '='
87
87
  attr = attr.encode UTF_8 unless attr.encoding == UTF_8
88
- key, _, val = attr.partition '='
89
- self[:attributes][key] = val
88
+ name, _, val = attr.partition '='
89
+ self[:attributes][name] = val
90
90
  end
91
91
  opts.on('-T', '--template-dir DIR', 'a directory containing custom converter templates that override the built-in converter (requires tilt gem)',
92
92
  'may be specified multiple times') do |template_dir|
@@ -205,7 +205,7 @@ module Asciidoctor
205
205
  # shave off the file to process so that options errors appear correctly
206
206
  if args.size == 1 && args[0] == '-'
207
207
  infiles << args.pop
208
- elsif
208
+ else
209
209
  args.each do |file|
210
210
  if file.start_with? '-'
211
211
  # warn, but don't panic; we may have enough to proceed, so we won't force a failure
@@ -0,0 +1,198 @@
1
+ module Asciidoctor
2
+ class << self
3
+ # Public: Parse the AsciiDoc source input into an Asciidoctor::Document and
4
+ # convert it to the specified backend format.
5
+ #
6
+ # Accepts input as an IO (or StringIO), String or String Array object. If the
7
+ # input is a File, the object is expected to be opened for reading and is not
8
+ # closed afterwards by this method. Information about the file (filename,
9
+ # directory name, etc) gets assigned to attributes on the Document object.
10
+ #
11
+ # If the :to_file option is true, and the input is a File, the output is
12
+ # written to a file adjacent to the input file, having an extension that
13
+ # corresponds to the backend format. Otherwise, if the :to_file option is
14
+ # specified, the file is written to that file. If :to_file is not an absolute
15
+ # path, it is resolved relative to :to_dir, if given, otherwise the
16
+ # Document#base_dir. If the target directory does not exist, it will not be
17
+ # created unless the :mkdirs option is set to true. If the file cannot be
18
+ # written because the target directory does not exist, or because it falls
19
+ # outside of the Document#base_dir in safe mode, an IOError is raised.
20
+ #
21
+ # If the output is going to be written to a file, the header and footer are
22
+ # included unless specified otherwise (writing to a file implies creating a
23
+ # standalone document). Otherwise, the header and footer are not included by
24
+ # default and the converted result is returned.
25
+ #
26
+ # input - the String AsciiDoc source filename
27
+ # options - a String, Array or Hash of options to control processing (default: {})
28
+ # String and Array values are converted into a Hash.
29
+ # See Asciidoctor::Document#initialize for details about options.
30
+ #
31
+ # Returns the Document object if the converted String is written to a
32
+ # file, otherwise the converted String
33
+ def convert input, options = {}
34
+ (options = options.merge).delete :parse
35
+ to_dir = options.delete :to_dir
36
+ mkdirs = options.delete :mkdirs
37
+
38
+ case (to_file = options.delete :to_file)
39
+ when true, nil
40
+ unless (write_to_target = to_dir)
41
+ sibling_path = ::File.absolute_path input.path if ::File === input
42
+ end
43
+ to_file = nil
44
+ when false
45
+ to_file = nil
46
+ when '/dev/null'
47
+ return load input, options
48
+ else
49
+ options[:to_file] = write_to_target = to_file unless (stream_output = to_file.respond_to? :write)
50
+ end
51
+
52
+ unless options.key? :standalone
53
+ if sibling_path || write_to_target
54
+ options[:standalone] = options.fetch :header_footer, true
55
+ elsif options.key? :header_footer
56
+ options[:standalone] = options[:header_footer]
57
+ end
58
+ end
59
+
60
+ # NOTE outfile may be controlled by document attributes, so resolve outfile after loading
61
+ if sibling_path
62
+ options[:to_dir] = outdir = ::File.dirname sibling_path
63
+ elsif write_to_target
64
+ if to_dir
65
+ if to_file
66
+ options[:to_dir] = ::File.dirname ::File.expand_path to_file, to_dir
67
+ else
68
+ options[:to_dir] = ::File.expand_path to_dir
69
+ end
70
+ elsif to_file
71
+ options[:to_dir] = ::File.dirname ::File.expand_path to_file
72
+ end
73
+ end
74
+
75
+ # NOTE :to_dir is always set when outputting to a file
76
+ # NOTE :to_file option only passed if assigned an explicit path
77
+ doc = load input, options
78
+
79
+ if sibling_path # write to file in same directory
80
+ outfile = ::File.join outdir, %(#{doc.attributes['docname']}#{doc.outfilesuffix})
81
+ raise ::IOError, %(input file and output file cannot be the same: #{outfile}) if outfile == sibling_path
82
+ elsif write_to_target # write to explicit file or directory
83
+ working_dir = (options.key? :base_dir) ? (::File.expand_path options[:base_dir]) : ::Dir.pwd
84
+ # QUESTION should the jail be the working_dir or doc.base_dir???
85
+ jail = doc.safe >= SafeMode::SAFE ? working_dir : nil
86
+ if to_dir
87
+ outdir = doc.normalize_system_path(to_dir, working_dir, jail, target_name: 'to_dir', recover: false)
88
+ if to_file
89
+ outfile = doc.normalize_system_path(to_file, outdir, nil, target_name: 'to_dir', recover: false)
90
+ # reestablish outdir as the final target directory (in the case to_file had directory segments)
91
+ outdir = ::File.dirname outfile
92
+ else
93
+ outfile = ::File.join outdir, %(#{doc.attributes['docname']}#{doc.outfilesuffix})
94
+ end
95
+ elsif to_file
96
+ outfile = doc.normalize_system_path(to_file, working_dir, jail, target_name: 'to_dir', recover: false)
97
+ # establish outdir as the final target directory (in the case to_file had directory segments)
98
+ outdir = ::File.dirname outfile
99
+ end
100
+
101
+ if ::File === input && outfile == (::File.absolute_path input.path)
102
+ raise ::IOError, %(input file and output file cannot be the same: #{outfile})
103
+ end
104
+
105
+ if mkdirs
106
+ Helpers.mkdir_p outdir
107
+ else
108
+ # NOTE we intentionally refer to the directory as it was passed to the API
109
+ raise ::IOError, %(target directory does not exist: #{to_dir} (hint: set :mkdirs option)) unless ::File.directory? outdir
110
+ end
111
+ else # write to stream
112
+ outfile = to_file
113
+ outdir = nil
114
+ end
115
+
116
+ if outfile && !stream_output
117
+ output = doc.convert 'outfile' => outfile, 'outdir' => outdir
118
+ else
119
+ output = doc.convert
120
+ end
121
+
122
+ if outfile
123
+ doc.write output, outfile
124
+
125
+ # NOTE document cannot control this behavior if safe >= SafeMode::SERVER
126
+ # NOTE skip if stylesdir is a URI
127
+ if !stream_output && doc.safe < SafeMode::SECURE && (doc.attr? 'linkcss') && (doc.attr? 'copycss') &&
128
+ (doc.basebackend? 'html') && !((stylesdir = (doc.attr 'stylesdir')) && (Helpers.uriish? stylesdir))
129
+ if (stylesheet = doc.attr 'stylesheet')
130
+ if DEFAULT_STYLESHEET_KEYS.include? stylesheet
131
+ copy_asciidoctor_stylesheet = true
132
+ elsif !(Helpers.uriish? stylesheet)
133
+ copy_user_stylesheet = true
134
+ end
135
+ end
136
+ copy_syntax_hl_stylesheet = (syntax_hl = doc.syntax_highlighter) && (syntax_hl.write_stylesheet? doc)
137
+ if copy_asciidoctor_stylesheet || copy_user_stylesheet || copy_syntax_hl_stylesheet
138
+ stylesoutdir = doc.normalize_system_path(stylesdir, outdir, doc.safe >= SafeMode::SAFE ? outdir : nil)
139
+ if mkdirs
140
+ Helpers.mkdir_p stylesoutdir
141
+ else
142
+ raise ::IOError, %(target stylesheet directory does not exist: #{stylesoutdir} (hint: set :mkdirs option)) unless ::File.directory? stylesoutdir
143
+ end
144
+
145
+ if copy_asciidoctor_stylesheet
146
+ Stylesheets.instance.write_primary_stylesheet stylesoutdir
147
+ # FIXME should Stylesheets also handle the user stylesheet?
148
+ elsif copy_user_stylesheet
149
+ if (stylesheet_src = doc.attr 'copycss') == '' || stylesheet_src == true
150
+ stylesheet_src = doc.normalize_system_path stylesheet
151
+ else
152
+ # NOTE in this case, copycss is a source location (but cannot be a URI)
153
+ stylesheet_src = doc.normalize_system_path stylesheet_src.to_s
154
+ end
155
+ stylesheet_dest = doc.normalize_system_path stylesheet, stylesoutdir, (doc.safe >= SafeMode::SAFE ? outdir : nil)
156
+ # NOTE don't warn if src can't be read and dest already exists (see #2323)
157
+ if stylesheet_src != stylesheet_dest && (stylesheet_data = doc.read_asset stylesheet_src,
158
+ warn_on_failure: !(::File.file? stylesheet_dest), label: 'stylesheet')
159
+ if (stylesheet_outdir = ::File.dirname stylesheet_dest) != stylesoutdir && !(::File.directory? stylesheet_outdir)
160
+ if mkdirs
161
+ Helpers.mkdir_p stylesheet_outdir
162
+ else
163
+ raise ::IOError, %(target stylesheet directory does not exist: #{stylesheet_outdir} (hint: set :mkdirs option))
164
+ end
165
+ end
166
+ ::File.write stylesheet_dest, stylesheet_data, mode: FILE_WRITE_MODE
167
+ end
168
+ end
169
+ syntax_hl.write_stylesheet doc, stylesoutdir if copy_syntax_hl_stylesheet
170
+ end
171
+ end
172
+ doc
173
+ else
174
+ output
175
+ end
176
+ end
177
+
178
+ # Public: Parse the contents of the AsciiDoc source file into an
179
+ # Asciidoctor::Document and convert it to the specified backend format.
180
+ #
181
+ # input - the String AsciiDoc source filename
182
+ # options - a String, Array or Hash of options to control processing (default: {})
183
+ # String and Array values are converted into a Hash.
184
+ # See Asciidoctor::Document#initialize for details about options.
185
+ #
186
+ # Returns the Document object if the converted String is written to a
187
+ # file, otherwise the converted String
188
+ def convert_file filename, options = {}
189
+ ::File.open(filename, FILE_READ_MODE) {|file| convert file, options }
190
+ end
191
+
192
+ # Deprecated: Use {Asciidoctor.convert} instead.
193
+ alias render convert
194
+
195
+ # Deprecated: Use {Asciidoctor.convert_file} instead.
196
+ alias render_file convert_file
197
+ end
198
+ end