asciidoctor 1.5.8 → 2.0.0.rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +162 -17
  3. data/LICENSE +1 -1
  4. data/README-de.adoc +12 -13
  5. data/README-fr.adoc +11 -12
  6. data/README-jp.adoc +11 -12
  7. data/README-zh_CN.adoc +12 -13
  8. data/README.adoc +6 -7
  9. data/asciidoctor.gemspec +19 -24
  10. data/bin/asciidoctor +5 -4
  11. data/data/reference/syntax.adoc +283 -0
  12. data/data/stylesheets/asciidoctor-default.css +56 -52
  13. data/data/stylesheets/coderay-asciidoctor.css +7 -9
  14. data/lib/asciidoctor.rb +171 -232
  15. data/lib/asciidoctor/abstract_block.rb +96 -105
  16. data/lib/asciidoctor/abstract_node.rb +118 -139
  17. data/lib/asciidoctor/attribute_list.rb +10 -14
  18. data/lib/asciidoctor/block.rb +20 -19
  19. data/lib/asciidoctor/callouts.rb +4 -2
  20. data/lib/asciidoctor/cli.rb +3 -2
  21. data/lib/asciidoctor/cli/invoker.rb +14 -21
  22. data/lib/asciidoctor/cli/options.rb +64 -54
  23. data/lib/asciidoctor/converter.rb +357 -185
  24. data/lib/asciidoctor/converter/composite.rb +40 -48
  25. data/lib/asciidoctor/converter/docbook5.rb +604 -640
  26. data/lib/asciidoctor/converter/html5.rb +949 -963
  27. data/lib/asciidoctor/converter/manpage.rb +569 -548
  28. data/lib/asciidoctor/converter/template.rb +231 -272
  29. data/lib/asciidoctor/core_ext.rb +5 -18
  30. data/lib/asciidoctor/core_ext/float/truncate.rb +19 -0
  31. data/lib/asciidoctor/core_ext/match_data/names.rb +7 -0
  32. data/lib/asciidoctor/core_ext/nil_or_empty.rb +1 -0
  33. data/lib/asciidoctor/core_ext/regexp/is_match.rb +4 -2
  34. data/lib/asciidoctor/document.rb +399 -377
  35. data/lib/asciidoctor/extensions.rb +72 -140
  36. data/lib/asciidoctor/helpers.rb +122 -83
  37. data/lib/asciidoctor/inline.rb +5 -1
  38. data/lib/asciidoctor/list.rb +13 -11
  39. data/lib/asciidoctor/logging.rb +17 -16
  40. data/lib/asciidoctor/parser.rb +390 -423
  41. data/lib/asciidoctor/path_resolver.rb +10 -5
  42. data/lib/asciidoctor/reader.rb +286 -263
  43. data/lib/asciidoctor/rouge_ext.rb +39 -0
  44. data/lib/asciidoctor/section.rb +9 -8
  45. data/lib/asciidoctor/stylesheets.rb +19 -37
  46. data/lib/asciidoctor/substitutors.rb +364 -509
  47. data/lib/asciidoctor/syntax_highlighter.rb +238 -0
  48. data/lib/asciidoctor/syntax_highlighter/coderay.rb +87 -0
  49. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +26 -0
  50. data/lib/asciidoctor/syntax_highlighter/html_pipeline.rb +10 -0
  51. data/lib/asciidoctor/syntax_highlighter/prettify.rb +27 -0
  52. data/lib/asciidoctor/syntax_highlighter/pygments.rb +149 -0
  53. data/lib/asciidoctor/syntax_highlighter/rouge.rb +129 -0
  54. data/lib/asciidoctor/table.rb +73 -66
  55. data/lib/asciidoctor/timings.rb +4 -2
  56. data/lib/asciidoctor/version.rb +2 -1
  57. data/lib/asciidoctor/writer.rb +30 -0
  58. data/man/asciidoctor.1 +19 -15
  59. data/man/asciidoctor.adoc +14 -12
  60. metadata +69 -216
  61. data/CONTRIBUTING.adoc +0 -185
  62. data/Gemfile +0 -60
  63. data/Rakefile +0 -129
  64. data/bin/asciidoctor-safe +0 -15
  65. data/features/open_block.feature +0 -92
  66. data/features/pass_block.feature +0 -66
  67. data/features/step_definitions.rb +0 -49
  68. data/features/text_formatting.feature +0 -57
  69. data/features/xref.feature +0 -1039
  70. data/lib/asciidoctor/converter/base.rb +0 -59
  71. data/lib/asciidoctor/converter/docbook45.rb +0 -93
  72. data/lib/asciidoctor/converter/factory.rb +0 -226
  73. data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +0 -6
  74. data/lib/asciidoctor/core_ext/1.8.7/concurrent/hash.rb +0 -5
  75. data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +0 -4
  76. data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +0 -6
  77. data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +0 -5
  78. data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +0 -6
  79. data/lib/asciidoctor/core_ext/1.8.7/string/limit_bytesize.rb +0 -29
  80. data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +0 -6
  81. data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +0 -6
  82. data/lib/asciidoctor/core_ext/string/limit_bytesize.rb +0 -10
  83. data/test/api_test.rb +0 -1240
  84. data/test/attribute_list_test.rb +0 -242
  85. data/test/attributes_test.rb +0 -1623
  86. data/test/blocks_test.rb +0 -3870
  87. data/test/converter_test.rb +0 -470
  88. data/test/document_test.rb +0 -1853
  89. data/test/extensions_test.rb +0 -1560
  90. data/test/fixtures/asciidoc_index.txt +0 -521
  91. data/test/fixtures/basic-docinfo-footer.html +0 -6
  92. data/test/fixtures/basic-docinfo-footer.xml +0 -8
  93. data/test/fixtures/basic-docinfo.html +0 -1
  94. data/test/fixtures/basic-docinfo.xml +0 -4
  95. data/test/fixtures/basic.asciidoc +0 -5
  96. data/test/fixtures/chapter-a.adoc +0 -3
  97. data/test/fixtures/child-include.adoc +0 -5
  98. data/test/fixtures/circle.svg +0 -9
  99. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +0 -6
  100. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +0 -6
  101. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +0 -1
  102. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +0 -3
  103. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +0 -5
  104. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +0 -6
  105. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +0 -3
  106. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +0 -5
  107. data/test/fixtures/custom-docinfodir/basic-docinfo.html +0 -1
  108. data/test/fixtures/custom-docinfodir/docinfo.html +0 -1
  109. data/test/fixtures/docinfo-footer.html +0 -1
  110. data/test/fixtures/docinfo-footer.xml +0 -9
  111. data/test/fixtures/docinfo.html +0 -1
  112. data/test/fixtures/docinfo.xml +0 -3
  113. data/test/fixtures/doctime-localtime.adoc +0 -2
  114. data/test/fixtures/dot.gif +0 -0
  115. data/test/fixtures/encoding.asciidoc +0 -13
  116. data/test/fixtures/file-with-missing-include.adoc +0 -1
  117. data/test/fixtures/grandchild-include.adoc +0 -3
  118. data/test/fixtures/hello-asciidoctor.pdf +0 -69
  119. data/test/fixtures/include-file.asciidoc +0 -24
  120. data/test/fixtures/include-file.jsx +0 -8
  121. data/test/fixtures/include-file.ml +0 -3
  122. data/test/fixtures/include-file.xml +0 -5
  123. data/test/fixtures/lists.adoc +0 -96
  124. data/test/fixtures/master.adoc +0 -5
  125. data/test/fixtures/mismatched-end-tag.adoc +0 -7
  126. data/test/fixtures/other-chapters.adoc +0 -11
  127. data/test/fixtures/outer-include.adoc +0 -5
  128. data/test/fixtures/parent-include-restricted.adoc +0 -5
  129. data/test/fixtures/parent-include.adoc +0 -5
  130. data/test/fixtures/sample.asciidoc +0 -30
  131. data/test/fixtures/section-a.adoc +0 -4
  132. data/test/fixtures/stylesheets/custom.css +0 -3
  133. data/test/fixtures/subdir/index.adoc +0 -3
  134. data/test/fixtures/subdir/inner-include.adoc +0 -3
  135. data/test/fixtures/subdir/middle-include.adoc +0 -5
  136. data/test/fixtures/subs-docinfo.html +0 -2
  137. data/test/fixtures/subs.adoc +0 -6
  138. data/test/fixtures/tagged-class-enclosed.rb +0 -25
  139. data/test/fixtures/tagged-class.rb +0 -23
  140. data/test/fixtures/tip.gif +0 -0
  141. data/test/fixtures/unclosed-tag.adoc +0 -3
  142. data/test/fixtures/unexpected-end-tag.adoc +0 -4
  143. data/test/invoker_test.rb +0 -745
  144. data/test/links_test.rb +0 -855
  145. data/test/lists_test.rb +0 -5151
  146. data/test/logger_test.rb +0 -211
  147. data/test/manpage_test.rb +0 -660
  148. data/test/options_test.rb +0 -262
  149. data/test/paragraphs_test.rb +0 -562
  150. data/test/parser_test.rb +0 -742
  151. data/test/paths_test.rb +0 -395
  152. data/test/preamble_test.rb +0 -173
  153. data/test/reader_test.rb +0 -2161
  154. data/test/sections_test.rb +0 -3575
  155. data/test/substitutions_test.rb +0 -2066
  156. data/test/tables_test.rb +0 -2036
  157. data/test/test_helper.rb +0 -447
  158. data/test/text_test.rb +0 -309
