asciidoctor 1.5.8 → 2.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +11 -0
  3. data/CHANGELOG.adoc +628 -45
  4. data/LICENSE +2 -1
  5. data/README-de.adoc +28 -38
  6. data/README-fr.adoc +30 -43
  7. data/README-jp.adoc +255 -201
  8. data/README-zh_CN.adoc +40 -44
  9. data/README.adoc +170 -143
  10. data/asciidoctor.gemspec +22 -34
  11. data/bin/asciidoctor +5 -4
  12. data/data/locale/attributes-ar.adoc +4 -3
  13. data/data/locale/attributes-be.adoc +23 -0
  14. data/data/locale/attributes-bg.adoc +4 -3
  15. data/data/locale/attributes-ca.adoc +6 -5
  16. data/data/locale/attributes-cs.adoc +4 -3
  17. data/data/locale/attributes-da.adoc +6 -5
  18. data/data/locale/attributes-de.adoc +6 -5
  19. data/data/locale/attributes-en.adoc +4 -4
  20. data/data/locale/attributes-es.adoc +6 -5
  21. data/data/locale/attributes-fa.adoc +4 -3
  22. data/data/locale/attributes-fi.adoc +4 -3
  23. data/data/locale/attributes-fr.adoc +8 -7
  24. data/data/locale/attributes-hu.adoc +4 -3
  25. data/data/locale/attributes-id.adoc +4 -3
  26. data/data/locale/attributes-it.adoc +6 -5
  27. data/data/locale/attributes-ja.adoc +4 -3
  28. data/data/locale/{attributes-kr.adoc → attributes-ko.adoc} +4 -3
  29. data/data/locale/attributes-nb.adoc +4 -3
  30. data/data/locale/attributes-nl.adoc +6 -5
  31. data/data/locale/attributes-nn.adoc +4 -3
  32. data/data/locale/attributes-pl.adoc +8 -7
  33. data/data/locale/attributes-pt.adoc +6 -5
  34. data/data/locale/attributes-pt_BR.adoc +6 -5
  35. data/data/locale/attributes-ro.adoc +4 -3
  36. data/data/locale/attributes-ru.adoc +6 -5
  37. data/data/locale/attributes-sr.adoc +4 -4
  38. data/data/locale/attributes-sr_Latn.adoc +4 -4
  39. data/data/locale/attributes-sv.adoc +4 -4
  40. data/data/locale/attributes-th.adoc +23 -0
  41. data/data/locale/attributes-tr.adoc +4 -3
  42. data/data/locale/attributes-uk.adoc +6 -5
  43. data/data/locale/attributes-vi.adoc +23 -0
  44. data/data/locale/attributes-zh_CN.adoc +4 -3
  45. data/data/locale/attributes-zh_TW.adoc +4 -3
  46. data/data/reference/syntax.adoc +296 -0
  47. data/data/stylesheets/asciidoctor-default.css +120 -114
  48. data/data/stylesheets/coderay-asciidoctor.css +15 -17
  49. data/lib/asciidoctor/abstract_block.rb +146 -140
  50. data/lib/asciidoctor/abstract_node.rb +152 -170
  51. data/lib/asciidoctor/attribute_list.rb +77 -89
  52. data/lib/asciidoctor/block.rb +29 -28
  53. data/lib/asciidoctor/callouts.rb +4 -2
  54. data/lib/asciidoctor/cli/invoker.rb +20 -24
  55. data/lib/asciidoctor/cli/options.rb +107 -96
  56. data/lib/asciidoctor/cli.rb +3 -2
  57. data/lib/asciidoctor/convert.rb +199 -0
  58. data/lib/asciidoctor/converter/composite.rb +40 -48
  59. data/lib/asciidoctor/converter/docbook5.rb +627 -644
  60. data/lib/asciidoctor/converter/html5.rb +1053 -951
  61. data/lib/asciidoctor/converter/manpage.rb +581 -532
  62. data/lib/asciidoctor/converter/template.rb +232 -271
  63. data/lib/asciidoctor/converter.rb +370 -185
  64. data/lib/asciidoctor/core_ext/float/truncate.rb +20 -0
  65. data/lib/asciidoctor/core_ext/hash/merge.rb +8 -0
  66. data/lib/asciidoctor/core_ext/match_data/names.rb +7 -0
  67. data/lib/asciidoctor/core_ext/nil_or_empty.rb +1 -0
  68. data/lib/asciidoctor/core_ext/regexp/is_match.rb +4 -2
  69. data/lib/asciidoctor/core_ext.rb +8 -17
  70. data/lib/asciidoctor/document.rb +503 -461
  71. data/lib/asciidoctor/extensions.rb +127 -174
  72. data/lib/asciidoctor/helpers.rb +184 -107
  73. data/lib/asciidoctor/inline.rb +9 -12
  74. data/lib/asciidoctor/list.rb +11 -29
  75. data/lib/asciidoctor/load.rb +119 -0
  76. data/lib/asciidoctor/logging.rb +22 -17
  77. data/lib/asciidoctor/parser.rb +673 -719
  78. data/lib/asciidoctor/path_resolver.rb +48 -33
  79. data/lib/asciidoctor/reader.rb +383 -338
  80. data/lib/asciidoctor/rouge_ext.rb +39 -0
  81. data/lib/asciidoctor/rx.rb +723 -0
  82. data/lib/asciidoctor/section.rb +17 -16
  83. data/lib/asciidoctor/stylesheets.rb +19 -37
  84. data/lib/asciidoctor/substitutors.rb +926 -1022
  85. data/lib/asciidoctor/syntax_highlighter/coderay.rb +88 -0
  86. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +34 -0
  87. data/lib/asciidoctor/syntax_highlighter/html_pipeline.rb +10 -0
  88. data/lib/asciidoctor/syntax_highlighter/prettify.rb +30 -0
  89. data/lib/asciidoctor/syntax_highlighter/pygments.rb +157 -0
  90. data/lib/asciidoctor/syntax_highlighter/rouge.rb +143 -0
  91. data/lib/asciidoctor/syntax_highlighter.rb +253 -0
  92. data/lib/asciidoctor/table.rb +152 -114
  93. data/lib/asciidoctor/timings.rb +7 -5
  94. data/lib/asciidoctor/version.rb +2 -1
  95. data/lib/asciidoctor/writer.rb +30 -0
  96. data/lib/asciidoctor.rb +266 -1340
  97. data/man/asciidoctor.1 +49 -47
  98. data/man/asciidoctor.adoc +54 -45
  99. metadata +50 -245
  100. data/CONTRIBUTING.adoc +0 -185
  101. data/Gemfile +0 -60
  102. data/Rakefile +0 -129
  103. data/bin/asciidoctor-safe +0 -15
  104. data/features/open_block.feature +0 -92
  105. data/features/pass_block.feature +0 -66
  106. data/features/step_definitions.rb +0 -49
  107. data/features/text_formatting.feature +0 -57
  108. data/features/xref.feature +0 -1039
  109. data/lib/asciidoctor/converter/base.rb +0 -59
  110. data/lib/asciidoctor/converter/docbook45.rb +0 -93
  111. data/lib/asciidoctor/converter/factory.rb +0 -226
  112. data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +0 -6
  113. data/lib/asciidoctor/core_ext/1.8.7/concurrent/hash.rb +0 -5
  114. data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +0 -4
  115. data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +0 -6
  116. data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +0 -5
  117. data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +0 -6
  118. data/lib/asciidoctor/core_ext/1.8.7/string/limit_bytesize.rb +0 -29
  119. data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +0 -6
  120. data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +0 -6
  121. data/lib/asciidoctor/core_ext/string/limit_bytesize.rb +0 -10
  122. data/test/api_test.rb +0 -1240
  123. data/test/attribute_list_test.rb +0 -242
  124. data/test/attributes_test.rb +0 -1623
  125. data/test/blocks_test.rb +0 -3870
  126. data/test/converter_test.rb +0 -470
  127. data/test/document_test.rb +0 -1853
  128. data/test/extensions_test.rb +0 -1560
  129. data/test/fixtures/asciidoc_index.txt +0 -521
  130. data/test/fixtures/basic-docinfo-footer.html +0 -6
  131. data/test/fixtures/basic-docinfo-footer.xml +0 -8
  132. data/test/fixtures/basic-docinfo.html +0 -1
  133. data/test/fixtures/basic-docinfo.xml +0 -4
  134. data/test/fixtures/basic.asciidoc +0 -5
  135. data/test/fixtures/chapter-a.adoc +0 -3
  136. data/test/fixtures/child-include.adoc +0 -5
  137. data/test/fixtures/circle.svg +0 -9
  138. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +0 -6
  139. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +0 -6
  140. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +0 -3
  141. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +0 -5
  142. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +0 -1
  143. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +0 -6
  144. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +0 -3
  145. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +0 -5
  146. data/test/fixtures/custom-docinfodir/basic-docinfo.html +0 -1
  147. data/test/fixtures/custom-docinfodir/docinfo.html +0 -1
  148. data/test/fixtures/docinfo-footer.html +0 -1
  149. data/test/fixtures/docinfo-footer.xml +0 -9
  150. data/test/fixtures/docinfo.html +0 -1
  151. data/test/fixtures/docinfo.xml +0 -3
  152. data/test/fixtures/doctime-localtime.adoc +0 -2
  153. data/test/fixtures/dot.gif +0 -0
  154. data/test/fixtures/encoding.asciidoc +0 -13
  155. data/test/fixtures/file-with-missing-include.adoc +0 -1
  156. data/test/fixtures/grandchild-include.adoc +0 -3
  157. data/test/fixtures/hello-asciidoctor.pdf +0 -69
  158. data/test/fixtures/include-file.asciidoc +0 -24
  159. data/test/fixtures/include-file.jsx +0 -8
  160. data/test/fixtures/include-file.ml +0 -3
  161. data/test/fixtures/include-file.xml +0 -5
  162. data/test/fixtures/lists.adoc +0 -96
  163. data/test/fixtures/master.adoc +0 -5
  164. data/test/fixtures/mismatched-end-tag.adoc +0 -7
  165. data/test/fixtures/other-chapters.adoc +0 -11
  166. data/test/fixtures/outer-include.adoc +0 -5
  167. data/test/fixtures/parent-include-restricted.adoc +0 -5
  168. data/test/fixtures/parent-include.adoc +0 -5
  169. data/test/fixtures/sample.asciidoc +0 -30
  170. data/test/fixtures/section-a.adoc +0 -4
  171. data/test/fixtures/stylesheets/custom.css +0 -3
  172. data/test/fixtures/subdir/index.adoc +0 -3
  173. data/test/fixtures/subdir/inner-include.adoc +0 -3
  174. data/test/fixtures/subdir/middle-include.adoc +0 -5
  175. data/test/fixtures/subs-docinfo.html +0 -2
  176. data/test/fixtures/subs.adoc +0 -6
  177. data/test/fixtures/tagged-class-enclosed.rb +0 -25
  178. data/test/fixtures/tagged-class.rb +0 -23
  179. data/test/fixtures/tip.gif +0 -0
  180. data/test/fixtures/unclosed-tag.adoc +0 -3
  181. data/test/fixtures/unexpected-end-tag.adoc +0 -4
  182. data/test/invoker_test.rb +0 -745
  183. data/test/links_test.rb +0 -855
  184. data/test/lists_test.rb +0 -5151
  185. data/test/logger_test.rb +0 -211
  186. data/test/manpage_test.rb +0 -660
  187. data/test/options_test.rb +0 -262
  188. data/test/paragraphs_test.rb +0 -562
  189. data/test/parser_test.rb +0 -742
  190. data/test/paths_test.rb +0 -395
  191. data/test/preamble_test.rb +0 -173
  192. data/test/reader_test.rb +0 -2161
  193. data/test/sections_test.rb +0 -3575
  194. data/test/substitutions_test.rb +0 -2066
  195. data/test/tables_test.rb +0 -2036
  196. data/test/test_helper.rb +0 -447
  197. data/test/text_test.rb +0 -309
