asciidoctor 1.5.5 → 1.5.6

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +216 -1
  3. data/CONTRIBUTING.adoc +2 -2
  4. data/Gemfile +20 -1
  5. data/LICENSE.adoc +1 -1
  6. data/README-fr.adoc +4 -3
  7. data/README-jp.adoc +11 -10
  8. data/README-zh_CN.adoc +4 -3
  9. data/README.adoc +17 -202
  10. data/Rakefile +41 -25
  11. data/asciidoctor.gemspec +9 -10
  12. data/data/locale/attributes.adoc +216 -34
  13. data/data/stylesheets/asciidoctor-default.css +23 -16
  14. data/features/step_definitions.rb +15 -19
  15. data/features/xref.feature +584 -20
  16. data/lib/asciidoctor.rb +292 -278
  17. data/lib/asciidoctor/abstract_block.rb +155 -94
  18. data/lib/asciidoctor/abstract_node.rb +108 -94
  19. data/lib/asciidoctor/attribute_list.rb +30 -22
  20. data/lib/asciidoctor/block.rb +7 -7
  21. data/lib/asciidoctor/cli/invoker.rb +47 -34
  22. data/lib/asciidoctor/cli/options.rb +22 -11
  23. data/lib/asciidoctor/converter.rb +3 -3
  24. data/lib/asciidoctor/converter/base.rb +2 -2
  25. data/lib/asciidoctor/converter/composite.rb +1 -1
  26. data/lib/asciidoctor/converter/docbook45.rb +2 -2
  27. data/lib/asciidoctor/converter/docbook5.rb +132 -87
  28. data/lib/asciidoctor/converter/factory.rb +0 -1
  29. data/lib/asciidoctor/converter/html5.rb +116 -98
  30. data/lib/asciidoctor/converter/manpage.rb +51 -52
  31. data/lib/asciidoctor/converter/template.rb +47 -36
  32. data/lib/asciidoctor/core_ext.rb +8 -2
  33. data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +4 -0
  34. data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +6 -0
  35. data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +5 -0
  36. data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +1 -1
  37. data/lib/asciidoctor/core_ext/1.8.7/string/{limit.rb → limit_bytesize.rb} +7 -6
  38. data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +6 -0
  39. data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +1 -1
  40. data/lib/asciidoctor/core_ext/nil_or_empty.rb +5 -5
  41. data/lib/asciidoctor/core_ext/regexp/is_match.rb +3 -0
  42. data/lib/asciidoctor/core_ext/string/{limit.rb → limit_bytesize.rb} +2 -2
  43. data/lib/asciidoctor/document.rb +216 -213
  44. data/lib/asciidoctor/extensions.rb +318 -185
  45. data/lib/asciidoctor/helpers.rb +35 -35
  46. data/lib/asciidoctor/inline.rb +32 -1
  47. data/lib/asciidoctor/list.rb +22 -6
  48. data/lib/asciidoctor/parser.rb +1008 -1038
  49. data/lib/asciidoctor/path_resolver.rb +46 -50
  50. data/lib/asciidoctor/reader.rb +275 -251
  51. data/lib/asciidoctor/section.rb +86 -58
  52. data/lib/asciidoctor/stylesheets.rb +6 -6
  53. data/lib/asciidoctor/substitutors.rb +567 -649
  54. data/lib/asciidoctor/table.rb +163 -108
  55. data/lib/asciidoctor/version.rb +1 -1
  56. data/man/asciidoctor.1 +18 -16
  57. data/man/asciidoctor.adoc +15 -13
  58. data/test/attributes_test.rb +138 -22
  59. data/test/blocks_test.rb +377 -97
  60. data/test/converter_test.rb +13 -0
  61. data/test/document_test.rb +244 -34
  62. data/test/extensions_test.rb +409 -42
  63. data/test/fixtures/asciidoc_index.txt +521 -0
  64. data/test/fixtures/basic-docinfo-footer.html +6 -0
  65. data/test/fixtures/basic-docinfo-footer.xml +8 -0
  66. data/test/fixtures/basic-docinfo.html +1 -0
  67. data/test/fixtures/basic-docinfo.xml +4 -0
  68. data/test/fixtures/basic.asciidoc +5 -0
  69. data/test/fixtures/chapter-a.adoc +3 -0
  70. data/test/fixtures/child-include.adoc +5 -0
  71. data/test/fixtures/circle.svg +9 -0
  72. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
  73. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
  74. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
  75. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
  76. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
  77. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
  78. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
  79. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
  80. data/test/fixtures/custom-docinfodir/basic-docinfo.html +1 -0
  81. data/test/fixtures/custom-docinfodir/docinfo.html +1 -0
  82. data/test/fixtures/docinfo-footer.html +1 -0
  83. data/test/fixtures/docinfo-footer.xml +9 -0
  84. data/test/fixtures/docinfo.html +1 -0
  85. data/test/fixtures/docinfo.xml +3 -0
  86. data/test/fixtures/dot.gif +0 -0
  87. data/test/fixtures/encoding.asciidoc +13 -0
  88. data/test/fixtures/grandchild-include.adoc +3 -0
  89. data/test/fixtures/hello-asciidoctor.pdf +69 -0
  90. data/test/fixtures/include-file.asciidoc +24 -0
  91. data/test/fixtures/include-file.ml +3 -0
  92. data/test/fixtures/include-file.xml +5 -0
  93. data/test/fixtures/master.adoc +5 -0
  94. data/test/fixtures/mismatched-end-tag.adoc +7 -0
  95. data/test/fixtures/parent-include-restricted.adoc +5 -0
  96. data/test/fixtures/parent-include.adoc +5 -0
  97. data/test/fixtures/sample.asciidoc +26 -0
  98. data/test/fixtures/stylesheets/custom.css +3 -0
  99. data/test/fixtures/subs-docinfo.html +2 -0
  100. data/test/fixtures/subs.adoc +7 -0
  101. data/test/fixtures/tagged-class-enclosed.rb +26 -0
  102. data/test/fixtures/tagged-class.rb +23 -0
  103. data/test/fixtures/tip.gif +0 -0
  104. data/test/invoker_test.rb +82 -4
  105. data/test/links_test.rb +312 -37
  106. data/test/lists_test.rb +204 -25
  107. data/test/manpage_test.rb +191 -4
  108. data/test/options_test.rb +18 -1
  109. data/test/paragraphs_test.rb +32 -7
  110. data/test/parser_test.rb +150 -30
  111. data/test/paths_test.rb +47 -13
  112. data/test/preamble_test.rb +1 -1
  113. data/test/reader_test.rb +366 -126
  114. data/test/sections_test.rb +203 -56
  115. data/test/substitutions_test.rb +339 -131
  116. data/test/tables_test.rb +315 -15
  117. data/test/test_helper.rb +400 -0
  118. data/test/text_test.rb +5 -5
  119. metadata +110 -22