@@ -1,18 +1,5 @@
1
- require 'asciidoctor/core_ext/nil_or_empty'
2
- require 'asciidoctor/core_ext/regexp/is_match'
3
- if RUBY_MIN_VERSION_1_9
4
- require 'asciidoctor/core_ext/string/limit_bytesize'
5
- if RUBY_ENGINE == 'opal'
6
- require 'asciidoctor/core_ext/1.8.7/io/binread'
7
- require 'asciidoctor/core_ext/1.8.7/io/write'
8
- end
9
- elsif RUBY_ENGINE != 'opal'
10
- require 'asciidoctor/core_ext/1.8.7/base64/strict_encode64'
11
- require 'asciidoctor/core_ext/1.8.7/hash/key'
12
- require 'asciidoctor/core_ext/1.8.7/io/binread'
13
- require 'asciidoctor/core_ext/1.8.7/io/write'
14
- require 'asciidoctor/core_ext/1.8.7/string/chr'
15
- require 'asciidoctor/core_ext/1.8.7/string/limit_bytesize'
16
- require 'asciidoctor/core_ext/1.8.7/symbol/empty'
17
- require 'asciidoctor/core_ext/1.8.7/symbol/length'
18
- end
1
+ # frozen_string_literal: true
2
+ require_relative 'core_ext/float/truncate'
3
+ require_relative 'core_ext/match_data/names' if RUBY_ENGINE == 'opal'
4
+ require_relative 'core_ext/nil_or_empty'
5
+ require_relative 'core_ext/regexp/is_match'
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ # NOTE remove once minimum required Ruby version is at least 2.4
3
+ Float.prepend(Module.new do
4
+ def truncate *args
5
+ if args.length == 1
6
+ if (precision = Integer args.shift) == 0
7
+ super
8
+ elsif precision > 0
9
+ precision_factor = 10.0 ** precision
10
+ (self * precision_factor).to_i / precision_factor
11
+ else
12
+ precision_factor = 10 ** precision.abs
13
+ (self / precision_factor).to_i * precision_factor
14
+ end
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end) if (Float.instance_method :truncate).arity == 0
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # NOTE remove once implemented in Opal
3
+ class MatchData
4
+ def names
5
+ []
6
+ end
7
+ end unless MatchData.method_defined? :names
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # A core library extension that defines the method nil_or_empty? as an alias to
2
3
  # optimize checks for nil? or empty? on common object types such as NilClass,