@@ -1,4 +1,4 @@
1
- # encoding: UTF-8
1
+ # frozen_string_literal: true
2
2
  module Asciidoctor
3
3
  # Public: The Document class represents a parsed AsciiDoc document.
4
4
  #
@@ -13,7 +13,7 @@ module Asciidoctor
13
13
  # :safe (or :unsafe) to enable all of Asciidoctor's features.
14
14
  #
15
15
  # Asciidoctor.load '= Hello, AsciiDoc!', safe: :safe
16
- # # => Asciidoctor::Document { doctype: "article", doctitle: "Hello, Asciidoc!", blocks: 0 }
16
+ # # => Asciidoctor::Document { doctype: "article", doctitle: "Hello, AsciiDoc!", blocks: 0 }
17
17
  #
18
18
  # Instances of this class can be used to extract information from the document
19
19
  # or alter its structure. As such, the Document object is most often used in
@@ -153,9 +153,9 @@ class Document < AbstractBlock
153
153
  #
154
154
  # A value of 10 (SERVER) disallows the document from setting attributes that
155
155
  # would affect the conversion of the document, in addition to all the security
156
- # features of SafeMode::SAFE. For instance, this value disallows changing the
157
- # backend or the source-highlighter using an attribute defined in the source
158
- # document. This is the most fundamental level of security for server-side
156
+ # features of SafeMode::SAFE. For instance, this level forbids changing the
157
+ # backend or source-highlighter using an attribute defined in the source
158
+ # document header. This is the most fundamental level of security for server
159
159
  # deployments (hence the name).
160
160
  #
161
161
  # A value of 20 (SECURE) disallows the document from attempting to read files
@@ -167,7 +167,7 @@ class Document < AbstractBlock
167
167
  # trusted content into the document).
168
168
  #
169
169
  # Since Asciidoctor is aiming for wide adoption, 20 (SECURE) is the default
170
- # value and is recommended for server-side deployments.
170
+ # value and is recommended for server deployments.
171
171
  #
172
172
  # A value of 100 (PARANOID) is planned to disallow the use of passthrough
173
173
  # macros and prevents the document from setting any known attributes in
@@ -198,13 +198,13 @@ class Document < AbstractBlock
198
198
  # Public: Get the document catalog Hash
199
199
  attr_reader :catalog
200
200
 
201
- # Public: Alias catalog property as references for backwards compatiblity
201
+ # Public: Alias catalog property as references for backwards compatibility
202
202
  alias references catalog
203
203
 
204
204
  # Public: Get the Hash of document counters
205
205
  attr_reader :counters
206
206
 
207
- # Public: Get the level-0 Section
207
+ # Public: Get the level-0 Section (i.e., doctitle). (Only stores the title, not the header attributes).
208
208
  attr_reader :header
209
209
 
210
210
  # Public: Get the String base directory for converting this document.
@@ -231,6 +231,9 @@ class Document < AbstractBlock
231
231
  # Public: Get the Converter associated with this document
232
232
  attr_reader :converter
233
233
 
234
+ # Public: Get the SyntaxHighlighter associated with this document
235
+ attr_reader :syntax_highlighter
236
+
234
237
  # Public: Get the activated Extensions::Registry associated with this document.
235
238
  attr_reader :extensions
236
239
 
@@ -238,7 +241,7 @@ class Document < AbstractBlock
238
241
  #
239
242
  # data - The AsciiDoc source data as a String or String Array. (default: nil)
240
243
  # options - A Hash of options to control processing (e.g., safe mode value (:safe), backend (:backend),
241
- # header/footer toggle (:header_footer), custom attributes (:attributes)). (default: {})
244
+ # standalone enclosure (:standalone), custom attributes (:attributes)). (default: {})
242
245
  #
243
246
  # Duplication of the options Hash is handled in the enclosing API.
244
247
  #
@@ -254,37 +257,37 @@ class Document < AbstractBlock
254
257
  @parent_document = parent_doc
255
258
  options[:base_dir] ||= parent_doc.base_dir
256
259
  options[:catalog_assets] = true if parent_doc.options[:catalog_assets]
257
- @catalog = parent_doc.catalog.inject({}) do |accum, (key, table)|
258
- accum[key] = (key == :footnotes ? [] : table)
259
- accum
260
- end
260
+ @catalog = parent_doc.catalog.merge footnotes: []
261
261
  # QUESTION should we support setting attribute in parent document from nested document?
262
- # NOTE we must dup or else all the assignments to the overrides clobbers the real attributes
263
- @attribute_overrides = attr_overrides = parent_doc.attributes.dup
264
- parent_doctype = attr_overrides.delete 'doctype'
262
+ @attribute_overrides = attr_overrides = (parent_doc.instance_variable_get :@attribute_overrides).merge parent_doc.attributes
265
263
  attr_overrides.delete 'compat-mode'
264
+ parent_doctype = attr_overrides.delete 'doctype'
265
+ attr_overrides.delete 'notitle'
266
+ attr_overrides.delete 'showtitle'
267
+ # QUESTION if toc is hard unset in parent document, should it be hard unset in nested document?
266
268
  attr_overrides.delete 'toc'
267
- attr_overrides.delete 'toc-placement'
269
+ @attributes['toc-placement'] = (attr_overrides.delete 'toc-placement') || 'auto'
268
270
  attr_overrides.delete 'toc-position'
269
271
  @safe = parent_doc.safe
270
272
  @attributes['compat-mode'] = '' if (@compat_mode = parent_doc.compat_mode)
273
+ @outfilesuffix = parent_doc.outfilesuffix
271
274
  @sourcemap = parent_doc.sourcemap
272
275
  @timings = nil
273
276
  @path_resolver = parent_doc.path_resolver
274
277
  @converter = parent_doc.converter
275
- initialize_extensions = false
278
+ initialize_extensions = nil
276
279
  @extensions = parent_doc.extensions
280
+ @syntax_highlighter = parent_doc.syntax_highlighter
277
281
  else
278
282
  @parent_document = nil
279
283
  @catalog = {
280
- :ids => {},
281
- :refs => {},
282
- :footnotes => [],
283
- :links => [],
284
- :images => [],
285
- :indexterms => [],
286
- :callouts => Callouts.new,
287
- :includes => {},
284
+ ids: {}, # deprecated; kept for backwards compatibility with converters
285
+ refs: {},
286
+ footnotes: [],
287
+ links: [],
288
+ images: [],
289
+ callouts: Callouts.new,
290
+ includes: {},
288
291
  }
289
292
  # copy attributes map and normalize keys
290
293
  # attribute overrides are attributes that can only be set from the commandline
@@ -307,8 +310,8 @@ class Document < AbstractBlock
307
310
  end
308
311
  attr_overrides[key.downcase] = val
309
312
  end
310
- if (to_file = options[:to_file])
311
- attr_overrides['outfilesuffix'] = ::File.extname to_file
313
+ if ::String === (to_file = options[:to_file])
314
+ attr_overrides['outfilesuffix'] = Helpers.extname to_file
312
315
  end
313
316
  # safely resolve the safe mode from const, int or string
314
317
  if !(safe_mode = options[:safe])
@@ -317,88 +320,69 @@ class Document < AbstractBlock
317
320
  # be permissive in case API user wants to define new levels
318
321
  @safe = safe_mode
319
322
  else