@@ -0,0 +1,4 @@
1
+ # Educate Ruby 1.8.7 about the Hash#key method.
2
+ class Hash
3
+ alias key index
4
+ end
@@ -0,0 +1,6 @@
1
+ def IO.binread name, length = nil, offset = 0
2
+ File.open name, 'rb' do |f|
3
+ f.seek offset unless offset == 0
4
+ length ? (f.read length) : f.read
5
+ end
6
+ end unless IO.respond_to? :binread
@@ -0,0 +1,5 @@
1
+ def IO.write name, string, offset = 0, opts = nil
2
+ File.open name, 'w' do |f|
3
+ f.write string
4
+ end
5
+ end unless IO.respond_to? :write
@@ -1,6 +1,6 @@
1
1
  # Educate Ruby 1.8.7 about the String#chr method.
2
2
  class String
3
3
  def chr
4
- self[0..0]
4
+ slice 0, 1
5
5
  end unless method_defined? :chr
6
6
  end
@@ -2,27 +2,28 @@ if RUBY_ENGINE_JRUBY
2
2
  class String
3
3
  # Safely truncate the string to the specified number of bytes.
4
4
  # If a multibyte char gets split, the dangling fragment is removed.
5
- def limit size
5
+ def limit_bytesize size
6
6
  return self unless size < bytesize