3
4
  # String, Array, Hash, and Numeric.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # NOTE remove once minimum required Ruby version is at least 2.4
1
3
  class Regexp
2
- alias match? === unless method_defined? :match?
3
- end
4
+ alias match? ===
5
+ end unless Regexp.method_defined? :match?
@@ -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
  #
@@ -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
@@ -204,7 +204,7 @@ class Document < AbstractBlock
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
 
@@ -254,10 +257,7 @@ 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.dup.tap {|catalog| catalog[:footnotes] = [] }
261
261
  # QUESTION should we support setting attribute in parent document from nested document?
262
262
  # NOTE we must dup or else all the assignments to the overrides clobbers the real attributes
263
263
  @attribute_overrides = attr_overrides = parent_doc.attributes.dup
@@ -268,23 +268,25 @@ class Document < AbstractBlock
268
268
  attr_overrides.delete 'toc-position'
269
269
  @safe = parent_doc.safe
270
270
  @attributes['compat-mode'] = '' if (@compat_mode = parent_doc.compat_mode)
271
+ @outfilesuffix = parent_doc.outfilesuffix
271
272
  @sourcemap = parent_doc.sourcemap
272
273
  @timings = nil
273
274
  @path_resolver = parent_doc.path_resolver
274
275
  @converter = parent_doc.converter
275
- initialize_extensions = false
276
+ initialize_extensions = nil
276
277
  @extensions = parent_doc.extensions
278
+ @syntax_highlighter = parent_doc.syntax_highlighter
277
279
  else
278
280
  @parent_document = nil
279
281
  @catalog = {
280
- :ids => {},
281
- :refs => {},
282
- :footnotes => [],
283
- :links => [],
284
- :images => [],
285
- :indexterms => [],
286
- :callouts => Callouts.new,
287
- :includes => {},
282
+ ids: {}, # deprecated; kept for backwards compatibility with converters
283
+ refs: {},
284
+ footnotes: [],
285
+ links: [],
286
+ images: [],
287
+ indexterms: [],
288
+ callouts: Callouts.new,
289
+ includes: {},
288
290
  }
289
291
  # copy attributes map and normalize keys
290
292
  # attribute overrides are attributes that can only be set from the commandline
@@ -317,20 +319,15 @@ class Document < AbstractBlock
317
319
  # be permissive in case API user wants to define new levels
318
320
  @safe = safe_mode
319
321
  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
322
+ @safe = (SafeMode.value_for_name safe_mode) rescue SafeMode::SECURE
326
323
  end
324
+ input_mtime = options.delete :input_mtime
327
325
  @compat_mode = attr_overrides.key? 'compat-mode'
328
326
  @sourcemap = options[:sourcemap]
329
327
  @timings = options.delete :timings
330
328
  @path_resolver = PathResolver.new
331
- @converter = nil
332
- initialize_extensions = defined? ::Asciidoctor::Extensions
333
- @extensions = nil # initialize furthur down
329
+ initialize_extensions = (defined? ::Asciidoctor::Extensions) ? true : nil
330
+ @extensions = nil # initialize furthur down if initialize_extensions is true
334
331
  end
335
332
 
336
333
  @parsed = false
@@ -383,7 +380,7 @@ class Document < AbstractBlock
383
380
  attrs['last-update-label'] = 'Last updated'
384
381
 
385
382
  attr_overrides['asciidoctor'] = ''
386
- attr_overrides['asciidoctor-version'] = VERSION
383
+ attr_overrides['asciidoctor-version'] = ::Asciidoctor::VERSION
387
384
 
388
385
  attr_overrides['safe-mode-name'] = (safe_mode_name = SafeMode.name_for_value @safe)
389
386
  attr_overrides["safe-mode-#{safe_mode_name}"] = ''
@@ -397,8 +394,9 @@ class Document < AbstractBlock
397
394
 
398
395
  attr_overrides['user-home'] = USER_HOME
399
396
 
400
- # legacy support for numbered attribute
397
+ # remap legacy attribute names
401
398
  attr_overrides['sectnums'] = attr_overrides.delete 'numbered' if attr_overrides.key? 'numbered'
399
+ attr_overrides['hardbreaks-option'] = attr_overrides.delete 'hardbreaks' if attr_overrides.key? 'hardbreaks'
402
400
 
403
401
  # If the base_dir option is specified, it overrides docdir and is used as the root for relative
404
402
  # paths. Otherwise, the base_dir is the directory of the source file (docdir), if set, otherwise
@@ -482,47 +480,28 @@ class Document < AbstractBlock
482
480
  else
483
481
  # setup default backend and doctype
484
482
  @backend = nil
485
- if (attrs['backend'] ||= DEFAULT_BACKEND) == 'manpage'
483
+ if (initial_backend = attrs['backend'] || DEFAULT_BACKEND) == 'manpage'
486
484
  @doctype = attrs['doctype'] = attr_overrides['doctype'] = 'manpage'
487
485
  else
488
486
  @doctype = (attrs['doctype'] ||= DEFAULT_DOCTYPE)
489
487
  end
490
- update_backend_attributes attrs['backend'], true
491
-
492
- #attrs['indir'] = attrs['docdir']
493
- #attrs['infile'] = attrs['docfile']
488
+ update_backend_attributes initial_backend, true
494
489
 
495
490
  # dynamic intrinstic attribute values
496
491
 
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})
492
+ #attrs['indir'] = attrs['docdir']
493
+ #attrs['infile'] = attrs['docfile']
517
494
 
518
495
  # fallback directories
519
496
  attrs['stylesdir'] ||= '.'