320
- # NOTE: not using infix rescue for performance reasons, see https://github.com/jruby/jruby/issues/1816
321
- begin
322
- @safe = SafeMode.value_for_name safe_mode.to_s
323
- rescue
324
- @safe = SafeMode::SECURE
325
- end
323
+ @safe = (SafeMode.value_for_name safe_mode) rescue SafeMode::SECURE
326
324
  end
325
+ input_mtime = options.delete :input_mtime
327
326
  @compat_mode = attr_overrides.key? 'compat-mode'
328
327
  @sourcemap = options[:sourcemap]
329
328
  @timings = options.delete :timings
330
329
  @path_resolver = PathResolver.new
331
- @converter = nil
332
- initialize_extensions = defined? ::Asciidoctor::Extensions
333
- @extensions = nil # initialize furthur down
330
+ initialize_extensions = (defined? ::Asciidoctor::Extensions) || (options.key? :extensions) ? ::Asciidoctor::Extensions : nil
331
+ @extensions = nil # initialize further down if initialize_extensions is true
332
+ options[:standalone] = options[:header_footer] if (options.key? :header_footer) && !(options.key? :standalone)
334
333
  end
335
334
 
336
- @parsed = false
337
- @header = @header_attributes = nil
335
+ @parsed = @reftexts = @header = @header_attributes = nil
338
336
  @counters = {}
339
337
  @attributes_modified = ::Set.new
340
338
  @docinfo_processor_extensions = {}
341
- header_footer = (options[:header_footer] ||= false)
339
+ standalone = options[:standalone]
342
340
  (@options = options).freeze
343
341
 
344
342
  attrs = @attributes
345
- #attrs['encoding'] = 'UTF-8'
346
- attrs['sectids'] = ''
347
- attrs['toc-placement'] = 'auto'
348
- if header_footer
349
- attrs['copycss'] = ''
350
- # sync embedded attribute with :header_footer option value
343
+ unless parent_doc
344
+ attrs['attribute-undefined'] = Compliance.attribute_undefined
345
+ attrs['attribute-missing'] = Compliance.attribute_missing
346
+ attrs.update DEFAULT_ATTRIBUTES
347
+ # TODO if lang attribute is set, @safe mode < SafeMode::SERVER, and !parent_doc,
348
+ # load attributes from data/locale/attributes-<lang>.adoc
349
+ end
350
+
351
+ if standalone
352
+ # sync embedded attribute with :standalone option value
351
353
  attr_overrides['embedded'] = nil
354
+ attrs['copycss'] = ''
355
+ attrs['iconfont-remote'] = ''
356
+ attrs['stylesheet'] = ''
357
+ attrs['webfonts'] = ''
352
358
  else
353
- attrs['notitle'] = ''
354
- # sync embedded attribute with :header_footer option value
359
+ # sync embedded attribute with :standalone option value
355
360
  attr_overrides['embedded'] = ''
361
+ if (attr_overrides.key? 'showtitle') && (attr_overrides.keys & %w(notitle showtitle))[-1] == 'showtitle'
362
+ attr_overrides['notitle'] = { nil => '', false => '@', '@' => false }[attr_overrides['showtitle']]
363
+ elsif attr_overrides.key? 'notitle'
364
+ attr_overrides['showtitle'] = { nil => '', false => '@', '@' => false }[attr_overrides['notitle']]
365
+ else
366
+ attrs['notitle'] = ''
367
+ end
356
368
  end
357
- attrs['stylesheet'] = ''
358
- attrs['webfonts'] = ''
359
- attrs['prewrap'] = ''
360
- attrs['attribute-undefined'] = Compliance.attribute_undefined
361
- attrs['attribute-missing'] = Compliance.attribute_missing
362
- attrs['iconfont-remote'] = ''
363
-
364
- # language strings
365
- # TODO load these based on language settings
366
- attrs['caution-caption'] = 'Caution'
367
- attrs['important-caption'] = 'Important'
368
- attrs['note-caption'] = 'Note'
369
- attrs['tip-caption'] = 'Tip'
370
- attrs['warning-caption'] = 'Warning'
371
- attrs['example-caption'] = 'Example'
372
- attrs['figure-caption'] = 'Figure'
373
- #attrs['listing-caption'] = 'Listing'
374
- attrs['table-caption'] = 'Table'
375
- attrs['toc-title'] = 'Table of Contents'
376
- #attrs['preface-title'] = 'Preface'
377
- attrs['section-refsig'] = 'Section'
378
- attrs['part-refsig'] = 'Part'
379
- attrs['chapter-refsig'] = 'Chapter'
380
- attrs['appendix-caption'] = attrs['appendix-refsig'] = 'Appendix'
381
- attrs['untitled-label'] = 'Untitled'
382
- attrs['version-label'] = 'Version'
383
- attrs['last-update-label'] = 'Last updated'
384
369
 
385
370
  attr_overrides['asciidoctor'] = ''
386
- attr_overrides['asciidoctor-version'] = VERSION
371
+ attr_overrides['asciidoctor-version'] = ::Asciidoctor::VERSION
387
372
 
388
373
  attr_overrides['safe-mode-name'] = (safe_mode_name = SafeMode.name_for_value @safe)