7
7
  result = (unpack %(a#{size}))[0]
8
8
  begin
9
9
  result.unpack 'U*'
10
- rescue ArgumentError
10
+ rescue ::ArgumentError
11
11
  result.chop!
12
12
  retry
13
13
  end
14
14
  result
15
- end unless method_defined? :limit
15
+ end unless method_defined? :limit_bytesize
16
16
  end
17
17
  else
18
18
  class String
19
+ ValidTrailingCharRx = /.$/u
19
20
  # Safely truncate the string to the specified number of bytes.
20
21
  # If a multibyte char gets split, the dangling fragment is removed.
21
- def limit size
22
+ def limit_bytesize size
22
23
  return self unless size < bytesize
23
24
  result = (unpack %(a#{size}))[0]
24
- result.chop! until result.empty? || /.$/u =~ result
25
+ result.chop! until result.empty? || (ValidTrailingCharRx.match? result)
25
26
  result
26
- end unless method_defined? :limit
27
+ end unless method_defined? :limit_bytesize
27
28
  end
28
29
  end
@@ -0,0 +1,6 @@
1
+ # Educate Ruby 1.8.7 about the Symbol#empty? method.
2
+ class Symbol
3
+ def empty?
4
+ to_s.empty?
5
+ end unless method_defined? :empty?
6
+ end
@@ -1,4 +1,4 @@
1
- # Educate Ruby 1.8.7 about the Symbol#length method.
1
+ # Educate Ruby 1.8.7 about the Symbol#empty? and Symbol#length methods.
2
2
  class Symbol
3
3
  def length
4
4
  to_s.length
@@ -3,21 +3,21 @@
3
3
  # String, Array, Hash, and Numeric.
4
4
 
5
5
  class NilClass
6
- alias :nil_or_empty? :nil? unless method_defined? :nil_or_empty?
6
+ alias nil_or_empty? nil? unless method_defined? :nil_or_empty?
7
7
  end
8
8
 
9
9
  class String
10
- alias :nil_or_empty? :empty? unless method_defined? :nil_or_empty?
10
+ alias nil_or_empty? empty? unless method_defined? :nil_or_empty?
11
11
  end
12
12
 
13
13
  class Array
14
- alias :nil_or_empty? :empty? unless method_defined? :nil_or_empty?
14
+ alias nil_or_empty? empty? unless method_defined? :nil_or_empty?
15
15
  end
16
16
 
17
17
  class Hash
18
- alias :nil_or_empty? :empty? unless method_defined? :nil_or_empty?
18
+ alias nil_or_empty? empty? unless method_defined? :nil_or_empty?
19
19
  end
20
20
 
21
21
  class Numeric
22
- alias :nil_or_empty? :nil? unless method_defined? :nil_or_empty?
22
+ alias nil_or_empty? nil? unless method_defined? :nil_or_empty?
23
23
  end
@@ -0,0 +1,3 @@
1
+ class Regexp
2
+ alias match? === unless method_defined? :match?
3
+ end
@@ -1,10 +1,10 @@
1
1
  class String
2
2
  # Safely truncate the string to the specified number of bytes.
3
3
  # If a multibyte char gets split, the dangling fragment is removed.
4
- def limit size
4
+ def limit_bytesize size
5
5
  return self unless size < bytesize
6
6
  # NOTE JRuby 1.7 & Rubinius fail to detect invalid encoding unless encoding is forced; impact is marginal.
7
7
  size -= 1 until ((result = byteslice 0, size).force_encoding ::Encoding::UTF_8).valid_encoding?
8
8
  result
9
- end unless method_defined? :limit
9
+ end unless method_defined? :limit_bytesize
10
10
  end
@@ -32,20 +32,21 @@ class Document < AbstractBlock
32
32
 
33
33
  def save_to block_attributes
34
34
  (block_attributes[:attribute_entries] ||= []) << self
35
+ self
35
36
  end
36
37
  end
37
38
 
38
39
  # Public Parsed and stores a partitioned title (i.e., title & subtitle).
39
40
  class Title
40
41
  attr_reader :main
41
- alias :title :main
42
+ alias title main
42
43
  attr_reader :subtitle
43
44
  attr_reader :combined
44
45
 
45
46
  def initialize val, opts = {}
46
47
  # TODO separate sanitization by type (:cdata for HTML/XML, :plain_text for non-SGML, false for none)
47
48
  if (@sanitized = opts[:sanitize]) && val.include?('<')
48
- val = val.gsub(XmlSanitizeRx, '').tr_s(' ', ' ').strip
49
+ val = val.gsub(XmlSanitizeRx, '').squeeze(' ').strip
49
50
  end
50
51
  if (sep = opts[:separator] || ':').empty? || !val.include?(sep = %(#{sep} ))
51
52
  @main = val
@@ -61,7 +62,7 @@ class Document < AbstractBlock
61
62
  end
62
63
 
63
64
  def subtitle?
64
- !!@subtitle
65
+ @subtitle ? true : false
65
66
  end
66
67
 
67
68
  def to_s
@@ -78,7 +79,7 @@ class Document < AbstractBlock
78
79
  #
79
80
  # A value of 1 (SAFE) closely parallels safe mode in AsciiDoc. In particular,
80
81
  # it prevents access to files which reside outside of the parent directory
81
- # of the source file and disables any macro other than the include macro.
82
+ # of the source file and disables any macro other than the include directive.
82
83
  #
83
84
  # A value of 10 (SERVER) disallows the document from setting attributes that
84
85
  # would affect the conversion of the document, in addition to all the security
@@ -90,7 +91,7 @@ class Document < AbstractBlock
90
91
  # A value of 20 (SECURE) disallows the document from attempting to read files
91
92
  # from the file system and including the contents of them into the document,
92
93
  # in addition to all the security features of SafeMode::SECURE. In
93
- # particular, it disallows use of the include::[] macro and the embedding of
94
+ # particular, it disallows use of the include::[] directive and the embedding of
94
95
  # binary content (data uri), stylesheets and JavaScripts referenced by the
95
96
  # document. (Asciidoctor and trusted extensions may still be allowed to embed
96
97
  # trusted content into the document).
@@ -115,11 +116,20 @@ class Document < AbstractBlock
115
116
  #
116
117
  attr_reader :compat_mode
117
118
 
118
- # Public: Get the Boolean flag that indicates whether source map information is tracked by the parser
119
- attr_reader :sourcemap
119
+ # Public: Get the cached value of the backend attribute for this document
120
+ attr_reader :backend
121
+
122
+ # Public: Get the cached value of the doctype attribute for this document
123
+ attr_reader :doctype
124
+
125
+ # Public: Get or set the Boolean flag that indicates whether source map information should be tracked by the parser
126
+ attr_accessor :sourcemap
120
127
 
121
- # Public: Get the Hash of document references
122
- attr_reader :references
128
+ # Public: Get the document catalog Hash
129
+ attr_reader :catalog
130
+
131
+ # Public: Alias catalog property as references for backwards compatiblity
132
+ alias references catalog
123
133
 
124
134
  # Public: Get the Hash of document counters
125
135
  attr_reader :counters
@@ -151,7 +161,7 @@ class Document < AbstractBlock
151
161
  # Public: Get the Converter associated with this document
152
162
  attr_reader :converter
153
163
 
154
- # Public: Get the extensions registry
164
+ # Public: Get the activated Extensions::Registry associated with this document.
155
165
  attr_reader :extensions
156
166
 
157
167
  # Public: Initialize a {Document} object.
@@ -173,32 +183,30 @@ class Document < AbstractBlock
173
183
  if (parent_doc = options.delete :parent)
174
184
  @parent_document = parent_doc
175
185
  options[:base_dir] ||= parent_doc.base_dir
176
- @references = parent_doc.references.inject({}) do |accum, (key,ref)|
177
- if key == :footnotes
178
- accum[:footnotes] = []
179
- else
180
- accum[key] = ref
181
- end
186
+ @catalog = parent_doc.catalog.inject({}) do |accum, (key, table)|
187
+ accum[key] = (key == :footnotes ? [] : table)
182
188
  accum
183
189
  end
184
190
  @callouts = parent_doc.callouts
185
191
  # QUESTION should we support setting attribute in parent document from nested document?
186
192
  # NOTE we must dup or else all the assignments to the overrides clobbers the real attributes
187
- attr_overrides = parent_doc.attributes.dup
188
- ['doctype', 'compat-mode', 'toc', 'toc-placement', 'toc-position'].each do |key|
189
- attr_overrides.delete key
190
- end
191
- @attribute_overrides = attr_overrides
193
+ @attribute_overrides = attr_overrides = parent_doc.attributes.dup
194
+ parent_doctype = attr_overrides.delete 'doctype'
195
+ attr_overrides.delete 'compat-mode'
196
+ attr_overrides.delete 'toc'
197
+ attr_overrides.delete 'toc-placement'
198
+ attr_overrides.delete 'toc-position'
192
199
  @safe = parent_doc.safe
193
- @compat_mode = parent_doc.compat_mode
200
+ @attributes['compat-mode'] = '' if (@compat_mode = parent_doc.compat_mode)
194
201
  @sourcemap = parent_doc.sourcemap
195
202
  @converter = parent_doc.converter
196
203
  initialize_extensions = false
197
204
  @extensions = parent_doc.extensions
198
205
  else
199
206
  @parent_document = nil
200
- @references = {
207
+ @catalog = {
201
208
  :ids => {},
209
+ :refs => {},
202
210
  :footnotes => [],
203
211
  :links => [],
204
212
  :images => [],
@@ -231,7 +239,7 @@ class Document < AbstractBlock
231
239
  else
232
240
  # NOTE: not using infix rescue for performance reasons, see https://github.com/jruby/jruby/issues/1816
233
241
  begin
234
- @safe = SafeMode.const_get(safe_mode.to_s.upcase)
242
+ @safe = SafeMode.value_for_name safe_mode.to_s
235
243
  rescue
236
244
  @safe = SafeMode::SECURE
237
245
  end
@@ -247,19 +255,25 @@ class Document < AbstractBlock
247
255
  @header = nil
248
256
  @counters = {}
249
257
  @attributes_modified = ::Set.new
250
- @options = options
251
258
  @docinfo_processor_extensions = {}
252
259
  header_footer = (options[:header_footer] ||= false)
253
- options.freeze
260
+ (@options = options).freeze
254
261
 
255
262
  attrs = @attributes
256
263
  #attrs['encoding'] = 'UTF-8'
257
264
  attrs['sectids'] = ''
258
- attrs['notitle'] = '' unless header_footer
259
265
  attrs['toc-placement'] = 'auto'
266
+ if header_footer
267
+ attrs['copycss'] = ''
268
+ # sync embedded attribute with :header_footer option value
269
+ attr_overrides['embedded'] = nil
270
+ else
271
+ attrs['notitle'] = ''
272
+ # sync embedded attribute with :header_footer option value
273
+ attr_overrides['embedded'] = ''
274
+ end
260
275
  attrs['stylesheet'] = ''
261
276
  attrs['webfonts'] = ''
262
- attrs['copycss'] = '' if header_footer
263
277
  attrs['prewrap'] = ''
264
278
  attrs['attribute-undefined'] = Compliance.attribute_undefined
265
279
  attrs['attribute-missing'] = Compliance.attribute_missing
@@ -272,7 +286,6 @@ class Document < AbstractBlock
272
286
  attrs['note-caption'] = 'Note'
273
287
  attrs['tip-caption'] = 'Tip'
274
288
  attrs['warning-caption'] = 'Warning'
275
- attrs['appendix-caption'] = 'Appendix'
276
289
  attrs['example-caption'] = 'Example'
277
290
  attrs['figure-caption'] = 'Figure'
278
291
  #attrs['listing-caption'] = 'Listing'
@@ -280,6 +293,10 @@ class Document < AbstractBlock
280
293
  attrs['toc-title'] = 'Table of Contents'
281
294
  #attrs['preface-title'] = 'Preface'
282
295
  attrs['manname-title'] = 'NAME'
296
+ attrs['section-refsig'] = 'Section'
297
+ #attrs['part-refsig'] = 'Part'
298
+ attrs['chapter-refsig'] = 'Chapter'
299
+ attrs['appendix-caption'] = attrs['appendix-refsig'] = 'Appendix'
283
300
  attrs['untitled-label'] = 'Untitled'
284
301
  attrs['version-label'] = 'Version'
285
302
  attrs['last-update-label'] = 'Last updated'
@@ -287,14 +304,10 @@ class Document < AbstractBlock
287
304
  attr_overrides['asciidoctor'] = ''
288
305
  attr_overrides['asciidoctor-version'] = VERSION
289
306
 
290
- safe_mode_name = SafeMode.constants.find {|l| SafeMode.const_get(l) == @safe }.to_s.downcase
291
- attr_overrides['safe-mode-name'] = safe_mode_name
307
+ attr_overrides['safe-mode-name'] = (safe_mode_name = SafeMode.name_for_value @safe)
292
308
  attr_overrides["safe-mode-#{safe_mode_name}"] = ''
293
309
  attr_overrides['safe-mode-level'] = @safe
294
310
 
295
- # sync the embedded attribute w/ the value of options...do not allow override
296
- attr_overrides['embedded'] = header_footer ? nil : ''
297
-
298
311
  # the only way to set the max-include-depth attribute is via the API; default to 64 like AsciiDoc Python
299
312
  attr_overrides['max-include-depth'] ||= 64
300
313
 
@@ -353,7 +366,7 @@ class Document < AbstractBlock
353
366
  end
354
367
 
355
368
  # the only way to set the max-attribute-value-size attribute is via the API; disabled by default
356
- @max_attribute_value_size = (val = (attr_overrides['max-attribute-value-size'] ||= nil)) ? val.to_i.abs : nil
369
+ @max_attribute_value_size = (size = (attr_overrides['max-attribute-value-size'] ||= nil)) ? size.to_i.abs : nil
357
370
 
358
371
  attr_overrides.delete_if do |key, val|
359
372
  verdict = false
@@ -373,8 +386,11 @@ class Document < AbstractBlock
373
386
  end
374
387
 
375
388
  if parent_doc
376
- # setup default doctype (backend is fixed)
377
- attrs['doctype'] ||= DEFAULT_DOCTYPE
389
+ @backend = attrs['backend']
390
+ # reset doctype unless it matches the default value
391
+ unless (@doctype = attrs['doctype'] = parent_doctype) == DEFAULT_DOCTYPE
392
+ update_doctype_attributes DEFAULT_DOCTYPE
393
+ end
378
394
 
379
395
  # don't need to do the extra processing within our own document
380
396
  # FIXME line info isn't reported correctly within include files in nested document
@@ -389,10 +405,11 @@ class Document < AbstractBlock
389
405
  @parsed = true
390
406
  else
391
407
  # setup default backend and doctype
408
+ @backend = nil
392
409
  if (attrs['backend'] ||= DEFAULT_BACKEND) == 'manpage'
393
- attrs['doctype'] = attr_overrides['doctype'] = 'manpage'
410
+ @doctype = attrs['doctype'] = attr_overrides['doctype'] = 'manpage'
394
411
  else
395
- attrs['doctype'] ||= DEFAULT_DOCTYPE
412
+ @doctype = (attrs['doctype'] ||= DEFAULT_DOCTYPE)
396
413
  end
397
414
  update_backend_attributes attrs['backend'], true
398
415
 
@@ -402,20 +419,25 @@ class Document < AbstractBlock
402
419
  # dynamic intrinstic attribute values
403
420
 
404
421
  # See https://reproducible-builds.org/specs/source-date-epoch/
405
- now = ::ENV['SOURCE_DATE_EPOCH'] ? (::Time.at ::ENV['SOURCE_DATE_EPOCH'].to_i).utc : ::Time.now
406
- localdate = (attrs['localdate'] ||= now.strftime('%Y-%m-%d'))
407
- unless (localtime = attrs['localtime'])
408
- begin
409
- localtime = attrs['localtime'] = now.strftime('%H:%M:%S %Z')
410
- rescue # Asciidoctor.js fails if timezone string has characters outside basic Latin (see asciidoctor.js#23)
411
- localtime = attrs['localtime'] = now.strftime('%H:%M:%S %z')
412
- end
422
+ # NOTE Opal can't call key? on ENV
423
+ now = ::ENV['SOURCE_DATE_EPOCH'] ? ::Time.at(Integer ::ENV['SOURCE_DATE_EPOCH']).utc : ::Time.now
424
+ if (localdate = attrs['localdate'])
425
+ localyear = (attrs['localyear'] ||= ((localdate.index '-') == 4 ? (localdate.slice 0, 4) : nil))
426
+ else
427
+ localdate = attrs['localdate'] = (now.strftime '%Y-%m-%d')
428
+ localyear = (attrs['localyear'] ||= now.year.to_s)
413
429
  end
430
+ localtime = (attrs['localtime'] ||= begin
431
+ now.strftime '%H:%M:%S %Z'
432
+ rescue # Asciidoctor.js fails if timezone string has characters outside basic Latin (see asciidoctor.js#23)
433
+ now.strftime '%H:%M:%S %z'
434
+ end)
414
435
  attrs['localdatetime'] ||= %(#{localdate} #{localtime})
415
436
 
416
437
  # docdate, doctime and docdatetime should default to
417
438
  # localdate, localtime and localdatetime if not otherwise set
418
439
  attrs['docdate'] ||= localdate
440
+ attrs['docyear'] ||= localyear
419
441
  attrs['doctime'] ||= localtime
420
442
  attrs['docdatetime'] ||= %(#{localdate} #{localtime})
421
443
 
@@ -424,22 +446,21 @@ class Document < AbstractBlock
424
446
  attrs['iconsdir'] ||= ::File.join(attrs.fetch('imagesdir', './images'), 'icons')
425
447
 
426
448
  if initialize_extensions
427
- if (registry = options[:extensions_registry])
428
- if Extensions::Registry === registry || (::RUBY_ENGINE_JRUBY &&
429
- ::AsciidoctorJ::Extensions::ExtensionRegistry === registry)
430
- # take it as it is
431
- else
432
- registry = Extensions::Registry.new
449
+ if (ext_registry = options[:extension_registry])
450
+ # QUESTION should we warn the value type of the option is not a registry or boolean?
451
+ unless Extensions::Registry === ext_registry || (::RUBY_ENGINE_JRUBY &&
452
+ ::AsciidoctorJ::Extensions::ExtensionRegistry === ext_registry)
453
+ ext_registry = Extensions::Registry.new
433
454
  end
434
455
  elsif ::Proc === (ext_block = options[:extensions])
435
- registry = Extensions.build_registry(&ext_block)
456
+ ext_registry = Extensions.create(&ext_block)
436
457
  else
437
- registry = Extensions::Registry.new
458
+ ext_registry = Extensions::Registry.new
438
459
  end
439
- @extensions = registry.activate self
460
+ @extensions = ext_registry.activate self
440
461
  end
441
462
 
442
- @reader = PreprocessorReader.new self, data, Reader::Cursor.new(attrs['docfile'], @base_dir)
463
+ @reader = PreprocessorReader.new self, data, (Reader::Cursor.new attrs['docfile'], @base_dir), :normalize => true
443
464
  end
444
465
  end
445
466
 
@@ -460,7 +481,9 @@ class Document < AbstractBlock
460
481
  else
461
482
  doc = self
462
483
  # create reader if data is provided (used when data is not known at the time the Document object is created)
463
- @reader = PreprocessorReader.new doc, data, Reader::Cursor.new(@attributes['docfile'], @base_dir) if data
484
+ if data
485
+ @reader = PreprocessorReader.new doc, data, (Reader::Cursor.new @attributes['docfile'], @base_dir), :normalize => true
486
+ end
464
487
 
465
488
  if (exts = @parent_document ? nil : @extensions) && exts.preprocessors?
466
489
  exts.preprocessors.each do |ext|
@@ -469,13 +492,13 @@ class Document < AbstractBlock
469
492
  end
470
493
 
471
494
  # Now parse the lines in the reader into blocks
472
- Parser.parse @reader, doc, :header_only => !!@options[:parse_header_only]
495
+ Parser.parse @reader, doc, :header_only => @options[:parse_header_only]
473
496
 
474
497
  # should we call sort of post-parse function?
475
498
  restore_attributes
476
499
 
477
- if exts && exts.treeprocessors?
478
- exts.treeprocessors.each do |ext|
500
+ if exts && exts.tree_processors?
501
+ exts.tree_processors.each do |ext|
479
502
  if (result = ext.process_method[doc]) && Document === result && result != doc
480
503
  doc = result
481
504
  end
@@ -493,19 +516,15 @@ class Document < AbstractBlock
493
516
  # seed - the initial value as a String or Integer
494
517
  #
495
518
  # returns the next number in the sequence for the specified counter
496
- def counter(name, seed = nil)
497
- if (attr_is_seed = !(attr_val = @attributes[name]).nil_or_empty?) && @counters.key?(name)
498
- @counters[name] = nextval(attr_val)
519
+ def counter name, seed = nil
520
+ return @parent_document.counter name, seed if @parent_document
521
+ if (attr_seed = !(attr_val = @attributes[name]).nil_or_empty?) && (@counters.key? name)
522
+ @attributes[name] = @counters[name] = (nextval attr_val)
523
+ elsif seed
524
+ @attributes[name] = @counters[name] = (seed == seed.to_i.to_s ? seed.to_i : seed)
499
525
  else
500
- if seed.nil?
501
- seed = nextval(attr_is_seed ? attr_val : 0)
502
- elsif seed.to_i.to_s == seed
503
- seed = seed.to_i
504
- end
505
- @counters[name] = seed
526
+ @attributes[name] = @counters[name] = nextval(attr_seed ? attr_val : 0)
506
527
  end
507
-
508
- (@attributes[name] = @counters[name])
509
528
  end
510
529
 
511
530
  # Public: Increment the specified counter and store it in the block's attributes
@@ -514,11 +533,11 @@ class Document < AbstractBlock
514
533
  # block - the Block on which to save the counter
515
534
  #
516
535
  # returns the next number in the sequence for the specified counter
517
- def counter_increment(counter_name, block)
518
- val = counter(counter_name)
519
- AttributeEntry.new(counter_name, val).save_to(block.attributes)
520
- val
536
+ def increment_and_store_counter counter_name, block
537
+ ((AttributeEntry.new counter_name, (counter counter_name)).save_to block.attributes).value
521
538
  end
539
+ # Deprecated: Map old counter_increment method to increment_counter for backwards compatibility
540
+ alias counter_increment increment_and_store_counter
522
541
 
523
542
  # Internal: Get the next value in the sequence.
524
543
  #
@@ -540,44 +559,42 @@ class Document < AbstractBlock
540
559
  end
541
560
  end
542
561
 
543
- def register(type, value, force = false)
562
+ def register type, value
544
563
  case type
545
- when :ids
546
- id, reftext = [*value]
547
- reftext ||= '[' + id + ']'
548
- if force
549
- @references[:ids][id] = reftext
550
- else
551
- @references[:ids][id] ||= reftext
564
+ when :ids # deprecated
565
+ id, reftext = value
566
+ @catalog[:ids][id] ||= reftext || ('[' + id + ']')
567
+ when :refs
568
+ id, ref, reftext = value
569
+ unless (refs = @catalog[:refs]).key? id
570
+ @catalog[:ids][id] = reftext || ('[' + id + ']')
571
+ refs[id] = ref
552
572
  end
553
573
  when :footnotes, :indexterms
554
- @references[type] << value
574
+ @catalog[type] << value
555
575
  else
556
- if @options[:catalog_assets]
557
- @references[type] << value
558
- end
576
+ @catalog[type] << value if @options[:catalog_assets]
559
577
  end
560
578
  end
561
579
 
562
580
  def footnotes?
563
- !@references[:footnotes].empty?
581
+ @catalog[:footnotes].empty? ? false : true
564
582
  end
565
583
 
566
584
  def footnotes
567
- @references[:footnotes]
585
+ @catalog[:footnotes]
568
586
  end
569
587
 
570
588
  def nested?
571
- !!@parent_document
589
+ @parent_document ? true : false
572
590
  end
573
591
 
574
592
  def embedded?
575
- # QUESTION should this be !@options[:header_footer] ?
576
593
  @attributes.key? 'embedded'
577
594
  end
578
595
 
579
596
  def extensions?
580
- !!@extensions
597
+ @extensions ? true : false
581
598
  end
582
599
 
583
600
  # Make the raw source for the Document available.
@@ -590,14 +607,6 @@ class Document < AbstractBlock
590
607
  @reader.source_lines if @reader
591
608
  end
592
609
 
593
- def doctype
594
- @doctype ||= @attributes['doctype']
595
- end
596
-
597
- def backend
598
- @backend ||= @attributes['backend']
599
- end
600
-
601
610
  def basebackend? base
602
611
  @attributes['basebackend'] == base
603
612
  end
@@ -607,9 +616,11 @@ class Document < AbstractBlock
607
616
  @attributes['title']
608
617
  end
609
618
 
610
- def title=(title)
611
- @header ||= Section.new(self, 0)
612
- @header.title = title
619
+ def title= title
620
+ unless (sect = @header)
621
+ (sect = (@header = Section.new self, 0, false)).sectname = 'header'
622
+ end
623
+ sect.title = title
613
624
  end
614
625
 
615
626
  # Public: Resolves the primary title for the document
@@ -634,7 +645,7 @@ class Document < AbstractBlock
634
645
  def doctitle opts = {}
635
646
  if !(val = @attributes['title'].nil_or_empty?)
636
647
  val = title
637
- elsif (sect = first_section) && sect.title?
648
+ elsif (sect = first_section)
638
649
  val = sect.title
639
650
  elsif opts[:use_fallback] && (val = @attributes['untitled-label'])
640
651
  # use val set in condition
@@ -645,12 +656,12 @@ class Document < AbstractBlock
645
656
  if (separator = opts[:partition])
646
657
  Title.new val, opts.merge({ :separator => (separator == true ? @attributes['title-separator'] : separator) })
647
658
  elsif opts[:sanitize] && val.include?('<')
648
- val.gsub(XmlSanitizeRx, '').tr_s(' ', ' ').strip
659
+ val.gsub(XmlSanitizeRx, '').squeeze(' ').strip
649
660
  else
650
661
  val
651
662
  end
652
663
  end
653
- alias :name :doctitle
664
+ alias name doctitle
654
665
 
655
666
  # Public: Convenience method to retrieve the document attribute 'author'
656
667
  #
@@ -678,15 +689,14 @@ class Document < AbstractBlock
678
689
  @attributes.key? 'nofooter'
679
690
  end
680
691
 
681
- # QUESTION move to AbstractBlock?
682
692
  def first_section
683
- has_header? ? @header : (@blocks || []).find {|e| e.context == :section }
693
+ @header || @blocks.find {|e| e.context == :section }
684
694
  end
685
695
 
686
696
  def has_header?
687
697
  @header ? true : false
688
698
  end
689
- alias :header? :has_header?
699
+ alias header? has_header?
690
700
 
691
701
  # Public: Append a content Block to this Document.
692
702
  #
@@ -696,14 +706,14 @@ class Document < AbstractBlock
696
706
  #
697
707
  # Returns The parent Block
698
708
  def << block
699
- assign_index block if block.context == :section
709
+ enumerate_section block if block.context == :section
700
710
  super
701
711
  end
702
712
 
703
713
  # Internal: called after the header has been parsed and before the content
704
714
  # will be parsed.
705
715
  #--
706
- # QUESTION should we invoke the Treeprocessors here, passing in a phase?
716
+ # QUESTION should we invoke the TreeProcessors here, passing in a phase?
707
717
  # QUESTION is finalize_header the right name?
708
718
  def finalize_header unrooted_attributes, header_valid = true
709
719
  clear_playback_attributes unrooted_attributes
@@ -769,11 +779,8 @@ class Document < AbstractBlock
769
779
  attrs['toc-class'] ||= default_toc_class if default_toc_class
770
780
  end
771
781
 
772
- if attrs.key? 'compat-mode'
773
- attrs['source-language'] = attrs['language'] if attrs.has_key? 'language'
774
- @compat_mode = true
775
- else
776
- @compat_mode = false
782
+ if (@compat_mode = attrs.key? 'compat-mode')
783
+ attrs['source-language'] = attrs['language'] if attrs.key? 'language'
777
784
  end
778
785
 
779
786
  # NOTE pin the outfilesuffix after the header is parsed
@@ -825,32 +832,32 @@ class Document < AbstractBlock
825
832
  #
826
833
  # If the attribute is locked, false is returned. Otherwise, the value is
827
834
  # assigned to the attribute name after first performing attribute
828
- # substitutions on the value. If the attribute name is 'backend', then the
829
- # value of backend-related attributes are updated.
835
+ # substitutions on the value. If the attribute name is 'backend' or
836
+ # 'doctype', then the value of backend-related attributes are updated.
830
837
  #
831
838
  # name - the String attribute name
832
- # value - the String attribute value
839
+ # value - the String attribute value; must not be nil (default: '')
833
840
  #
834
- # returns true if the attribute was set, false if it was not set because it's locked
835
- def set_attribute(name, value)
836
- if attribute_locked?(name)
841
+ # Returns the resolved value if the attribute was set or false if it was not because it's locked.
842
+ def set_attribute name, value = ''
843
+ if attribute_locked? name
837
844
  false
838
845
  else
839
846
  if @max_attribute_value_size
840
- resolved_value = (apply_attribute_value_subs value).limit @max_attribute_value_size
847
+ resolved_value = (apply_attribute_value_subs value).limit_bytesize @max_attribute_value_size
841
848
  else
842
849
  resolved_value = apply_attribute_value_subs value
843
850
  end
844
851
  case name
845
852
  when 'backend'
846
- update_backend_attributes resolved_value, !!@attributes_modified.delete?('htmlsyntax')
853
+ update_backend_attributes resolved_value, (@attributes_modified.delete? 'htmlsyntax')
847
854
  when 'doctype'
848
855
  update_doctype_attributes resolved_value
849
856
  else
850
857
  @attributes[name] = resolved_value
851
858
  end
852
859
  @attributes_modified << name
853
- true
860
+ resolved_value
854
861
  end
855
862
  end
856
863
 
@@ -890,61 +897,60 @@ class Document < AbstractBlock
890
897
  # value - The String attribute value on which to perform substitutions
891
898
  #
892
899
  # Returns The String value with substitutions performed
893
- def apply_attribute_value_subs(value)
894
- if (m = AttributeEntryPassMacroRx.match(value))
895
- if !m[1].empty?
896
- subs = resolve_pass_subs m[1]
897
- subs.empty? ? m[2] : (apply_subs m[2], subs)
898
- else
899
- m[2]
900
- end
900
+ def apply_attribute_value_subs value
901
+ if AttributeEntryPassMacroRx =~ value
902
+ $1 ? (apply_subs $2, (resolve_pass_subs $1)) : $2
901
903
  else
902
904
  apply_header_subs value
903
905
  end
904
906
  end
905
907
 
906
- # Public: Update the backend attributes to reflect a change in the selected backend
908
+ # Public: Update the backend attributes to reflect a change in the active backend.
907
909
  #
908
910
  # This method also handles updating the related doctype attributes if the
909
911
  # doctype attribute is assigned at the time this method is called.
910
- def update_backend_attributes new_backend, force = false
911
- if force || (new_backend && new_backend != @attributes['backend'])
912
- attrs = @attributes
913
- current_backend = attrs['backend']
914
- current_basebackend = attrs['basebackend']
915
- current_doctype = attrs['doctype']
912
+ #
913
+ # Returns the resolved String backend if updated, nothing otherwise.
914
+ def update_backend_attributes new_backend, force = nil
915
+ if force || (new_backend && new_backend != @backend)
916
+ current_backend, current_basebackend, current_doctype = @backend, (attrs = @attributes)['basebackend'], @doctype
916
917
  if new_backend.start_with? 'xhtml'
917
918
  attrs['htmlsyntax'] = 'xml'
918
919
  new_backend = new_backend[1..-1]
919
920
  elsif new_backend.start_with? 'html'
920
921
  attrs['htmlsyntax'] = 'html' unless attrs['htmlsyntax'] == 'xml'
921
922
  end
922
- if (resolved_name = BACKEND_ALIASES[new_backend])
923
- new_backend = resolved_name
923
+ if (resolved_backend = BACKEND_ALIASES[new_backend])
924
+ new_backend = resolved_backend
924
925
  end
925
- if current_backend
926
- attrs.delete %(backend-#{current_backend})
927
- if current_doctype
926
+ if current_doctype
927
+ if current_backend
928
+ attrs.delete %(backend-#{current_backend})
928
929
  attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype})
929
930
  end
930
- end
931
- if current_doctype
932
- attrs[%(doctype-#{current_doctype})] = ''
933
931
  attrs[%(backend-#{new_backend}-doctype-#{current_doctype})] = ''
932
+ attrs[%(doctype-#{current_doctype})] = ''
933
+ elsif current_backend
934
+ attrs.delete %(backend-#{current_backend})
934
935
  end
935
- attrs['backend'] = new_backend
936
936
  attrs[%(backend-#{new_backend})] = ''
937
+ @backend = attrs['backend'] = new_backend
937
938
  # (re)initialize converter
938
939
  if Converter::BackendInfo === (@converter = create_converter)
939
940
  new_basebackend = @converter.basebackend
940
941
  attrs['outfilesuffix'] = @converter.outfilesuffix unless attribute_locked? 'outfilesuffix'
941
942
  new_filetype = @converter.filetype
942
- else
943
+ elsif @converter
943
944
  new_basebackend = new_backend.sub TrailingDigitsRx, ''
944
- # QUESTION should we be forcing the basebackend to html if unknown?
945
- new_outfilesuffix = DEFAULT_EXTENSIONS[new_basebackend] || '.html'
946
- new_filetype = new_outfilesuffix[1..-1]
945
+ if (new_outfilesuffix = DEFAULT_EXTENSIONS[new_basebackend])
946
+ new_filetype = new_outfilesuffix[1..-1]
947
+ else
948
+ new_outfilesuffix, new_basebackend, new_filetype = '.html', 'html', 'html'
949
+ end
947
950
  attrs['outfilesuffix'] = new_outfilesuffix unless attribute_locked? 'outfilesuffix'
951
+ else
952
+ # NOTE ideally we shouldn't need the converter before the converter phase, but we do
953
+ raise ::NotImplementedError, %(asciidoctor: FAILED: missing converter for backend '#{new_backend}'. Processing aborted.)
948
954
  end
949
955
  if (current_filetype = attrs['filetype'])
950
956
  attrs.delete %(filetype-#{current_filetype})
@@ -957,38 +963,44 @@ class Document < AbstractBlock
957
963
  attrs.delete 'pagewidth'
958
964
  end
959
965
  if new_basebackend != current_basebackend
960
- if current_basebackend
961
- attrs.delete %(basebackend-#{current_basebackend})
962
- if current_doctype
966
+ if current_doctype
967
+ if current_basebackend
968
+ attrs.delete %(basebackend-#{current_basebackend})
963
969
  attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype})
964
970
  end
971
+ attrs[%(basebackend-#{new_basebackend}-doctype-#{current_doctype})] = ''
972
+ elsif current_basebackend
973
+ attrs.delete %(basebackend-#{current_basebackend})
965
974
  end
966
- attrs['basebackend'] = new_basebackend
967
975
  attrs[%(basebackend-#{new_basebackend})] = ''
968
- attrs[%(basebackend-#{new_basebackend}-doctype-#{current_doctype})] = '' if current_doctype
976
+ attrs['basebackend'] = new_basebackend
969
977
  end
970
- # clear cached backend value
971
- @backend = nil
978
+ return new_backend
972
979
  end
973
980
  end
974
981
 
982
+ # TODO document me
983
+ #
984
+ # Returns the String doctype if updated, nothing otherwise.
975
985
  def update_doctype_attributes new_doctype
976
- if new_doctype && new_doctype != @attributes['doctype']
977
- attrs = @attributes
978
- current_doctype = attrs['doctype']
979
- current_backend = attrs['backend']
980
- current_basebackend = attrs['basebackend']
986
+ if new_doctype && new_doctype != @doctype
987
+ current_backend, current_basebackend, current_doctype = @backend, (attrs = @attributes)['basebackend'], @doctype
981
988
  if current_doctype
982
989
  attrs.delete %(doctype-#{current_doctype})
983
- attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype}) if current_backend
984
- attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype}) if current_basebackend
990
+ if current_backend
991
+ attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype})
992
+ attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = ''
993
+ end
994
+ if current_basebackend
995
+ attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype})
996
+ attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = ''
997
+ end
998
+ else
999
+ attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = '' if current_backend
1000
+ attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = '' if current_basebackend
985
1001
  end
986
- attrs['doctype'] = new_doctype
987
1002
  attrs[%(doctype-#{new_doctype})] = ''
988
- attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = '' if current_backend
989
- attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = '' if current_basebackend
990
- # clear cached doctype value
991
- @doctype = nil
1003
+ return @doctype = attrs['doctype'] = new_doctype
992
1004
  end
993
1005
  end
994
1006
 
@@ -1033,17 +1045,15 @@ class Document < AbstractBlock
1033
1045
  @attributes.delete 'outdir' unless (@attributes['outdir'] = opts['outdir'])
1034
1046
  end
1035
1047
 
1036
- # QUESTION should we add processors that execute before conversion begins?
1037
- unless @converter
1038
- fail %(asciidoctor: FAILED: missing converter for backend '#{backend}'. Processing aborted.)
1039
- end
1048
+ # QUESTION should we add extensions that execute before conversion begins?
1040
1049
 
1041
1050
  if doctype == 'inline'
1042
- # QUESTION should we warn if @blocks.size > 0 and the first block is not a paragraph?
1043
- if (block = @blocks[0]) && block.content_model != :compound
1044
- output = block.content
1045
- else
1046
- output = nil
1051
+ if (block = @blocks[0])
1052
+ if block.content_model == :compound || block.content_model == :empty
1053
+ warn %(asciidoctor: WARNING: no inline candidate; use the inline doctype to convert a single paragragh, verbatim, or raw block)
1054
+ else
1055
+ output = block.content
1056
+ end
1047
1057
  end
1048
1058
  else
1049
1059
  transform = ((opts.key? :header_footer) ? opts[:header_footer] : @options[:header_footer]) ? 'document' : 'embedded'
@@ -1062,7 +1072,7 @@ class Document < AbstractBlock
1062
1072
  end
1063
1073
 
1064
1074
  # Alias render to convert to maintain backwards compatibility
1065
- alias :render :convert
1075
+ alias render convert
1066
1076
 
1067
1077
  # Public: Write the output to the specified file
1068
1078
  #
@@ -1076,10 +1086,10 @@ class Document < AbstractBlock
1076
1086
  unless output.nil_or_empty?
1077
1087
  target.write output.chomp
1078
1088
  # ensure there's a trailing endline
1079
- target.write EOL
1089
+ target.write LF
1080
1090
  end
1081
1091
  else
1082
- ::File.open(target, 'w') {|f| f.write output }
1092
+ ::IO.write target, output
1083
1093
  end
1084
1094
  nil
1085
1095
  end
@@ -1126,11 +1136,9 @@ class Document < AbstractBlock
1126
1136
  if safe >= SafeMode::SECURE
1127
1137
  ''
1128
1138
  else
1129
- qualifier = location == :head ? nil : %(-#{location})
1139
+ content = []
1140
+ qualifier = %(-#{location}) unless location == :head
1130
1141
  suffix = @outfilesuffix unless suffix
1131
- docinfodir = @attributes['docinfodir']
1132
-
1133
- content = nil
1134
1142
 
1135
1143
  if (docinfo = @attributes['docinfo']).nil_or_empty?
1136
1144
  if @attributes.key? 'docinfo2'
@@ -1141,51 +1149,46 @@ class Document < AbstractBlock
1141
1149
  docinfo = docinfo ? ['private'] : nil
1142
1150
  end
1143
1151
  else
1144
- docinfo = docinfo.split(',').map(&:strip)
1152
+ docinfo = docinfo.split(',').map {|it| it.strip }
1145
1153
  end
1146
1154
 
1147
1155
  if docinfo
1148
- docinfo_filename = %(docinfo#{qualifier}#{suffix})
1156
+ docinfo_file, docinfo_dir, docinfo_subs = %(docinfo#{qualifier}#{suffix}), @attributes['docinfodir'], resolve_docinfo_subs
1149
1157
  unless (docinfo & ['shared', %(shared-#{location})]).empty?
1150
- docinfo_path = normalize_system_path(docinfo_filename, docinfodir)
1158
+ docinfo_path = normalize_system_path docinfo_file, docinfo_dir
1151
1159
  # NOTE normalizing the lines is essential if we're performing substitutions
1152
- if (content = read_asset(docinfo_path, :normalize => true))
1153
- if (docinfosubs ||= resolve_docinfo_subs)
1154
- content = (docinfosubs == :attributes) ? sub_attributes(content) : apply_subs(content, docinfosubs)
1155
- end
1160
+ if (shd_content = (read_asset docinfo_path, :normalize => true))
1161
+ content << (apply_subs shd_content, docinfo_subs)
1156
1162
  end
1157
1163
  end
1158
1164
 
1159
1165
  unless @attributes['docname'].nil_or_empty? || (docinfo & ['private', %(private-#{location})]).empty?
1160
- docinfo_path = normalize_system_path(%(#{@attributes['docname']}-#{docinfo_filename}), docinfodir)
1166
+ docinfo_path = normalize_system_path %(#{@attributes['docname']}-#{docinfo_file}), docinfo_dir
1161
1167
  # NOTE normalizing the lines is essential if we're performing substitutions
1162
- if (content2 = read_asset(docinfo_path, :normalize => true))
1163
- if (docinfosubs ||= resolve_docinfo_subs)
1164
- content2 = (docinfosubs == :attributes) ? sub_attributes(content2) : apply_subs(content2, docinfosubs)
1165
- end
1166
- content = content ? %(#{content}#{EOL}#{content2}) : content2
1168
+ if (pvt_content = (read_asset docinfo_path, :normalize => true))
1169
+ content << (apply_subs pvt_content, docinfo_subs)
1167
1170
  end
1168
1171
  end
1169
1172
  end
1170
1173
 
1171
1174
  # TODO allow document to control whether extension docinfo is contributed
1172
- if @extensions && docinfo_processors?(location)
1173
- contentx = @docinfo_processor_extensions[location].map {|candidate| candidate.process_method[self] }.compact * EOL
1174
- content = content ? %(#{content}#{EOL}#{contentx}) : contentx
1175
+ if @extensions && (docinfo_processors? location)
1176
+ content += @docinfo_processor_extensions[location].map {|ext| ext.process_method[self] }.compact
1175
1177
  end
1176
1178
 
1177
- # coerce to string (in case the value is nil)
1178
- %(#{content})
1179
+ content * LF
1179
1180
  end
1180
1181
  end
1181
1182
 
1183
+ # Internal: Resolve the list of comma-delimited subs to apply to docinfo files.
1184
+ #
1185
+ # Resolve the list of substitutions from the value of the docinfosubs
1186
+ # document attribute, if specified. Otherwise, return an Array containing
1187
+ # the Symbol :attributes.
1188
+ #
1189
+ # Returns an [Array] of substitution [Symbol]s
1182
1190
  def resolve_docinfo_subs
1183
- if @attributes.key? 'docinfosubs'
1184
- subs = resolve_subs @attributes['docinfosubs'], :block, nil, 'docinfo'
1185
- subs.empty? ? nil : subs
1186
- else
1187
- :attributes
1188
- end
1191
+ (@attributes.key? 'docinfosubs') ? (resolve_subs @attributes['docinfosubs'], :block, nil, 'docinfo') : [:attributes]
1189
1192
  end
1190
1193
 
1191
1194
  def docinfo_processors?(location = :head)