asciidoctor 1.5.8 → 2.0.0.rc.1

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