389
- attr_overrides["safe-mode-#{safe_mode_name}"] = ''
374
+ attr_overrides[%(safe-mode-#{safe_mode_name})] = ''
390
375
  attr_overrides['safe-mode-level'] = @safe
391
376
 
392
- # the only way to set the max-include-depth attribute is via the API; default to 64 like AsciiDoc Python
377
+ # the only way to set the max-include-depth attribute is via the API; default to 64 like AsciiDoc.py
393
378
  attr_overrides['max-include-depth'] ||= 64
394
379
 
395
380
  # the only way to set the allow-uri-read attribute is via the API; disabled by default
396
381
  attr_overrides['allow-uri-read'] ||= nil
397
382
 
398
- attr_overrides['user-home'] = USER_HOME
399
-
400
- # legacy support for numbered attribute
383
+ # remap legacy attribute names
401
384
  attr_overrides['sectnums'] = attr_overrides.delete 'numbered' if attr_overrides.key? 'numbered'
385
+ attr_overrides['hardbreaks-option'] = attr_overrides.delete 'hardbreaks' if attr_overrides.key? 'hardbreaks'
402
386
 
403
387
  # If the base_dir option is specified, it overrides docdir and is used as the root for relative
404
388
  # paths. Otherwise, the base_dir is the directory of the source file (docdir), if set, otherwise
@@ -414,11 +398,11 @@ class Document < AbstractBlock
414
398
 
415
399
  # allow common attributes backend and doctype to be set using options hash, coerce values to string
416
400
  if (backend_val = options[:backend])
417
- attr_overrides['backend'] = %(#{backend_val})
401
+ attr_overrides['backend'] = backend_val.to_s
418
402
  end
419
403
 
420
404
  if (doctype_val = options[:doctype])
421
- attr_overrides['doctype'] = %(#{doctype_val})
405
+ attr_overrides['doctype'] = doctype_val.to_s
422
406
  end
423
407
 
424
408
  if @safe >= SafeMode::SERVER
@@ -431,7 +415,7 @@ class Document < AbstractBlock
431
415
  attr_overrides['docfile'] = attr_overrides['docfile'][(attr_overrides['docdir'].length + 1)..-1]
432
416
  end
433
417
  attr_overrides['docdir'] = ''
434
- attr_overrides['user-home'] = '.'
418
+ attr_overrides['user-home'] ||= '.'
435
419
  if @safe >= SafeMode::SECURE
436
420
  attr_overrides['max-attribute-value-size'] = 4096 unless attr_overrides.key? 'max-attribute-value-size'
437
421
  # assign linkcss (preventing css embedding) unless explicitly disabled from the commandline or API
@@ -440,6 +424,8 @@ class Document < AbstractBlock
440
424
  # restrict document from enabling icons
441
425
  attr_overrides['icons'] ||= nil
442
426
  end
427
+ else
428
+ attr_overrides['user-home'] ||= USER_HOME
443
429
  end
444
430
 
445
431
  # the only way to set the max-attribute-value-size attribute is via the API; disabled by default
@@ -482,58 +468,39 @@ class Document < AbstractBlock
482
468
  else
483
469
  # setup default backend and doctype
484
470
  @backend = nil
485
- if (attrs['backend'] ||= DEFAULT_BACKEND) == 'manpage'
471
+ if (initial_backend = attrs['backend'] || DEFAULT_BACKEND) == 'manpage'
486
472
  @doctype = attrs['doctype'] = attr_overrides['doctype'] = 'manpage'
487
473
  else
488
474
  @doctype = (attrs['doctype'] ||= DEFAULT_DOCTYPE)
489
475
  end
490
- update_backend_attributes attrs['backend'], true
491
-
492
- #attrs['indir'] = attrs['docdir']
493
- #attrs['infile'] = attrs['docfile']
476
+ update_backend_attributes initial_backend, true
494
477
 
495
478
  # dynamic intrinstic attribute values
496
479
 
497
- # See https://reproducible-builds.org/specs/source-date-epoch/
498
- # NOTE Opal can't call key? on ENV
499
- now = ::ENV['SOURCE_DATE_EPOCH'] ? ::Time.at(Integer ::ENV['SOURCE_DATE_EPOCH']).utc : ::Time.now
500
- if (localdate = attrs['localdate'])
501
- localyear = (attrs['localyear'] ||= ((localdate.index '-') == 4 ? (localdate.slice 0, 4) : nil))
502
- else
503
- localdate = attrs['localdate'] = (now.strftime '%F')
504
- localyear = (attrs['localyear'] ||= now.year.to_s)
505
- end
506
- # %Z is OS dependent and may contain characters that aren't UTF-8 encoded (see asciidoctor#2770 and asciidoctor.js#23)
507
- # Ruby 1.8 doesn't support %:z
508
- localtime = (attrs['localtime'] ||= now.strftime %(%T #{now.utc_offset == 0 ? 'UTC' : '%z'}))
509
- attrs['localdatetime'] ||= %(#{localdate} #{localtime})
510
-
511
- # docdate, doctime and docdatetime should default to
512
- # localdate, localtime and localdatetime if not otherwise set
513
- attrs['docdate'] ||= localdate
514
- attrs['docyear'] ||= localyear
515
- attrs['doctime'] ||= localtime
516
- attrs['docdatetime'] ||= %(#{localdate} #{localtime})
480
+ #attrs['indir'] = attrs['docdir']
481
+ #attrs['infile'] = attrs['docfile']
517
482
 
518
483
  # fallback directories
519
484
  attrs['stylesdir'] ||= '.'
520
485
  attrs['iconsdir'] ||= %(#{attrs.fetch 'imagesdir', './images'}/icons)
521
486
 
487
+ fill_datetime_attributes attrs, input_mtime
488
+
522
489
  if initialize_extensions
523
490
  if (ext_registry = options[:extension_registry])
524
491
  # QUESTION should we warn if the value type of this option is not a registry
525
- if Extensions::Registry === ext_registry || (::RUBY_ENGINE_JRUBY &&
492
+ if Extensions::Registry === ext_registry || ((defined? ::AsciidoctorJ::Extensions::ExtensionRegistry) &&
526
493
  ::AsciidoctorJ::Extensions::ExtensionRegistry === ext_registry)
527
494
  @extensions = ext_registry.activate self
528
495
  end
529
- elsif ::Proc === (ext_block = options[:extensions])
496
+ elsif (ext_block = options[:extensions]).nil?
497
+ @extensions = Extensions::Registry.new.activate self unless Extensions.groups.empty?
498
+ elsif ::Proc === ext_block
530
499
  @extensions = Extensions.create(&ext_block).activate self
531
- elsif !Extensions.groups.empty?
532
- @extensions = Extensions::Registry.new.activate self
533
500
  end
534
501
  end
535
502
 
536
- @reader = PreprocessorReader.new self, data, (Reader::Cursor.new attrs['docfile'], @base_dir), :normalize => true
503
+ @reader = PreprocessorReader.new self, data, (Reader::Cursor.new attrs['docfile'], @base_dir), normalize: true
537
504
  @source_location = @reader.cursor if @sourcemap
538
505
  end
539
506
  end
@@ -556,7 +523,7 @@ class Document < AbstractBlock
556
523
  doc = self
557
524
  # create reader if data is provided (used when data is not known at the time the Document object is created)
558
525
  if data
559
- @reader = PreprocessorReader.new doc, data, (Reader::Cursor.new @attributes['docfile'], @base_dir), :normalize => true
526
+ @reader = PreprocessorReader.new doc, data, (Reader::Cursor.new @attributes['docfile'], @base_dir), normalize: true
560
527
  @source_location = @reader.cursor if @sourcemap
561
528
  end
562
529
 
@@ -567,7 +534,7 @@ class Document < AbstractBlock
567
534
  end
568
535
 
569
536
  # Now parse the lines in the reader into blocks
570
- Parser.parse @reader, doc, :header_only => @options[:parse_header_only]
537
+ Parser.parse @reader, doc, header_only: @options[:parse_header_only]
571
538
 
572
539
  # should we call sort of post-parse function?
573
540
  restore_attributes
@@ -585,6 +552,11 @@ class Document < AbstractBlock
585
552
  end
586
553
  end
587
554
 
555
+ # Public: Returns whether the source lines of the document have been parsed.
556
+ def parsed?
557
+ @parsed
558
+ end
559
+
588
560
  # Public: Get the named counter and take the next number in the sequence.
589
561
  #
590
562
  # name - the String name of the counter
@@ -593,13 +565,15 @@ class Document < AbstractBlock
593
565
  # returns the next number in the sequence for the specified counter
594
566
  def counter name, seed = nil
595
567
  return @parent_document.counter name, seed if @parent_document
596
- if (attr_seed = !(attr_val = @attributes[name]).nil_or_empty?) && (@counters.key? name)
597
- @attributes[name] = @counters[name] = (nextval attr_val)
568
+ if ((locked = attribute_locked? name) && (curr_val = @counters[name])) || !(curr_val = @attributes[name]).nil_or_empty?
569
+ next_val = @counters[name] = Helpers.nextval curr_val
598
570
  elsif seed
599
- @attributes[name] = @counters[name] = (seed == seed.to_i.to_s ? seed.to_i : seed)
571
+ next_val = @counters[name] = seed == seed.to_i.to_s ? seed.to_i : seed
600
572
  else
601
- @attributes[name] = @counters[name] = nextval(attr_seed ? attr_val : 0)
573
+ next_val = @counters[name] = 1
602
574
  end
575
+ @attributes[name] = next_val unless locked
576
+ next_val
603
577
  end
604
578
 
605
579
  # Public: Increment the specified counter and store it in the block's attributes
@@ -614,46 +588,56 @@ class Document < AbstractBlock
614
588
  # Deprecated: Map old counter_increment method to increment_counter for backwards compatibility
615
589
  alias counter_increment increment_and_store_counter
616
590
 
617
- # Internal: Get the next value in the sequence.
618
- #
619
- # Handles both integer and character sequences.
620
- #
621
- # current - the value to increment as a String or Integer
622
- #
623
- # returns the next value in the sequence according to the current value's type
624
- def nextval(current)
625
- if ::Integer === current
626
- current + 1
627
- else
628
- intval = current.to_i
629
- if intval.to_s != current.to_s
630
- (current[0].ord + 1).chr
631
- else
632
- intval + 1
633
- end
634
- end
635
- end
636
-
591
+ # Public: Register a reference in the document catalog
637
592
  def register type, value
638
593
  case type
639
594
  when :ids # deprecated
640
- id, reftext = value
641
- @catalog[:ids][id] ||= reftext || ('[' + id + ']')
595
+ register :refs, [(id = value[0]), (Inline.new self, :anchor, value[1], type: :ref, id: id)]
642
596
  when :refs
643
- id, ref, reftext = value
644
- unless (refs = @catalog[:refs]).key? id
645
- @catalog[:ids][id] = reftext || ('[' + id + ']')
646
- refs[id] = ref
647
- end
648
- when :footnotes, :indexterms
597
+ @catalog[:refs][value[0]] ||= (ref = value[1])
598
+ ref
599
+ when :footnotes
649
600
  @catalog[type] << value
650
601
  else
651
- if @options[:catalog_assets]
652
- @catalog[type] << (type == :images ? (ImageReference.new value[0], value[1]) : value)
602
+ @catalog[type] << (type == :images ? (ImageReference.new value, @attributes['imagesdir']) : value) if @options[:catalog_assets]
603
+ end
604
+ end
605
+
606
+ # Public: Scan registered references and return the ID of the first reference that matches the specified reference text.
607
+ #
608
+ # text - The String reference text to compare to the converted reference text of each registered reference.
609
+ #
610
+ # Returns the String ID of the first reference with matching reference text or nothing if no reference is found.
611
+ def resolve_id text
612
+ if @reftexts
613
+ @reftexts[text]
614
+ elsif @parsed
615
+ # @reftexts is set eagerly to prevent nested lazy init
616
+ (@reftexts = {}).tap {|accum| @catalog[:refs].each {|id, ref| accum[ref.xreftext] ||= id } }[text]
617
+ else
618
+ resolved_id = nil
619
+ # @reftexts is set eagerly to prevent nested lazy init
620
+ @reftexts = accum = {}
621
+ @catalog[:refs].each do |id, ref|
622
+ # NOTE short-circuit early since we're throwing away this table anyway
623
+ if (xreftext = ref.xreftext) == text
624
+ resolved_id = id
625
+ break
626
+ end
627
+ accum[xreftext] ||= id
653
628
  end
629
+ @reftexts = nil
630
+ resolved_id
654
631
  end
655
632
  end
656
633
 
634
+ # Public: Check whether this Document has any child Section objects.
635
+ #
636
+ # Returns A [Boolean] to indicate whether this Document has child Section objects
637
+ def sections?
638
+ @next_section_index > 0
639
+ end
640
+
657
641
  def footnotes?
658
642
  @catalog[:footnotes].empty? ? false : true
659
643
  end
@@ -706,7 +690,7 @@ class Document < AbstractBlock
706
690
  #
707
691
  # title - the String title to assign as the title of the document header
708
692
  #
709
- # Returns the new [String] title assigned to the document header
693
+ # Returns the specified [String] title
710
694
  def title= title
711
695
  unless (sect = @header)
712
696
  (sect = (@header = Section.new self, 0)).sectname = 'header'
@@ -743,7 +727,7 @@ class Document < AbstractBlock
743
727
  end
744
728
 
745
729
  if (separator = opts[:partition])
746
- Title.new val, opts.merge({ :separator => (separator == true ? @attributes['title-separator'] : separator) })
730
+ Title.new val, opts.merge({ separator: (separator == true ? @attributes['title-separator'] : separator) })
747
731
  elsif opts[:sanitize] && val.include?('<')
748
732
  val.gsub(XmlSanitizeRx, '').squeeze(' ').strip
749
733
  else
@@ -752,6 +736,10 @@ class Document < AbstractBlock
752
736
  end
753
737
  alias name doctitle
754
738
 
739
+ def xreftext xrefstyle = nil
740
+ (val = reftext) && !val.empty? ? val : title
741
+ end
742
+
755
743
  # Public: Convenience method to retrieve the document attribute 'author'
756
744
  #
757
745
  # returns the full name of the author as a String
@@ -788,7 +776,7 @@ class Document < AbstractBlock
788
776
  end
789
777
 
790
778
  def notitle
791
- !@attributes.key?('showtitle') && @attributes.key?('notitle')
779
+ @attributes.key? 'notitle'
792
780
  end
793
781
 
794
782
  def noheader
@@ -803,10 +791,10 @@ class Document < AbstractBlock
803
791
  @header || @blocks.find {|e| e.context == :section }
804
792
  end
805
793
 
806
- def has_header?
794
+ def header?
807
795
  @header ? true : false
808
796
  end
809
- alias header? has_header?
797
+ alias has_header? header?
810
798
 
811
799
  # Public: Append a content Block to this Document.
812
800
  #
@@ -820,8 +808,8 @@ class Document < AbstractBlock
820
808
  super
821
809
  end
822
810
 
823
- # Internal: called after the header has been parsed and before the content
824
- # will be parsed.
811
+ # Internal: Called by the parser after parsing the header and before parsing
812
+ # the body, even if no header is found.
825
813
  #--
826
814
  # QUESTION should we invoke the TreeProcessors here, passing in a phase?
827
815
  # QUESTION is finalize_header the right name?
@@ -832,96 +820,7 @@ class Document < AbstractBlock
832
820
  unrooted_attributes
833
821
  end
834
822
 
835
- # Internal: Branch the attributes so that the original state can be restored
836
- # at a future time.
837
- def save_attributes
838
- # enable toc and sectnums (i.e., numbered) by default in DocBook backend
839
- # NOTE the attributes_modified should go away once we have a proper attribute storage & tracking facility
840
- if (attrs = @attributes)['basebackend'] == 'docbook'
841
- attrs['toc'] = '' unless attribute_locked?('toc') || @attributes_modified.include?('toc')
842
- attrs['sectnums'] = '' unless attribute_locked?('sectnums') || @attributes_modified.include?('sectnums')
843
- end
844
-
845
- unless attrs.key?('doctitle') || !(val = doctitle)
846
- attrs['doctitle'] = val
847
- end
848
-
849
- # css-signature cannot be updated after header attributes are processed
850
- @id = attrs['css-signature'] unless @id
851
-
852
- toc_position_val = if (toc_val = (attrs.delete('toc2') ? 'left' : attrs['toc']))
853
- # toc-placement allows us to separate position from using fitted slot vs macro
854
- (toc_placement = attrs.fetch('toc-placement', 'macro')) && toc_placement != 'auto' ? toc_placement : attrs['toc-position']
855
- else
856
- nil
857
- end
858
-
859
- if toc_val && (!toc_val.empty? || !toc_position_val.nil_or_empty?)
860
- default_toc_position = 'left'
861
- # TODO rename toc2 to aside-toc
862
- default_toc_class = 'toc2'
863
- if !toc_position_val.nil_or_empty?
864
- position = toc_position_val
865
- elsif !toc_val.empty?
866
- position = toc_val
867
- else
868
- position = default_toc_position
869
- end
870
- attrs['toc'] = ''
871
- attrs['toc-placement'] = 'auto'
872
- case position
873
- when 'left', '<', '&lt;'
874
- attrs['toc-position'] = 'left'
875
- when 'right', '>', '&gt;'
876
- attrs['toc-position'] = 'right'
877
- when 'top', '^'
878
- attrs['toc-position'] = 'top'
879
- when 'bottom', 'v'
880
- attrs['toc-position'] = 'bottom'
881
- when 'preamble', 'macro'
882
- attrs['toc-position'] = 'content'
883
- attrs['toc-placement'] = position
884
- default_toc_class = nil
885
- else
886
- attrs.delete 'toc-position'
887
- default_toc_class = nil
888
- end
889
- attrs['toc-class'] ||= default_toc_class if default_toc_class
890
- end
891
-
892
- if (@compat_mode = attrs.key? 'compat-mode')
893
- attrs['source-language'] = attrs['language'] if attrs.key? 'language'
894
- end
895
-
896
- # NOTE pin the outfilesuffix after the header is parsed
897
- @outfilesuffix = attrs['outfilesuffix']
898
-
899
- @header_attributes = attrs.dup
900
-
901
- # unfreeze "flexible" attributes
902
- unless @parent_document
903
- FLEXIBLE_ATTRIBUTES.each do |name|
904
- # turning a flexible attribute off should be permanent
905
- # (we may need more config if that's not always the case)
906
- if @attribute_overrides.key?(name) && @attribute_overrides[name]
907
- @attribute_overrides.delete(name)
908
- end
909
- end
910
- end
911
- end
912
-
913
- # Internal: Restore the attributes to the previously saved state (attributes in header)
914
- def restore_attributes
915
- @catalog[:callouts].rewind unless @parent_document
916
- @attributes.replace @header_attributes
917
- end
918
-
919
- # Internal: Delete any attributes stored for playback
920
- def clear_playback_attributes(attributes)
921
- attributes.delete(:attribute_entries)
922
- end
923
-
924
- # Internal: Replay attribute assignments at the block level
823
+ # Public: Replay attribute assignments at the block level
925
824
  def playback_attributes(block_attributes)
926
825
  if block_attributes.key? :attribute_entries
927
826
  block_attributes[:attribute_entries].each do |entry|
@@ -937,6 +836,12 @@ class Document < AbstractBlock
937
836
  end
938
837
  end
939
838
 
839
+ # Public: Restore the attributes to the previously saved state (attributes in header)
840
+ def restore_attributes
841
+ @catalog[:callouts].rewind unless @parent_document
842
+ @attributes.replace @header_attributes
843
+ end
844
+
940
845
  # Public: Set the specified attribute on the document if the name is not locked
941
846
  #
942
847
  # If the attribute is locked, false is returned. Otherwise, the value is
@@ -945,28 +850,27 @@ class Document < AbstractBlock
945
850
  # 'doctype', then the value of backend-related attributes are updated.
946
851
  #
947
852
  # name - the String attribute name
948
- # value - the String attribute value; must not be nil (default: '')
853
+ # value - the String attribute value; must not be nil (optional, default: '')
949
854
  #
950
- # Returns the resolved value if the attribute was set or false if it was not because it's locked.
855
+ # Returns the substituted value if the attribute was set or nil if it was not because it's locked.
951
856
  def set_attribute name, value = ''
952
- if attribute_locked? name
953
- false
954
- else
955
- if @max_attribute_value_size
956
- resolved_value = (apply_attribute_value_subs value).limit_bytesize @max_attribute_value_size
957
- else
958
- resolved_value = apply_attribute_value_subs value
959
- end
960
- case name
961
- when 'backend'
962
- update_backend_attributes resolved_value, (@attributes_modified.delete? 'htmlsyntax')
963
- when 'doctype'
964
- update_doctype_attributes resolved_value
857
+ unless attribute_locked? name
858
+ value = apply_attribute_value_subs value unless value.empty?
859
+ # NOTE if @header_attributes is set, we're beyond the document header
860
+ if @header_attributes
861
+ @attributes[name] = value
965
862
  else
966
- @attributes[name] = resolved_value
863
+ case name
864
+ when 'backend'
865
+ update_backend_attributes value, (@attributes_modified.delete? 'htmlsyntax') && value == @backend
866
+ when 'doctype'
867
+ update_doctype_attributes value
868
+ else
869
+ @attributes[name] = value
870
+ end
871
+ @attributes_modified << name
967
872
  end
968
- @attributes_modified << name
969
- resolved_value
873
+ value
970
874
  end
971
875
  end
972
876
 
@@ -1017,151 +921,6 @@ class Document < AbstractBlock
1017
921
  end
1018
922
  end
1019
923
 
1020
- # Internal: Apply substitutions to the attribute value
1021
- #
1022
- # If the value is an inline passthrough macro (e.g., pass:<subs>[value]),
1023
- # apply the substitutions defined in <subs> to the value, or leave the value
1024
- # unmodified if no substitutions are specified. If the value is not an
1025
- # inline passthrough macro, apply header substitutions to the value.
1026
- #
1027
- # value - The String attribute value on which to perform substitutions
1028
- #
1029
- # Returns The String value with substitutions performed
1030
- def apply_attribute_value_subs value
1031
- if AttributeEntryPassMacroRx =~ value
1032
- $1 ? (apply_subs $2, (resolve_pass_subs $1)) : $2
1033
- else
1034
- apply_header_subs value
1035
- end
1036
- end
1037
-
1038
- # Public: Update the backend attributes to reflect a change in the active backend.
1039
- #
1040
- # This method also handles updating the related doctype attributes if the
1041
- # doctype attribute is assigned at the time this method is called.
1042
- #
1043
- # Returns the resolved String backend if updated, nothing otherwise.
1044
- def update_backend_attributes new_backend, force = nil
1045
- if force || (new_backend && new_backend != @backend)
1046
- current_backend, current_basebackend, current_doctype = @backend, (attrs = @attributes)['basebackend'], @doctype
1047
- if new_backend.start_with? 'xhtml'
1048
- attrs['htmlsyntax'] = 'xml'
1049
- new_backend = new_backend.slice 1, new_backend.length
1050
- elsif new_backend.start_with? 'html'
1051
- attrs['htmlsyntax'] = 'html' unless attrs['htmlsyntax'] == 'xml'
1052
- end
1053
- if (resolved_backend = BACKEND_ALIASES[new_backend])
1054
- new_backend = resolved_backend
1055
- end
1056
- if current_doctype
1057
- if current_backend
1058
- attrs.delete %(backend-#{current_backend})
1059
- attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype})
1060
- end
1061
- attrs[%(backend-#{new_backend}-doctype-#{current_doctype})] = ''
1062
- attrs[%(doctype-#{current_doctype})] = ''
1063
- elsif current_backend
1064
- attrs.delete %(backend-#{current_backend})
1065
- end
1066
- attrs[%(backend-#{new_backend})] = ''
1067
- @backend = attrs['backend'] = new_backend
1068
- # (re)initialize converter
1069
- if Converter::BackendInfo === (@converter = create_converter)
1070
- new_basebackend = @converter.basebackend
1071
- attrs['outfilesuffix'] = @converter.outfilesuffix unless attribute_locked? 'outfilesuffix'
1072
- new_filetype = @converter.filetype
1073
- elsif @converter
1074
- new_basebackend = new_backend.sub TrailingDigitsRx, ''
1075
- if (new_outfilesuffix = DEFAULT_EXTENSIONS[new_basebackend])
1076
- new_filetype = new_outfilesuffix.slice 1, new_outfilesuffix.length
1077
- else
1078
- new_outfilesuffix, new_basebackend, new_filetype = '.html', 'html', 'html'
1079
- end
1080
- attrs['outfilesuffix'] = new_outfilesuffix unless attribute_locked? 'outfilesuffix'
1081
- else
1082
- # NOTE ideally we shouldn't need the converter before the converter phase, but we do
1083
- raise ::NotImplementedError, %(asciidoctor: FAILED: missing converter for backend '#{new_backend}'. Processing aborted.)
1084
- end
1085
- if (current_filetype = attrs['filetype'])
1086
- attrs.delete %(filetype-#{current_filetype})
1087
- end
1088
- attrs['filetype'] = new_filetype
1089
- attrs[%(filetype-#{new_filetype})] = ''
1090
- if (page_width = DEFAULT_PAGE_WIDTHS[new_basebackend])
1091
- attrs['pagewidth'] = page_width
1092
- else
1093
- attrs.delete 'pagewidth'
1094
- end
1095
- if new_basebackend != current_basebackend
1096
- if current_doctype
1097
- if current_basebackend
1098
- attrs.delete %(basebackend-#{current_basebackend})
1099
- attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype})
1100
- end
1101
- attrs[%(basebackend-#{new_basebackend}-doctype-#{current_doctype})] = ''
1102
- elsif current_basebackend
1103
- attrs.delete %(basebackend-#{current_basebackend})
1104
- end
1105
- attrs[%(basebackend-#{new_basebackend})] = ''
1106
- attrs['basebackend'] = new_basebackend
1107
- end
1108
- return new_backend
1109
- end
1110
- end
1111
-
1112
- # TODO document me
1113
- #
1114
- # Returns the String doctype if updated, nothing otherwise.
1115
- def update_doctype_attributes new_doctype
1116
- if new_doctype && new_doctype != @doctype
1117
- current_backend, current_basebackend, current_doctype = @backend, (attrs = @attributes)['basebackend'], @doctype
1118
- if current_doctype
1119
- attrs.delete %(doctype-#{current_doctype})
1120
- if current_backend
1121
- attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype})
1122
- attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = ''
1123
- end
1124
- if current_basebackend
1125
- attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype})
1126
- attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = ''
1127
- end
1128
- else
1129
- attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = '' if current_backend
1130
- attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = '' if current_basebackend
1131
- end
1132
- attrs[%(doctype-#{new_doctype})] = ''
1133
- return @doctype = attrs['doctype'] = new_doctype
1134
- end
1135
- end
1136
-
1137
- # TODO document me
1138
- def create_converter
1139
- converter_opts = {}
1140
- converter_opts[:htmlsyntax] = @attributes['htmlsyntax']
1141
- if (template_dir = @options[:template_dir])
1142
- template_dirs = [template_dir]
1143
- elsif (template_dirs = @options[:template_dirs])
1144
- template_dirs = Array template_dirs
1145
- end
1146
- if template_dirs
1147
- converter_opts[:template_dirs] = template_dirs
1148
- converter_opts[:template_cache] = @options.fetch :template_cache, true
1149
- converter_opts[:template_engine] = @options[:template_engine]
1150
- converter_opts[:template_engine_options] = @options[:template_engine_options]
1151
- converter_opts[:eruby] = @options[:eruby]
1152
- converter_opts[:safe] = @safe
1153
- end
1154
- if (converter = @options[:converter])
1155
- converter_factory = Converter::Factory.new ::Hash[backend, converter]
1156
- else
1157
- converter_factory = Converter::Factory.default false
1158
- end
1159
- # QUESTION should we honor the convert_opts?
1160
- # QUESTION should we pass through all options and attributes too?
1161
- #converter_opts.update opts
1162
- converter_factory.create backend, converter_opts
1163
- end
1164
-
1165
924
  # Public: Convert the AsciiDoc document using the templates
1166
925
  # loaded by the Converter. If a :template_dir is not specified,
1167
926
  # or a template is missing, the converter will fall back to
@@ -1186,7 +945,13 @@ class Document < AbstractBlock
1186
945
  end
1187
946
  end
1188
947
  else
1189
- transform = ((opts.key? :header_footer) ? opts[:header_footer] : @options[:header_footer]) ? 'document' : 'embedded'
948
+ if opts.key? :standalone
949
+ transform = opts[:standalone] ? 'document' : 'embedded'
950
+ elsif opts.key? :header_footer
951
+ transform = opts[:header_footer] ? 'document' : 'embedded'
952
+ else
953
+ transform = @options[:standalone] ? 'document' : 'embedded'
954
+ end
1190
955
  output = @converter.convert self, transform
1191
956
  end
1192
957
 
@@ -1202,13 +967,19 @@ class Document < AbstractBlock
1202
967
  output
1203
968
  end
1204
969
 
1205
- # Alias render to convert to maintain backwards compatibility
970
+ # Deprecated: Use {Document#convert} instead.
1206
971
  alias render convert
1207
972
 
1208
973
  # Public: Write the output to the specified file
1209
974
  #
1210
- # If the converter responds to :write, delegate the work of writing the file
1211
- # to that method. Otherwise, write the output the specified file.
975
+ # If the converter responds to :write, delegate the work of writing the output
976
+ # to that method. Otherwise, write the output to the specified file. In the
977
+ # latter case, this method ensures the output has a trailing newline if the
978
+ # target responds to write and the output is not empty.
979
+ #
980
+ # output - The output to write. Unless the converter responds to write, this
981
+ # object is expected to be a String.
982
+ # target - The file to write, either a File object or a String path.
1212
983
  #
1213
984
  # Returns nothing
1214
985
  def write output, target
@@ -1223,38 +994,17 @@ class Document < AbstractBlock
1223
994
  # ensure there's a trailing endline
1224
995
  target.write LF
1225
996
  end
1226
- elsif COERCE_ENCODING
1227
- ::IO.write target, output, :encoding => ::Encoding::UTF_8
1228
997
  else
1229
- ::IO.write target, output
998
+ ::File.write target, output, mode: FILE_WRITE_MODE
1230
999
  end
1231
- if @backend == 'manpage' && ::String === target && (@converter.respond_to? :write_alternate_pages)
1232
- @converter.write_alternate_pages @attributes['mannames'], @attributes['manvolnum'], target
1000
+ if @backend == 'manpage' && ::String === target && (@converter.class.respond_to? :write_alternate_pages)
1001
+ @converter.class.write_alternate_pages @attributes['mannames'], @attributes['manvolnum'], target
1233
1002
  end
1234
1003
  end
1235
1004
  @timings.record :write if @timings
1236
1005
  nil
1237
1006
  end
1238
1007
 
1239
- =begin
1240
- def convert_to target, opts = {}
1241
- start = ::Time.now.to_f if (monitor = opts[:monitor])
1242
- output = (r = converter opts).convert
1243
- monitor[:convert] = ::Time.now.to_f - start if monitor
1244
-
1245
- unless target.respond_to? :write
1246
- @attributes['outfile'] = target = ::File.expand_path target
1247
- @attributes['outdir'] = ::File.dirname target
1248
- end
1249
-
1250
- start = ::Time.now.to_f if monitor
1251
- r.write output, target
1252
- monitor[:write] = ::Time.now.to_f - start if monitor
1253
-
1254
- output
1255
- end
1256
- =end
1257
-
1258
1008
  def content
1259
1009
  # NOTE per AsciiDoc-spec, remove the title before converting the body
1260
1010
  @attributes.delete('title')
@@ -1274,12 +1024,9 @@ class Document < AbstractBlock
1274
1024
  # returns The contents of the docinfo file(s) or empty string if no files are
1275
1025
  # found or the safe mode is secure or greater.
1276
1026
  def docinfo location = :head, suffix = nil
1277
- if safe >= SafeMode::SECURE
1278
- ''
1279
- else
1280
- content = []
1027
+ if safe < SafeMode::SECURE
1281
1028
  qualifier = %(-#{location}) unless location == :head
1282
- suffix = @outfilesuffix unless suffix
1029
+ suffix ||= @outfilesuffix
1283
1030
 
1284
1031
  if (docinfo = @attributes['docinfo']).nil_or_empty?
1285
1032
  if @attributes.key? 'docinfo2'
@@ -1294,33 +1041,88 @@ class Document < AbstractBlock
1294
1041
  end
1295
1042
 
1296
1043
  if docinfo
1044
+ content = []
1297
1045
  docinfo_file, docinfo_dir, docinfo_subs = %(docinfo#{qualifier}#{suffix}), @attributes['docinfodir'], resolve_docinfo_subs
1298
1046
  unless (docinfo & ['shared', %(shared-#{location})]).empty?
1299
1047
  docinfo_path = normalize_system_path docinfo_file, docinfo_dir
1300
1048
  # NOTE normalizing the lines is essential if we're performing substitutions
1301
- if (shd_content = (read_asset docinfo_path, :normalize => true))
1302
- content << (apply_subs shd_content, docinfo_subs)
1049
+ if (shared_docinfo = read_asset docinfo_path, normalize: true)
1050
+ content << (apply_subs shared_docinfo, docinfo_subs)
1303
1051
  end
1304
1052
  end
1305
1053
 
1306
1054
  unless @attributes['docname'].nil_or_empty? || (docinfo & ['private', %(private-#{location})]).empty?
1307
1055
  docinfo_path = normalize_system_path %(#{@attributes['docname']}-#{docinfo_file}), docinfo_dir
1308
1056
  # NOTE normalizing the lines is essential if we're performing substitutions
1309
- if (pvt_content = (read_asset docinfo_path, :normalize => true))
1310
- content << (apply_subs pvt_content, docinfo_subs)
1057
+ if (private_docinfo = read_asset docinfo_path, normalize: true)
1058
+ content << (apply_subs private_docinfo, docinfo_subs)
1311
1059
  end
1312
1060
  end
1313
1061
  end
1062
+ end
1314
1063
 
1315
- # TODO allow document to control whether extension docinfo is contributed
1316
- if @extensions && (docinfo_processors? location)
1317
- content += @docinfo_processor_extensions[location].map {|ext| ext.process_method[self] }.compact
1318
- end
1319
-
1064
+ # TODO allow document to control whether extension docinfo is contributed
1065
+ if @extensions && (docinfo_processors? location)
1066
+ ((content || []).concat @docinfo_processor_extensions[location].map {|ext| ext.process_method[self] }.compact).join LF
1067
+ elsif content
1320
1068
  content.join LF
1069
+ else
1070
+ ''
1071
+ end
1072
+ end
1073
+
1074
+ def docinfo_processors?(location = :head)
1075
+ if @docinfo_processor_extensions.key?(location)
1076
+ # false means we already performed a lookup and didn't find any
1077
+ @docinfo_processor_extensions[location] != false
1078
+ elsif @extensions && @document.extensions.docinfo_processors?(location)
1079
+ !!(@docinfo_processor_extensions[location] = @document.extensions.docinfo_processors(location))
1080
+ else
1081
+ @docinfo_processor_extensions[location] = false
1321
1082
  end
1322
1083
  end
1323
1084
 
1085
+ def to_s
1086
+ %(#<#{self.class}@#{object_id} {doctype: #{doctype.inspect}, doctitle: #{(@header && @header.title).inspect}, blocks: #{@blocks.size}}>)
1087
+ end
1088
+
1089
+ private
1090
+
1091
+ # Internal: Apply substitutions to the attribute value
1092
+ #
1093
+ # If the value is an inline passthrough macro (e.g., pass:<subs>[value]),
1094
+ # apply the substitutions defined in <subs> to the value, or leave the value
1095
+ # unmodified if no substitutions are specified. If the value is not an
1096
+ # inline passthrough macro, apply header substitutions to the value.
1097
+ #
1098
+ # value - The String attribute value on which to perform substitutions
1099
+ #
1100
+ # Returns The String value with substitutions performed
1101
+ def apply_attribute_value_subs value
1102
+ if AttributeEntryPassMacroRx =~ value
1103
+ value = $2
1104
+ value = apply_subs value, (resolve_pass_subs $1) if $1
1105
+ else
1106
+ value = apply_header_subs value
1107
+ end
1108
+ @max_attribute_value_size ? (limit_bytesize value, @max_attribute_value_size) : value
1109
+ end
1110
+
1111
+ # Internal: Safely truncates a string to the specified number of bytes.
1112
+ #
1113
+ # If a multibyte char gets split, the dangling fragment is dropped.
1114
+ #
1115
+ # str - The String the truncate.
1116
+ # max - The maximum allowable size of the String, in bytes.
1117
+ #
1118
+ # Returns the String truncated to the specified bytesize.
1119
+ def limit_bytesize str, max
1120
+ if str.bytesize > max
1121
+ max -= 1 until (str = str.byteslice 0, max).valid_encoding?
1122
+ end
1123
+ str
1124
+ end
1125
+
1324
1126
  # Internal: Resolve the list of comma-delimited subs to apply to docinfo files.
1325
1127
  #
1326
1128
  # Resolve the list of substitutions from the value of the docinfosubs
@@ -1332,20 +1134,260 @@ class Document < AbstractBlock
1332
1134
  (@attributes.key? 'docinfosubs') ? (resolve_subs @attributes['docinfosubs'], :block, nil, 'docinfo') : [:attributes]
1333
1135
  end
1334
1136
 
1335
- def docinfo_processors?(location = :head)
1336
- if @docinfo_processor_extensions.key?(location)
1337
- # false means we already performed a lookup and didn't find any
1338
- @docinfo_processor_extensions[location] != false
1339
- elsif @extensions && @document.extensions.docinfo_processors?(location)
1340
- !!(@docinfo_processor_extensions[location] = @document.extensions.docinfo_processors(location))
1137
+ # Internal: Create and initialize an instance of the converter for this document
1138
+ #--
1139
+ # QUESTION is there any additional information we should be passing to the converter?
1140
+ def create_converter backend, delegate_backend
1141
+ converter_opts = { document: self, htmlsyntax: @attributes['htmlsyntax'] }
1142
+ if (template_dirs = (opts = @options)[:template_dirs] || opts[:template_dir])
1143
+ converter_opts[:template_dirs] = [*template_dirs]
1144
+ converter_opts[:template_cache] = opts.fetch :template_cache, true
1145
+ converter_opts[:template_engine] = opts[:template_engine]
1146
+ converter_opts[:template_engine_options] = opts[:template_engine_options]
1147
+ converter_opts[:eruby] = opts[:eruby]
1148
+ converter_opts[:safe] = @safe
1149
+ converter_opts[:delegate_backend] = delegate_backend if delegate_backend
1150
+ end
1151
+ if (converter = opts[:converter])
1152
+ (Converter::CustomFactory.new backend => converter).create backend, converter_opts
1341
1153
  else
1342
- @docinfo_processor_extensions[location] = false
1154
+ (opts.fetch :converter_factory, Converter).create backend, converter_opts
1343
1155
  end
1344
1156
  end
1345
1157
 
1346
- def to_s
1347
- %(#<#{self.class}@#{object_id} {doctype: #{doctype.inspect}, doctitle: #{(@header != nil ? @header.title : nil).inspect}, blocks: #{@blocks.size}}>)
1158
+ # Internal: Delete any attributes stored for playback
1159
+ def clear_playback_attributes(attributes)
1160
+ attributes.delete(:attribute_entries)
1161
+ end
1162
+
1163
+ # Internal: Branch the attributes so that the original state can be restored
1164
+ # at a future time.
1165
+ #
1166
+ # Returns the duplicated attributes, which will later be restored
1167
+ def save_attributes
1168
+ unless ((attrs = @attributes).key? 'doctitle') || !(doctitle_val = doctitle)
1169
+ attrs['doctitle'] = doctitle_val
1170
+ end
1171
+
1172
+ # css-signature cannot be updated after header attributes are processed
1173
+ @id ||= attrs['css-signature']
1174
+
1175
+ if (toc_val = (attrs.delete 'toc2') ? 'left' : attrs['toc'])
1176
+ # toc-placement allows us to separate position from using fitted slot vs macro
1177
+ toc_position_val = (toc_placement_val = attrs.fetch 'toc-placement', 'macro') && toc_placement_val != 'auto' ? toc_placement_val : attrs['toc-position']
1178
+ unless toc_val.empty? && toc_position_val.nil_or_empty?
1179
+ default_toc_position = 'left'
1180
+ # TODO rename toc2 to aside-toc
1181
+ default_toc_class = 'toc2'
1182
+ position = toc_position_val.nil_or_empty? ? (toc_val.empty? ? default_toc_position : toc_val) : toc_position_val
1183
+ attrs['toc'] = ''
1184
+ attrs['toc-placement'] = 'auto'
1185
+ case position
1186
+ when 'left', '<', '&lt;'
1187
+ attrs['toc-position'] = 'left'
1188
+ when 'right', '>', '&gt;'
1189
+ attrs['toc-position'] = 'right'
1190
+ when 'top', '^'
1191
+ attrs['toc-position'] = 'top'
1192
+ when 'bottom', 'v'
1193
+ attrs['toc-position'] = 'bottom'
1194
+ when 'preamble', 'macro'
1195
+ attrs['toc-position'] = 'content'
1196
+ attrs['toc-placement'] = position
1197
+ default_toc_class = nil
1198
+ else
1199
+ attrs.delete 'toc-position'
1200
+ default_toc_class = nil
1201
+ end
1202
+ attrs['toc-class'] ||= default_toc_class if default_toc_class
1203
+ end
1204
+ end
1205
+
1206
+ if (icons_val = attrs['icons']) && !(attrs.key? 'icontype')
1207
+ case icons_val
1208
+ when '', 'font'
1209
+ else
1210
+ attrs['icons'] = ''
1211
+ attrs['icontype'] = icons_val unless icons_val == 'image'
1212
+ end
1213
+ end
1214
+
1215
+ if (@compat_mode = attrs.key? 'compat-mode') && (attrs.key? 'language')
1216
+ attrs['source-language'] = attrs['language']
1217
+ end
1218
+
1219
+ unless @parent_document
1220
+ if (basebackend = attrs['basebackend']) == 'html'
1221
+ # QUESTION should we allow source-highlighter to be disabled in AsciiDoc table cell?
1222
+ if (syntax_hl_name = attrs['source-highlighter']) && !attrs[%(#{syntax_hl_name}-unavailable)]
1223
+ if (syntax_hl_factory = @options[:syntax_highlighter_factory])
1224
+ @syntax_highlighter = syntax_hl_factory.create syntax_hl_name, @backend, document: self
1225
+ elsif (syntax_hls = @options[:syntax_highlighters])
1226
+ @syntax_highlighter = (SyntaxHighlighter::DefaultFactoryProxy.new syntax_hls).create syntax_hl_name, @backend, document: self
1227
+ else
1228
+ @syntax_highlighter = SyntaxHighlighter.create syntax_hl_name, @backend, document: self
1229
+ end
1230
+ end
1231
+ # enable toc and sectnums (i.e., numbered) by default in DocBook backend
1232
+ elsif basebackend == 'docbook'
1233
+ # NOTE the attributes_modified should go away once we have a proper attribute storage & tracking facility
1234
+ attrs['toc'] = '' unless (attribute_locked? 'toc') || (@attributes_modified.include? 'toc')
1235
+ attrs['sectnums'] = '' unless (attribute_locked? 'sectnums') || (@attributes_modified.include? 'sectnums')
1236
+ end
1237
+
1238
+ # NOTE pin the outfilesuffix after the header is parsed
1239
+ @outfilesuffix = attrs['outfilesuffix']
1240
+
1241
+ # unfreeze "flexible" attributes
1242
+ FLEXIBLE_ATTRIBUTES.each do |name|
1243
+ # turning a flexible attribute off should be permanent
1244
+ # (we may need more config if that's not always the case)
1245
+ if @attribute_overrides.key?(name) && @attribute_overrides[name]
1246
+ @attribute_overrides.delete(name)
1247
+ end
1248
+ end
1249
+ end
1250
+
1251
+ @header_attributes = attrs.merge
1348
1252
  end
1349
1253
 
1254
+ # Internal: Assign the local and document datetime attributes, which includes localdate, localyear, localtime,
1255
+ # localdatetime, docdate, docyear, doctime, and docdatetime. Honor the SOURCE_DATE_EPOCH environment variable, if set.
1256
+ def fill_datetime_attributes attrs, input_mtime
1257
+ # See https://reproducible-builds.org/specs/source-date-epoch/
1258
+ now = (::ENV.key? 'SOURCE_DATE_EPOCH') ? (source_date_epoch = (::Time.at Integer ::ENV['SOURCE_DATE_EPOCH']).utc) : ::Time.now
1259
+ if (localdate = attrs['localdate'])
1260
+ attrs['localyear'] ||= (localdate.index '-') == 4 ? (localdate.slice 0, 4) : nil
1261
+ else
1262
+ localdate = attrs['localdate'] = now.strftime '%F'
1263
+ attrs['localyear'] ||= now.year.to_s
1264
+ end
1265
+ # %Z is OS dependent and may contain characters that aren't UTF-8 encoded (see asciidoctor#2770 and asciidoctor.js#23)
1266
+ localtime = (attrs['localtime'] ||= now.strftime %(%T #{now.utc_offset == 0 ? 'UTC' : '%z'}))
1267
+ attrs['localdatetime'] ||= %(#{localdate} #{localtime})
1268
+ # docdate, doctime and docdatetime should default to localdate, localtime and localdatetime if not otherwise set
1269
+ input_mtime = source_date_epoch || input_mtime || now
1270
+ if (docdate = attrs['docdate'])
1271
+ attrs['docyear'] ||= ((docdate.index '-') == 4 ? (docdate.slice 0, 4) : nil)
1272
+ else
1273
+ docdate = attrs['docdate'] = input_mtime.strftime '%F'
1274
+ attrs['docyear'] ||= input_mtime.year.to_s
1275
+ end
1276
+ # %Z is OS dependent and may contain characters that aren't UTF-8 encoded (see asciidoctor#2770 and asciidoctor.js#23)
1277
+ doctime = (attrs['doctime'] ||= input_mtime.strftime %(%T #{input_mtime.utc_offset == 0 ? 'UTC' : '%z'}))
1278
+ attrs['docdatetime'] ||= %(#{docdate} #{doctime})
1279
+ nil
1280
+ end
1281
+
1282
+ # Internal: Update the backend attributes to reflect a change in the active backend.
1283
+ #
1284
+ # This method also handles updating the related doctype attributes if the
1285
+ # doctype attribute is assigned at the time this method is called.
1286
+ #
1287
+ # Returns the resolved String backend if updated, nothing otherwise.
1288
+ def update_backend_attributes new_backend, init = nil
1289
+ if init || new_backend != @backend
1290
+ current_backend = @backend
1291
+ current_basebackend = (attrs = @attributes)['basebackend']
1292
+ current_doctype = @doctype
1293
+ actual_backend, _, new_backend = new_backend.partition ':' if new_backend.include? ':'
1294
+ if new_backend.start_with? 'xhtml'
1295
+ attrs['htmlsyntax'] = 'xml'
1296
+ new_backend = new_backend.slice 1, new_backend.length
1297
+ elsif new_backend.start_with? 'html'
1298
+ attrs['htmlsyntax'] ||= 'html'
1299
+ end
1300
+ new_backend = BACKEND_ALIASES[new_backend] || new_backend
1301
+ new_backend, delegate_backend = actual_backend, new_backend if actual_backend
1302
+ if current_doctype
1303
+ if current_backend
1304
+ attrs.delete %(backend-#{current_backend})
1305
+ attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype})
1306
+ end
1307
+ attrs[%(backend-#{new_backend}-doctype-#{current_doctype})] = ''
1308
+ attrs[%(doctype-#{current_doctype})] = ''
1309
+ elsif current_backend
1310
+ attrs.delete %(backend-#{current_backend})
1311
+ end
1312
+ attrs[%(backend-#{new_backend})] = ''
1313
+ # QUESTION should we defer the @backend assignment until after the converter is created?
1314
+ @backend = attrs['backend'] = new_backend
1315
+ # (re)initialize converter
1316
+ if Converter::BackendTraits === (converter = create_converter new_backend, delegate_backend)
1317
+ new_basebackend = converter.basebackend
1318
+ new_filetype = converter.filetype
1319
+ if (htmlsyntax = converter.htmlsyntax)
1320
+ attrs['htmlsyntax'] = htmlsyntax
1321
+ end
1322
+ if init
1323
+ attrs['outfilesuffix'] ||= converter.outfilesuffix
1324
+ else
1325
+ attrs['outfilesuffix'] = converter.outfilesuffix unless attribute_locked? 'outfilesuffix'
1326
+ end
1327
+ elsif converter
1328
+ backend_traits = Converter.derive_backend_traits new_backend
1329
+ new_basebackend = backend_traits[:basebackend]
1330
+ new_filetype = backend_traits[:filetype]
1331
+ if init
1332
+ attrs['outfilesuffix'] ||= backend_traits[:outfilesuffix]
1333
+ else
1334
+ attrs['outfilesuffix'] = backend_traits[:outfilesuffix] unless attribute_locked? 'outfilesuffix'
1335
+ end
1336
+ else
1337
+ # NOTE ideally we shouldn't need the converter before the converter phase, but we do
1338
+ raise ::NotImplementedError, %(asciidoctor: FAILED: missing converter for backend '#{new_backend}'. Processing aborted.)
1339
+ end
1340
+ @converter = converter
1341
+ if (current_filetype = attrs['filetype'])
1342
+ attrs.delete %(filetype-#{current_filetype})
1343
+ end
1344
+ attrs['filetype'] = new_filetype
1345
+ attrs[%(filetype-#{new_filetype})] = ''
1346
+ if (page_width = DEFAULT_PAGE_WIDTHS[new_basebackend])
1347
+ attrs['pagewidth'] = page_width
1348
+ else
1349
+ attrs.delete 'pagewidth'
1350
+ end
1351
+ if new_basebackend != current_basebackend
1352
+ if current_doctype
1353
+ if current_basebackend
1354
+ attrs.delete %(basebackend-#{current_basebackend})
1355
+ attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype})
1356
+ end
1357
+ attrs[%(basebackend-#{new_basebackend}-doctype-#{current_doctype})] = ''
1358
+ elsif current_basebackend
1359
+ attrs.delete %(basebackend-#{current_basebackend})
1360
+ end
1361
+ attrs[%(basebackend-#{new_basebackend})] = ''
1362
+ attrs['basebackend'] = new_basebackend
1363
+ end
1364
+ new_backend
1365
+ end
1366
+ end
1367
+
1368
+ # Internal: Update the doctype and backend attributes to reflect a change in the active doctype.
1369
+ #
1370
+ # Returns the String doctype if updated, nothing otherwise.
1371
+ def update_doctype_attributes new_doctype
1372
+ if new_doctype && new_doctype != @doctype
1373
+ current_backend, current_basebackend, current_doctype = @backend, (attrs = @attributes)['basebackend'], @doctype
1374
+ if current_doctype
1375
+ attrs.delete %(doctype-#{current_doctype})
1376
+ if current_backend
1377
+ attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype})
1378
+ attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = ''
1379
+ end
1380
+ if current_basebackend
1381
+ attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype})
1382
+ attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = ''
1383
+ end
1384
+ else
1385
+ attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = '' if current_backend
1386
+ attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = '' if current_basebackend
1387
+ end
1388
+ attrs[%(doctype-#{new_doctype})] = ''
1389
+ @doctype = attrs['doctype'] = new_doctype
1390
+ end
1391
+ end
1350
1392
  end
1351
1393
  end