520
497
  attrs['iconsdir'] ||= %(#{attrs.fetch 'imagesdir', './images'}/icons)
521
498
 
499
+ fill_datetime_attributes attrs, input_mtime
500
+
522
501
  if initialize_extensions
523
502
  if (ext_registry = options[:extension_registry])
524
503
  # QUESTION should we warn if the value type of this option is not a registry
525
- if Extensions::Registry === ext_registry || (::RUBY_ENGINE_JRUBY &&
504
+ if Extensions::Registry === ext_registry || ((defined? ::AsciidoctorJ::Extensions::ExtensionRegistry) &&
526
505
  ::AsciidoctorJ::Extensions::ExtensionRegistry === ext_registry)
527
506
  @extensions = ext_registry.activate self
528
507
  end
@@ -533,7 +512,7 @@ class Document < AbstractBlock
533
512
  end
534
513
  end
535
514
 
536
- @reader = PreprocessorReader.new self, data, (Reader::Cursor.new attrs['docfile'], @base_dir), :normalize => true
515
+ @reader = PreprocessorReader.new self, data, (Reader::Cursor.new attrs['docfile'], @base_dir), normalize: true
537
516
  @source_location = @reader.cursor if @sourcemap
538
517
  end
539
518
  end
@@ -556,7 +535,7 @@ class Document < AbstractBlock
556
535
  doc = self
557
536
  # create reader if data is provided (used when data is not known at the time the Document object is created)
558
537
  if data
559
- @reader = PreprocessorReader.new doc, data, (Reader::Cursor.new @attributes['docfile'], @base_dir), :normalize => true
538
+ @reader = PreprocessorReader.new doc, data, (Reader::Cursor.new @attributes['docfile'], @base_dir), normalize: true
560
539
  @source_location = @reader.cursor if @sourcemap
561
540
  end
562
541
 
@@ -567,7 +546,7 @@ class Document < AbstractBlock
567
546
  end
568
547
 
569
548
  # Now parse the lines in the reader into blocks
570
- Parser.parse @reader, doc, :header_only => @options[:parse_header_only]
549
+ Parser.parse @reader, doc, header_only: @options[:parse_header_only]
571
550
 
572
551
  # should we call sort of post-parse function?
573
552
  restore_attributes
@@ -585,6 +564,11 @@ class Document < AbstractBlock
585
564
  end
586
565
  end
587
566
 
567
+ # Public: Returns whether the source lines of the document have been parsed.
568
+ def parsed?
569
+ @parsed
570
+ end
571
+
588
572
  # Public: Get the named counter and take the next number in the sequence.
589
573
  #
590
574
  # name - the String name of the counter
@@ -594,11 +578,11 @@ class Document < AbstractBlock
594
578
  def counter name, seed = nil
595
579
  return @parent_document.counter name, seed if @parent_document
596
580
  if (attr_seed = !(attr_val = @attributes[name]).nil_or_empty?) && (@counters.key? name)
597
- @attributes[name] = @counters[name] = (nextval attr_val)
581
+ @attributes[name] = @counters[name] = Helpers.nextval attr_val
598
582
  elsif seed
599
- @attributes[name] = @counters[name] = (seed == seed.to_i.to_s ? seed.to_i : seed)
583
+ @attributes[name] = @counters[name] = seed == seed.to_i.to_s ? seed.to_i : seed
600
584
  else
601
- @attributes[name] = @counters[name] = nextval(attr_seed ? attr_val : 0)
585
+ @attributes[name] = @counters[name] = Helpers.nextval attr_seed ? attr_val : 0
602
586
  end
603
587
  end
604
588
 
@@ -614,46 +598,26 @@ class Document < AbstractBlock
614
598
  # Deprecated: Map old counter_increment method to increment_counter for backwards compatibility
615
599
  alias counter_increment increment_and_store_counter
616
600
 
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
-
601
+ # Public: Register a reference in the document catalog
637
602
  def register type, value
638
603
  case type
639
604
  when :ids # deprecated
640
- id, reftext = value
641
- @catalog[:ids][id] ||= reftext || ('[' + id + ']')
605
+ register :refs, [(id = value[0]), (Inline.new self, :anchor, value[1], type: :ref, id: id)]
642
606
  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
607
+ @catalog[:refs][value[0]] ||= (ref = value[1])
608
+ ref
648
609
  when :footnotes, :indexterms
649
610
  @catalog[type] << value
650
611
  else
651
- if @options[:catalog_assets]
652
- @catalog[type] << (type == :images ? (ImageReference.new value[0], value[1]) : value)
653
- end
612
+ @catalog[type] << (type == :images ? (ImageReference.new value[0], value[1]) : value) if @options[:catalog_assets]
654
613
  end
655
614
  end
656
615
 
616
+ def resolve_id text
617
+ ((@reftexts ||= @parsed ? {}.tap {|accum| @catalog[:refs].each {|id, ref| accum[ref.xreftext] = id } } : nil) ||
618
+ {}.tap {|accum| @catalog[:refs].find {|id, ref| ref.xreftext == text ? accum[text] = id : nil } })[text]
619
+ end
620
+
657
621
  def footnotes?
658
622
  @catalog[:footnotes].empty? ? false : true
659
623
  end
@@ -743,7 +707,7 @@ class Document < AbstractBlock
743
707
  end
744
708
 
745
709
  if (separator = opts[:partition])
746
- Title.new val, opts.merge({ :separator => (separator == true ? @attributes['title-separator'] : separator) })
710
+ Title.new val, opts.merge({ separator: (separator == true ? @attributes['title-separator'] : separator) })
747
711
  elsif opts[:sanitize] && val.include?('<')
748
712
  val.gsub(XmlSanitizeRx, '').squeeze(' ').strip
749
713
  else
@@ -803,10 +767,10 @@ class Document < AbstractBlock
803
767
  @header || @blocks.find {|e| e.context == :section }
804
768
  end
805
769
 
806
- def has_header?
770
+ def header?
807
771
  @header ? true : false
808
772
  end
809
- alias header? has_header?
773
+ alias has_header? header?
810
774
 
811
775
  # Public: Append a content Block to this Document.
812
776
  #
@@ -820,8 +784,8 @@ class Document < AbstractBlock
820
784
  super
821
785
  end
822
786
 
823
- # Internal: called after the header has been parsed and before the content
824
- # will be parsed.
787
+ # Internal: Called by the parser after parsing the header and before parsing
788
+ # the body, even if no header is found.
825
789
  #--
826
790
  # QUESTION should we invoke the TreeProcessors here, passing in a phase?
827
791
  # QUESTION is finalize_header the right name?
@@ -832,96 +796,7 @@ class Document < AbstractBlock
832
796
  unrooted_attributes
833
797
  end
834
798
 
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
799
+ # Public: Replay attribute assignments at the block level
925
800
  def playback_attributes(block_attributes)
926
801
  if block_attributes.key? :attribute_entries
927
802
  block_attributes[:attribute_entries].each do |entry|
@@ -937,6 +812,12 @@ class Document < AbstractBlock
937
812
  end
938
813
  end
939
814
 
815
+ # Public: Restore the attributes to the previously saved state (attributes in header)
816
+ def restore_attributes
817
+ @catalog[:callouts].rewind unless @parent_document
818
+ @attributes.replace @header_attributes
819
+ end
820
+
940
821
  # Public: Set the specified attribute on the document if the name is not locked
941
822
  #
942
823
  # If the attribute is locked, false is returned. Otherwise, the value is
@@ -945,28 +826,27 @@ class Document < AbstractBlock
945
826
  # 'doctype', then the value of backend-related attributes are updated.
946
827
  #
947
828
  # name - the String attribute name
948
- # value - the String attribute value; must not be nil (default: '')
829
+ # value - the String attribute value; must not be nil (optional, default: '')
949
830
  #
950
- # Returns the resolved value if the attribute was set or false if it was not because it's locked.
831
+ # Returns the substituted value if the attribute was set or nil if it was not because it's locked.
951
832
  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
833
+ unless attribute_locked? name
834
+ value = apply_attribute_value_subs value unless value.empty?
835
+ # NOTE if @header_attributes is set, we're beyond the document header
836
+ if @header_attributes
837
+ @attributes[name] = value
957
838
  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
965
- else
966
- @attributes[name] = resolved_value
839
+ case name
840
+ when 'backend'
841
+ update_backend_attributes value, (@attributes_modified.delete? 'htmlsyntax') && value == @backend
842
+ when 'doctype'
843
+ update_doctype_attributes value
844
+ else
845
+ @attributes[name] = value
846
+ end
847
+ @attributes_modified << name
967
848
  end
968
- @attributes_modified << name
969
- resolved_value
849
+ value
970
850
  end
971
851
  end
972
852
 
@@ -1017,151 +897,6 @@ class Document < AbstractBlock
1017
897
  end
1018
898
  end
1019
899
 
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
900
  # Public: Convert the AsciiDoc document using the templates
1166
901
  # loaded by the Converter. If a :template_dir is not specified,
1167
902
  # or a template is missing, the converter will fall back to
@@ -1223,13 +958,11 @@ class Document < AbstractBlock
1223
958
  # ensure there's a trailing endline
1224
959
  target.write LF
1225
960
  end
1226
- elsif COERCE_ENCODING
1227
- ::IO.write target, output, :encoding => ::Encoding::UTF_8
1228
961
  else
1229
- ::IO.write target, output
962
+ ::File.write target, output, mode: FILE_WRITE_MODE
1230
963
  end
1231
- if @backend == 'manpage' && ::String === target && (@converter.respond_to? :write_alternate_pages)
1232
- @converter.write_alternate_pages @attributes['mannames'], @attributes['manvolnum'], target
964
+ if @backend == 'manpage' && ::String === target && (@converter.class.respond_to? :write_alternate_pages)
965
+ @converter.class.write_alternate_pages @attributes['mannames'], @attributes['manvolnum'], target
1233
966
  end
1234
967
  end
1235
968
  @timings.record :write if @timings
@@ -1274,10 +1007,7 @@ class Document < AbstractBlock
1274
1007
  # returns The contents of the docinfo file(s) or empty string if no files are
1275
1008
  # found or the safe mode is secure or greater.
1276
1009
  def docinfo location = :head, suffix = nil
1277
- if safe >= SafeMode::SECURE
1278
- ''
1279
- else
1280
- content = []
1010
+ if safe < SafeMode::SECURE
1281
1011
  qualifier = %(-#{location}) unless location == :head
1282
1012
  suffix = @outfilesuffix unless suffix
1283
1013
 
@@ -1294,33 +1024,85 @@ class Document < AbstractBlock
1294
1024
  end
1295
1025
 
1296
1026
  if docinfo
1027
+ content = []
1297
1028
  docinfo_file, docinfo_dir, docinfo_subs = %(docinfo#{qualifier}#{suffix}), @attributes['docinfodir'], resolve_docinfo_subs
1298
1029
  unless (docinfo & ['shared', %(shared-#{location})]).empty?
1299
1030
  docinfo_path = normalize_system_path docinfo_file, docinfo_dir
1300
1031
  # 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)
1032
+ if (shared_docinfo = read_asset docinfo_path, normalize: true)
1033
+ content << (apply_subs shared_docinfo, docinfo_subs)
1303
1034
  end
1304
1035
  end
1305
1036
 
1306
1037
  unless @attributes['docname'].nil_or_empty? || (docinfo & ['private', %(private-#{location})]).empty?
1307
1038
  docinfo_path = normalize_system_path %(#{@attributes['docname']}-#{docinfo_file}), docinfo_dir
1308
1039
  # 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)
1040
+ if (private_docinfo = read_asset docinfo_path, normalize: true)
1041
+ content << (apply_subs private_docinfo, docinfo_subs)
1311
1042
  end
1312
1043
  end
1313
1044
  end
1045
+ end
1314
1046
 
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
1047
+ # TODO allow document to control whether extension docinfo is contributed
1048
+ if @extensions && (docinfo_processors? location)
1049
+ (content ||= []).concat @docinfo_processor_extensions[location].map {|ext| ext.process_method[self] }.compact
1050
+ end
1319
1051
 
1320
- content.join LF
1052
+ content ? (content.join LF) : ''
1053
+ end
1054
+
1055
+ def docinfo_processors?(location = :head)
1056
+ if @docinfo_processor_extensions.key?(location)
1057
+ # false means we already performed a lookup and didn't find any
1058
+ @docinfo_processor_extensions[location] != false
1059
+ elsif @extensions && @document.extensions.docinfo_processors?(location)
1060
+ !!(@docinfo_processor_extensions[location] = @document.extensions.docinfo_processors(location))
1061
+ else
1062
+ @docinfo_processor_extensions[location] = false
1321
1063
  end
1322
1064
  end
1323
1065
 
1066
+ def to_s
1067
+ %(#<#{self.class}@#{object_id} {doctype: #{doctype.inspect}, doctitle: #{(@header != nil ? @header.title : nil).inspect}, blocks: #{@blocks.size}}>)
1068
+ end
1069
+
1070
+ private
1071
+
1072
+ # Internal: Apply substitutions to the attribute value
1073
+ #
1074
+ # If the value is an inline passthrough macro (e.g., pass:<subs>[value]),
1075
+ # apply the substitutions defined in <subs> to the value, or leave the value
1076
+ # unmodified if no substitutions are specified. If the value is not an
1077
+ # inline passthrough macro, apply header substitutions to the value.
1078
+ #
1079
+ # value - The String attribute value on which to perform substitutions
1080
+ #
1081
+ # Returns The String value with substitutions performed
1082
+ def apply_attribute_value_subs value
1083
+ if AttributeEntryPassMacroRx =~ value
1084
+ value = $1 ? (apply_subs $2, (resolve_pass_subs $1)) : $2
1085
+ else
1086
+ value = apply_header_subs value
1087
+ end
1088
+ @max_attribute_value_size ? (limit_bytesize value, @max_attribute_value_size) : value
1089
+ end
1090
+
1091
+ # Internal: Safely truncates a string to the specified number of bytes.
1092
+ #
1093
+ # If a multibyte char gets split, the dangling fragment is dropped.
1094
+ #
1095
+ # str - The String the truncate.
1096
+ # max - The maximum allowable size of the String, in bytes.
1097
+ #
1098
+ # Returns the String truncated to the specified bytesize.
1099
+ def limit_bytesize str, max
1100
+ if str.bytesize > max
1101
+ max -= 1 until (str = str.byteslice 0, max).valid_encoding?
1102
+ end
1103
+ str
1104
+ end
1105
+
1324
1106
  # Internal: Resolve the list of comma-delimited subs to apply to docinfo files.
1325
1107
  #
1326
1108
  # Resolve the list of substitutions from the value of the docinfosubs
@@ -1332,20 +1114,260 @@ class Document < AbstractBlock
1332
1114
  (@attributes.key? 'docinfosubs') ? (resolve_subs @attributes['docinfosubs'], :block, nil, 'docinfo') : [:attributes]
1333
1115
  end
1334
1116
 
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))
1117
+ # Internal: Create and initialize an instance of the converter for this document
1118
+ #--
1119
+ # QUESTION is there any additional information we should be passing to the converter?
1120
+ def create_converter backend, delegate_backend
1121
+ converter_opts = { document: self, htmlsyntax: @attributes['htmlsyntax'] }
1122
+ if (template_dirs = (opts = @options)[:template_dirs] || opts[:template_dir])
1123
+ converter_opts[:template_dirs] = [*template_dirs]
1124
+ converter_opts[:template_cache] = opts.fetch :template_cache, true
1125
+ converter_opts[:template_engine] = opts[:template_engine]
1126
+ converter_opts[:template_engine_options] = opts[:template_engine_options]
1127
+ converter_opts[:eruby] = opts[:eruby]
1128
+ converter_opts[:safe] = @safe
1129
+ converter_opts[:delegate_backend] = delegate_backend if delegate_backend
1130
+ end
1131
+ if (converter = opts[:converter])
1132
+ (Converter::CustomFactory.new backend => converter).create backend, converter_opts
1341
1133
  else
1342
- @docinfo_processor_extensions[location] = false
1134
+ (opts.fetch :converter_factory, Converter).create backend, converter_opts
1343
1135
  end
1344
1136
  end
1345
1137
 
1346
- def to_s
1347
- %(#<#{self.class}@#{object_id} {doctype: #{doctype.inspect}, doctitle: #{(@header != nil ? @header.title : nil).inspect}, blocks: #{@blocks.size}}>)
1138
+ # Internal: Delete any attributes stored for playback
1139
+ def clear_playback_attributes(attributes)
1140
+ attributes.delete(:attribute_entries)
1348
1141
  end
1349
1142
 
1143
+ # Internal: Branch the attributes so that the original state can be restored
1144
+ # at a future time.
1145
+ #
1146
+ # Returns the duplicated attributes, which will later be restored
1147
+ def save_attributes
1148
+ unless ((attrs = @attributes).key? 'doctitle') || !(doctitle_val = doctitle)
1149
+ attrs['doctitle'] = doctitle_val
1150
+ end
1151
+
1152
+ # css-signature cannot be updated after header attributes are processed
1153
+ @id ||= attrs['css-signature']
1154
+
1155
+ if (toc_val = (attrs.delete 'toc2') ? 'left' : attrs['toc'])
1156
+ # toc-placement allows us to separate position from using fitted slot vs macro
1157
+ toc_position_val = (toc_placement_val = attrs.fetch 'toc-placement', 'macro') && toc_placement_val != 'auto' ? toc_placement_val : attrs['toc-position']
1158
+ unless toc_val.empty? && toc_position_val.nil_or_empty?
1159
+ default_toc_position = 'left'
1160
+ # TODO rename toc2 to aside-toc
1161
+ default_toc_class = 'toc2'
1162
+ position = toc_position_val.nil_or_empty? ? (toc_val.empty? ? default_toc_position : toc_val) : toc_position_val
1163
+ attrs['toc'] = ''
1164
+ attrs['toc-placement'] = 'auto'
1165
+ case position
1166
+ when 'left', '<', '&lt;'
1167
+ attrs['toc-position'] = 'left'
1168
+ when 'right', '>', '&gt;'
1169
+ attrs['toc-position'] = 'right'
1170
+ when 'top', '^'
1171
+ attrs['toc-position'] = 'top'
1172
+ when 'bottom', 'v'
1173
+ attrs['toc-position'] = 'bottom'
1174
+ when 'preamble', 'macro'
1175
+ attrs['toc-position'] = 'content'
1176
+ attrs['toc-placement'] = position
1177
+ default_toc_class = nil
1178
+ else
1179
+ attrs.delete 'toc-position'
1180
+ default_toc_class = nil
1181
+ end
1182
+ attrs['toc-class'] ||= default_toc_class if default_toc_class
1183
+ end
1184
+ end
1185
+
1186
+ if (icons_val = attrs['icons']) && !(attrs.key? 'icontype')
1187
+ case icons_val
1188
+ when '', 'font'
1189
+ else
1190
+ attrs['icons'] = ''
1191
+ attrs['icontype'] = icons_val
1192
+ end
1193
+ end
1194
+
1195
+ if (@compat_mode = attrs.key? 'compat-mode')
1196
+ attrs['source-language'] = attrs['language'] if attrs.key? 'language'
1197
+ end
1198
+
1199
+ unless @parent_document
1200
+ if (basebackend = attrs['basebackend']) == 'html'
1201
+ # QUESTION should we allow source-highlighter to be disabled in AsciiDoc table cell?
1202
+ if (syntax_hl_name = attrs['source-highlighter']) && !attrs[%(#{syntax_hl_name}-unavailable)]
1203
+ if (syntax_hl_factory = @options[:syntax_highlighter_factory])
1204
+ @syntax_highlighter = syntax_hl_factory.create syntax_hl_name, @backend, document: self
1205
+ elsif (syntax_hls = @options[:syntax_highlighters])
1206
+ @syntax_highlighter = (SyntaxHighlighter::DefaultFactoryProxy.new syntax_hls).create syntax_hl_name, @backend, document: self
1207
+ else
1208
+ @syntax_highlighter = SyntaxHighlighter.create syntax_hl_name, @backend, document: self
1209
+ end
1210
+ end
1211
+ # enable toc and sectnums (i.e., numbered) by default in DocBook backend
1212
+ elsif basebackend == 'docbook'
1213
+ # NOTE the attributes_modified should go away once we have a proper attribute storage & tracking facility
1214
+ attrs['toc'] = '' unless (attribute_locked? 'toc') || (@attributes_modified.include? 'toc')
1215
+ attrs['sectnums'] = '' unless (attribute_locked? 'sectnums') || (@attributes_modified.include? 'sectnums')
1216
+ end
1217
+
1218
+ # NOTE pin the outfilesuffix after the header is parsed
1219
+ @outfilesuffix = attrs['outfilesuffix']
1220
+
1221
+ # unfreeze "flexible" attributes
1222
+ FLEXIBLE_ATTRIBUTES.each do |name|
1223
+ # turning a flexible attribute off should be permanent
1224
+ # (we may need more config if that's not always the case)
1225
+ if @attribute_overrides.key?(name) && @attribute_overrides[name]
1226
+ @attribute_overrides.delete(name)
1227
+ end
1228
+ end
1229
+ end
1230
+
1231
+ @header_attributes = attrs.dup
1232
+ end
1233
+
1234
+ # Internal: Assign the local and document datetime attributes, which includes localdate, localyear, localtime,
1235
+ # localdatetime, docdate, docyear, doctime, and docdatetime. Honor the SOURCE_DATE_EPOCH environment variable, if set.
1236
+ def fill_datetime_attributes attrs, input_mtime
1237
+ # See https://reproducible-builds.org/specs/source-date-epoch/
1238
+ now = (::ENV.key? 'SOURCE_DATE_EPOCH') ? (source_date_epoch = (::Time.at Integer ::ENV['SOURCE_DATE_EPOCH']).utc) : ::Time.now
1239
+ if (localdate = attrs['localdate'])
1240
+ attrs['localyear'] ||= (localdate.index '-') == 4 ? (localdate.slice 0, 4) : nil
1241
+ else
1242
+ localdate = attrs['localdate'] = now.strftime '%F'
1243
+ attrs['localyear'] ||= now.year.to_s
1244
+ end
1245
+ # %Z is OS dependent and may contain characters that aren't UTF-8 encoded (see asciidoctor#2770 and asciidoctor.js#23)
1246
+ localtime = (attrs['localtime'] ||= now.strftime %(%T #{now.utc_offset == 0 ? 'UTC' : '%z'}))
1247
+ attrs['localdatetime'] ||= %(#{localdate} #{localtime})
1248
+ # docdate, doctime and docdatetime should default to localdate, localtime and localdatetime if not otherwise set
1249
+ input_mtime = source_date_epoch || input_mtime || now
1250
+ if (docdate = attrs['docdate'])
1251
+ attrs['docyear'] ||= ((docdate.index '-') == 4 ? (docdate.slice 0, 4) : nil)
1252
+ else
1253
+ docdate = attrs['docdate'] = input_mtime.strftime '%F'
1254
+ attrs['docyear'] ||= input_mtime.year.to_s
1255
+ end
1256
+ # %Z is OS dependent and may contain characters that aren't UTF-8 encoded (see asciidoctor#2770 and asciidoctor.js#23)
1257
+ doctime = (attrs['doctime'] ||= input_mtime.strftime %(%T #{input_mtime.utc_offset == 0 ? 'UTC' : '%z'}))
1258
+ attrs['docdatetime'] ||= %(#{docdate} #{doctime})
1259
+ nil
1260
+ end
1261
+
1262
+ # Internal: Update the backend attributes to reflect a change in the active backend.
1263
+ #
1264
+ # This method also handles updating the related doctype attributes if the
1265
+ # doctype attribute is assigned at the time this method is called.
1266
+ #
1267
+ # Returns the resolved String backend if updated, nothing otherwise.
1268
+ def update_backend_attributes new_backend, init = nil
1269
+ if init || new_backend != @backend
1270
+ current_backend = @backend
1271
+ current_basebackend = (attrs = @attributes)['basebackend']
1272
+ current_doctype = @doctype
1273
+ actual_backend, _, new_backend = new_backend.partition ':' if new_backend.include? ':'
1274
+ if new_backend.start_with? 'xhtml'
1275
+ attrs['htmlsyntax'] = 'xml'
1276
+ new_backend = new_backend.slice 1, new_backend.length
1277
+ elsif new_backend.start_with? 'html'
1278
+ attrs['htmlsyntax'] ||= 'html'
1279
+ end
1280
+ new_backend = BACKEND_ALIASES[new_backend] || new_backend
1281
+ new_backend, delegate_backend = actual_backend, new_backend if actual_backend
1282
+ if current_doctype
1283
+ if current_backend
1284
+ attrs.delete %(backend-#{current_backend})
1285
+ attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype})
1286
+ end
1287
+ attrs[%(backend-#{new_backend}-doctype-#{current_doctype})] = ''
1288
+ attrs[%(doctype-#{current_doctype})] = ''
1289
+ elsif current_backend
1290
+ attrs.delete %(backend-#{current_backend})
1291
+ end
1292
+ attrs[%(backend-#{new_backend})] = ''
1293
+ # QUESTION should we defer the @backend assignment until after the converter is created?
1294
+ @backend = attrs['backend'] = new_backend
1295
+ # (re)initialize converter
1296
+ if Converter::BackendTraits === (converter = create_converter new_backend, delegate_backend)
1297
+ new_basebackend = converter.basebackend
1298
+ new_filetype = converter.filetype
1299
+ if (htmlsyntax = converter.htmlsyntax)
1300
+ attrs['htmlsyntax'] = htmlsyntax
1301
+ end
1302
+ if init
1303
+ attrs['outfilesuffix'] ||= converter.outfilesuffix
1304
+ else
1305
+ attrs['outfilesuffix'] = converter.outfilesuffix unless attribute_locked? 'outfilesuffix'
1306
+ end
1307
+ elsif converter
1308
+ backend_traits = Converter::BackendTraits.derive_backend_traits new_backend
1309
+ new_basebackend = backend_traits[:basebackend]
1310
+ new_filetype = backend_traits[:filetype]
1311
+ if init
1312
+ attrs['outfilesuffix'] ||= backend_traits[:outfilesuffix]
1313
+ else
1314
+ attrs['outfilesuffix'] = backend_traits[:outfilesuffix] unless attribute_locked? 'outfilesuffix'
1315
+ end
1316
+ else
1317
+ # NOTE ideally we shouldn't need the converter before the converter phase, but we do
1318
+ raise ::NotImplementedError, %(asciidoctor: FAILED: missing converter for backend '#{new_backend}'. Processing aborted.)
1319
+ end
1320
+ @converter = converter
1321
+ if (current_filetype = attrs['filetype'])
1322
+ attrs.delete %(filetype-#{current_filetype})
1323
+ end
1324
+ attrs['filetype'] = new_filetype
1325
+ attrs[%(filetype-#{new_filetype})] = ''
1326
+ if (page_width = DEFAULT_PAGE_WIDTHS[new_basebackend])
1327
+ attrs['pagewidth'] = page_width
1328
+ else
1329
+ attrs.delete 'pagewidth'
1330
+ end
1331
+ if new_basebackend != current_basebackend
1332
+ if current_doctype
1333
+ if current_basebackend
1334
+ attrs.delete %(basebackend-#{current_basebackend})
1335
+ attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype})
1336
+ end
1337
+ attrs[%(basebackend-#{new_basebackend}-doctype-#{current_doctype})] = ''
1338
+ elsif current_basebackend
1339
+ attrs.delete %(basebackend-#{current_basebackend})
1340
+ end
1341
+ attrs[%(basebackend-#{new_basebackend})] = ''
1342
+ attrs['basebackend'] = new_basebackend
1343
+ end
1344
+ new_backend
1345
+ end
1346
+ end
1347
+
1348
+ # Internal: Update the doctype and backend attributes to reflect a change in the active doctype.
1349
+ #
1350
+ # Returns the String doctype if updated, nothing otherwise.
1351
+ def update_doctype_attributes new_doctype
1352
+ if new_doctype && new_doctype != @doctype
1353
+ current_backend, current_basebackend, current_doctype = @backend, (attrs = @attributes)['basebackend'], @doctype
1354
+ if current_doctype
1355
+ attrs.delete %(doctype-#{current_doctype})
1356
+ if current_backend
1357
+ attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype})
1358
+ attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = ''
1359
+ end
1360
+ if current_basebackend
1361
+ attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype})
1362
+ attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = ''
1363
+ end
1364
+ else
1365
+ attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = '' if current_backend
1366
+ attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = '' if current_basebackend
1367
+ end
1368
+ attrs[%(doctype-#{new_doctype})] = ''
1369
+ return @doctype = attrs['doctype'] = new_doctype
1370
+ end
1371
+ end
1350
1372
  end
1351
1373
  end