asciidoctor 0.1.4 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of asciidoctor might be problematic. Click here for more details.

Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +209 -25
  3. data/{LICENSE → LICENSE.adoc} +4 -3
  4. data/README.adoc +392 -395
  5. data/Rakefile +94 -137
  6. data/benchmark/benchmark.rb +127 -0
  7. data/benchmark/sample-data/mdbasics.adoc +334 -0
  8. data/bin/asciidoctor +5 -8
  9. data/bin/asciidoctor-safe +4 -8
  10. data/compat/asciidoc.conf +78 -11
  11. data/compat/font-awesome-3-compat.css +397 -0
  12. data/data/stylesheets/asciidoctor-default.css +399 -0
  13. data/data/stylesheets/coderay-asciidoctor.css +89 -0
  14. data/features/open_block.feature +92 -0
  15. data/features/pass_block.feature +66 -0
  16. data/features/step_definitions.rb +42 -0
  17. data/features/text_formatting.feature +55 -0
  18. data/features/xref.feature +116 -0
  19. data/lib/asciidoctor.rb +1155 -605
  20. data/lib/asciidoctor/abstract_block.rb +157 -71
  21. data/lib/asciidoctor/abstract_node.rb +150 -93
  22. data/lib/asciidoctor/attribute_list.rb +85 -90
  23. data/lib/asciidoctor/block.rb +51 -24
  24. data/lib/asciidoctor/callouts.rb +4 -7
  25. data/lib/asciidoctor/cli.rb +3 -0
  26. data/lib/asciidoctor/cli/invoker.rb +86 -76
  27. data/lib/asciidoctor/cli/options.rb +111 -61
  28. data/lib/asciidoctor/converter.rb +232 -0
  29. data/lib/asciidoctor/converter/base.rb +58 -0
  30. data/lib/asciidoctor/converter/composite.rb +66 -0
  31. data/lib/asciidoctor/converter/docbook45.rb +94 -0
  32. data/lib/asciidoctor/converter/docbook5.rb +684 -0
  33. data/lib/asciidoctor/converter/factory.rb +225 -0
  34. data/lib/asciidoctor/converter/html5.rb +1081 -0
  35. data/lib/asciidoctor/converter/template.rb +296 -0
  36. data/lib/asciidoctor/core_ext.rb +7 -0
  37. data/lib/asciidoctor/core_ext/object/nil_or_empty.rb +23 -0
  38. data/lib/asciidoctor/core_ext/string/chr.rb +6 -0
  39. data/lib/asciidoctor/core_ext/symbol/length.rb +6 -0
  40. data/lib/asciidoctor/document.rb +590 -304
  41. data/lib/asciidoctor/extensions.rb +1100 -308
  42. data/lib/asciidoctor/helpers.rb +109 -46
  43. data/lib/asciidoctor/inline.rb +16 -9
  44. data/lib/asciidoctor/list.rb +23 -15
  45. data/lib/asciidoctor/opal_ext.rb +4 -0
  46. data/lib/asciidoctor/opal_ext/comparable.rb +38 -0
  47. data/lib/asciidoctor/opal_ext/dir.rb +13 -0
  48. data/lib/asciidoctor/opal_ext/error.rb +2 -0
  49. data/lib/asciidoctor/opal_ext/file.rb +125 -0
  50. data/lib/asciidoctor/{lexer.rb → parser.rb} +646 -455
  51. data/lib/asciidoctor/path_resolver.rb +141 -77
  52. data/lib/asciidoctor/reader.rb +257 -187
  53. data/lib/asciidoctor/section.rb +12 -16
  54. data/lib/asciidoctor/stylesheets.rb +91 -0
  55. data/lib/asciidoctor/substitutors.rb +1548 -0
  56. data/lib/asciidoctor/table.rb +73 -57
  57. data/lib/asciidoctor/timings.rb +39 -0
  58. data/lib/asciidoctor/version.rb +1 -1
  59. data/man/asciidoctor.1 +22 -14
  60. data/man/asciidoctor.adoc +18 -10
  61. data/test/attributes_test.rb +314 -14
  62. data/test/blocks_test.rb +763 -118
  63. data/test/converter_test.rb +352 -0
  64. data/test/document_test.rb +518 -199
  65. data/test/extensions_test.rb +273 -103
  66. data/test/fixtures/asciidoc_index.txt +27 -13
  67. data/test/fixtures/basic-docinfo.xml +1 -1
  68. data/test/fixtures/chapter-a.adoc +3 -0
  69. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
  70. data/test/fixtures/docinfo.xml +1 -1
  71. data/test/fixtures/include-file.asciidoc +2 -0
  72. data/test/fixtures/master.adoc +5 -0
  73. data/test/invoker_test.rb +173 -61
  74. data/test/links_test.rb +97 -21
  75. data/test/lists_test.rb +181 -22
  76. data/test/options_test.rb +86 -2
  77. data/test/paragraphs_test.rb +47 -5
  78. data/test/{lexer_test.rb → parser_test.rb} +128 -57
  79. data/test/paths_test.rb +36 -1
  80. data/test/preamble_test.rb +25 -17
  81. data/test/reader_test.rb +404 -249
  82. data/test/sections_test.rb +623 -58
  83. data/test/substitutions_test.rb +609 -132
  84. data/test/tables_test.rb +198 -24
  85. data/test/test_helper.rb +101 -31
  86. data/test/text_test.rb +88 -31
  87. metadata +160 -64
  88. data/Gemfile +0 -12
  89. data/Guardfile +0 -18
  90. data/asciidoctor.gemspec +0 -143
  91. data/lib/asciidoctor/backends/_stylesheets.rb +0 -466
  92. data/lib/asciidoctor/backends/base_template.rb +0 -114
  93. data/lib/asciidoctor/backends/docbook45.rb +0 -774
  94. data/lib/asciidoctor/backends/docbook5.rb +0 -103
  95. data/lib/asciidoctor/backends/html5.rb +0 -1214
  96. data/lib/asciidoctor/renderer.rb +0 -259
  97. data/lib/asciidoctor/substituters.rb +0 -1083
  98. data/test/fixtures/asciidoc.txt +0 -105
  99. data/test/fixtures/ascshort.txt +0 -32
  100. data/test/fixtures/list_elements.asciidoc +0 -10
  101. data/test/renderer_test.rb +0 -162
@@ -0,0 +1,296 @@
1
+ module Asciidoctor
2
+ # A {Converter} implementation that uses templates composed in template
3
+ # languages supported by {https://github.com/rtomayko/tilt Tilt} to convert
4
+ # {AbstractNode} objects from a parsed AsciiDoc document tree to the backend
5
+ # format.
6
+ #
7
+ # The converter scans the provided directories for template files that are
8
+ # supported by Tilt. If an engine name (e.g., "slim") is specified in the
9
+ # options Hash passed to the constructor, the scan is limited to template
10
+ # files that have a matching extension (e.g., ".slim"). The scanner trims any
11
+ # extensions from the basename of the file and uses the resulting name as the
12
+ # key under which to store the template. When the {Converter#convert} method
13
+ # is invoked, the transform argument is used to select the template from this
14
+ # table and use it to convert the node.
15
+ #
16
+ # For example, the template file "path/to/templates/paragraph.html.slim" will
17
+ # be registered as the "paragraph" transform. The template would then be used
18
+ # to convert a paragraph {Block} object from the parsed AsciiDoc tree to an
19
+ # HTML backend format (e.g., "html5").
20
+ #
21
+ # As an optimization, scan results and templates are cached for the lifetime
22
+ # of the Ruby process. If the {https://rubygems.org/gems/thread_safe
23
+ # thread_safe} gem is installed, these caches are guaranteed to be thread
24
+ # safe. If this gem is not present, a warning is issued.
25
+ class Converter::TemplateConverter < Converter::Base
26
+ DEFAULT_ENGINE_OPTIONS = {
27
+ :erb => { :trim => '<' },
28
+ # TODO line 466 of haml/compiler.rb sorts the attributes; file an issue to make this configurable
29
+ # NOTE AsciiDoc syntax expects HTML/XML output to use double quotes around attribute values
30
+ :haml => { :format => :xhtml, :attr_wrapper => '"', :ugly => true, :escape_attrs => false },
31
+ :slim => { :disable_escape => true, :sort_attrs => false, :pretty => false }
32
+ }
33
+
34
+ # QUESTION are we handling how we load the thread_safe support correctly?
35
+ begin
36
+ require 'thread_safe' unless defined? ::ThreadSafe
37
+ @caches = { :scans => ::ThreadSafe::Cache.new, :templates => ::ThreadSafe::Cache.new }
38
+ rescue ::LoadError
39
+ @caches = {}
40
+ # FIXME perhaps only warn if the cache option is enabled?
41
+ warn 'asciidoctor: WARNING: gem \'thread_safe\' is not installed. This gem recommended when using custom backend templates.'
42
+ end
43
+
44
+ def self.caches
45
+ @caches
46
+ end
47
+
48
+ def self.clear_caches
49
+ @caches[:scans].clear if @caches[:scans]
50
+ @caches[:templates].clear if @caches[:templates]
51
+ end
52
+
53
+ def initialize backend, template_dirs, opts = {}
54
+ @backend = backend
55
+ @templates = {}
56
+ @template_dirs = template_dirs
57
+ @eruby = opts[:eruby]
58
+ @engine = opts[:template_engine]
59
+ @engine_options = DEFAULT_ENGINE_OPTIONS.inject({}) do |accum, (engine, default_opts)|
60
+ accum[engine] = default_opts.dup
61
+ accum
62
+ end
63
+ if (overrides = opts[:template_engine_options])
64
+ overrides.each do |engine, override_opts|
65
+ (@engine_options[engine] ||= {}).update override_opts
66
+ end
67
+ end
68
+ @engine_options[:haml][:format] = @engine_options[:slim][:format] = :html5 if opts[:htmlsyntax] == 'html'
69
+ case opts[:template_cache]
70
+ when true
71
+ @caches = self.class.caches
72
+ when ::Hash
73
+ @caches = opts[:template_cache]
74
+ else
75
+ @caches = {}
76
+ end
77
+ scan
78
+ #create_handlers
79
+ end
80
+
81
+ =begin
82
+ # Public: Called when this converter is added to a composite converter.
83
+ def composed parent
84
+ # TODO set the backend info determined during the scan
85
+ end
86
+ =end
87
+
88
+ # Internal: Scans the template directories specified in the constructor for Tilt-supported
89
+ # templates, loads the templates and stores the in a Hash that is accessible via the
90
+ # {TemplateConverter#templates} method.
91
+ #
92
+ # Returns nothing
93
+ def scan
94
+ path_resolver = PathResolver.new
95
+ backend = @backend
96
+ engine = @engine
97
+ @template_dirs.each do |template_dir|
98
+ # FIXME need to think about safe mode restrictions here
99
+ template_dir = path_resolver.system_path template_dir, nil
100
+ # NOTE last matching template wins for template name if no engine is given
101
+ file_pattern = '*'
102
+ if engine
103
+ file_pattern = %(*.#{engine})
104
+ # example: templates/haml
105
+ if ::File.directory?(engine_dir = (::File.join template_dir, engine))
106
+ template_dir = engine_dir
107
+ end
108
+ end
109
+
110
+ # example: templates/html5 or templates/haml/html5
111
+ if ::File.directory?(backend_dir = (::File.join template_dir, backend))
112
+ template_dir = backend_dir
113
+ end
114
+
115
+ pattern = ::File.join template_dir, file_pattern
116
+
117
+ if (scan_cache = @caches[:scans])
118
+ template_cache = @caches[:templates]
119
+ unless (templates = scan_cache[pattern])
120
+ templates = (scan_cache[pattern] = (scan_dir template_dir, pattern, template_cache))
121
+ end
122
+ templates.each do |name, template|
123
+ @templates[name] = template_cache[template.file] = template
124
+ end
125
+ else
126
+ @templates.update scan_dir(template_dir, pattern, @caches[:templates])
127
+ end
128
+ nil
129
+ end
130
+ end
131
+
132
+ =begin
133
+ # Internal: Creates convert methods (e.g., inline_anchor) that delegate to the discovered templates.
134
+ #
135
+ # Returns nothing
136
+ def create_handlers
137
+ @templates.each do |name, template|
138
+ create_handler name, template
139
+ end
140
+ nil
141
+ end
142
+
143
+ # Internal: Creates a convert method for the specified name that delegates to the specified template.
144
+ #
145
+ # Returns nothing
146
+ def create_handler name, template
147
+ metaclass = class << self; self; end
148
+ if name == 'document'
149
+ metaclass.send :define_method, name do |node|
150
+ (template.render node).strip
151
+ end
152
+ else
153
+ metaclass.send :define_method, name do |node|
154
+ (template.render node).chomp
155
+ end
156
+ end
157
+ end
158
+ =end
159
+
160
+ # Public: Convert an {AbstractNode} to the backend format using the named template.
161
+ #
162
+ # Looks for a template that matches the value of the
163
+ # {AbstractNode#node_name} property if a template name is not specified.
164
+ #
165
+ # node - the AbstractNode to convert
166
+ # template_name - the String name of the template to use, or the value of
167
+ # the node_name property on the node if a template name is
168
+ # not specified. (optional, default: nil)
169
+ #
170
+ # Returns the [String] result from rendering the template
171
+ def convert node, template_name = nil
172
+ template_name ||= node.node_name
173
+ unless (template = @templates[template_name])
174
+ raise %(Could not find a custom template to handle transform: #{template_name})
175
+ end
176
+ if template_name == 'document'
177
+ (template.render node).strip
178
+ else
179
+ (template.render node).chomp
180
+ end
181
+ end
182
+
183
+ # Public: Convert an {AbstractNode} using the named template with the
184
+ # additional options provided.
185
+ #
186
+ # Looks for a template that matches the value of the
187
+ # {AbstractNode#node_name} property if a template name is not specified.
188
+ #
189
+ # node - the AbstractNode to convert
190
+ # template_name - the String name of the template to use, or the value of
191
+ # the node_name property on the node if a template name is
192
+ # not specified. (optional, default: nil)
193
+ # opts - an optional Hash that is passed as local variables to the
194
+ # template. (optional, default: {})
195
+ #
196
+ # Returns the [String] result from rendering the template
197
+ def convert_with_options node, template_name = nil, opts = {}
198
+ template_name ||= node.node_name
199
+ unless (template = @templates[template_name])
200
+ raise %(Could not find a custom template to handle transform: #{template_name})
201
+ end
202
+ (template.render node, opts).chomp
203
+ end
204
+
205
+ # Public: Checks whether there is a Tilt template registered with the specified name.
206
+ #
207
+ # name - the String template name
208
+ #
209
+ # Returns a [Boolean] that indicates whether a Tilt template is registered for the
210
+ # specified template name.
211
+ def handles? name
212
+ @templates.key? name
213
+ end
214
+
215
+ # Public: Retrieves the templates that this converter manages.
216
+ #
217
+ # Returns a [Hash] of Tilt template objects keyed by template name.
218
+ def templates
219
+ @templates.dup.freeze
220
+ end
221
+
222
+ # Public: Registers a Tilt template with this converter.
223
+ #
224
+ # name - the String template name
225
+ # template - the Tilt template object to register
226
+ #
227
+ # Returns the Tilt template object
228
+ def register name, template
229
+ @templates[name] = if (template_cache = @caches[:templates])
230
+ template_cache[template.file] = template
231
+ else
232
+ template
233
+ end
234
+ #create_handler name, template
235
+ end
236
+
237
+ # Internal: Scan the specified directory for template files matching pattern and instantiate
238
+ # a Tilt template for each matched file.
239
+ #
240
+ # Returns the scan result as a [Hash]
241
+ def scan_dir template_dir, pattern, template_cache = nil
242
+ result = {}
243
+ eruby_loaded = nil
244
+ # Grab the files in the top level of the directory (do not recurse)
245
+ ::Dir.glob(pattern).select {|match| ::File.file? match }.each do |file|
246
+ if (basename = ::File.basename file) == 'helpers.rb' || (path_segments = basename.split '.').size < 2
247
+ next
248
+ end
249
+ # TODO we could derive the basebackend from the minor extension of the template file
250
+ #name, *rest, ext_name = *path_segments # this form only works in Ruby >= 1.9
251
+ name = path_segments[0]
252
+ if name == 'block_ruler'
253
+ name = 'thematic_break'
254
+ elsif name.start_with? 'block_'
255
+ name = name[6..-1]
256
+ end
257
+ ext_name = path_segments[-1]
258
+ template_class = ::Tilt
259
+ extra_engine_options = {}
260
+ if ext_name == 'slim'
261
+ # slim doesn't get loaded by Tilt, so we have to load it explicitly
262
+ Helpers.require_library 'slim' unless defined? ::Slim
263
+ elsif ext_name == 'erb'
264
+ template_class, extra_engine_options = (eruby_loaded ||= load_eruby @eruby)
265
+ end
266
+ next unless ::Tilt.registered? ext_name
267
+ unless template_cache && (template = template_cache[file])
268
+ template = template_class.new file, 1, (@engine_options[ext_name.to_sym] || {}).merge(extra_engine_options)
269
+ end
270
+ result[name] = template
271
+ end
272
+ if ::File.file?(helpers = (::File.join template_dir, 'helpers.rb'))
273
+ require helpers
274
+ end
275
+ result
276
+ end
277
+
278
+ # Internal: Load the eRuby implementation
279
+ #
280
+ # name - the String name of the eRuby implementation
281
+ #
282
+ # Returns an [Array] containing the Tilt template Class for the eRuby implementation
283
+ # and a Hash of additional options to pass to the initializer
284
+ def load_eruby name
285
+ if !name || name == 'erb'
286
+ require 'erb' unless defined? ::ERB
287
+ [::Tilt::ERBTemplate, {}]
288
+ elsif name == 'erubis'
289
+ Helpers.require_library 'erubis' unless defined? ::Erubis::FastEruby
290
+ [::Tilt::ErubisTemplate, { :engine_class => ::Erubis::FastEruby }]
291
+ else
292
+ raise ::ArgumentError, %(Unknown ERB implementation: #{name})
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,7 @@
1
+ require 'asciidoctor/core_ext/object/nil_or_empty'
2
+ unless RUBY_ENGINE == 'opal'
3
+ unless RUBY_MIN_VERSION_1_9
4
+ require 'asciidoctor/core_ext/string/chr'
5
+ require 'asciidoctor/core_ext/symbol/length'
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # A core library extension that defines the method nil_or_empty? as an alias to
2
+ # optimize checks for nil? or empty? on common object types such as NilClass,
3
+ # String, Array and Hash.
4
+
5
+ class NilClass
6
+ alias :nil_or_empty? :nil? unless respond_to? :nil_or_empty?
7
+ end
8
+
9
+ class String
10
+ alias :nil_or_empty? :empty? unless respond_to? :nil_or_empty?
11
+ end
12
+
13
+ class Array
14
+ alias :nil_or_empty? :empty? unless respond_to? :nil_or_empty?
15
+ end
16
+
17
+ class Hash
18
+ alias :nil_or_empty? :empty? unless respond_to? :nil_or_empty?
19
+ end
20
+
21
+ class Numeric
22
+ alias :nil_or_empty? :nil? unless respond_to? :nil_or_empty?
23
+ end
@@ -0,0 +1,6 @@
1
+ # Educate Ruby 1.8.7 about the String#chr method.
2
+ class String
3
+ def chr
4
+ self[0..0]
5
+ end unless respond_to? :chr
6
+ end
@@ -0,0 +1,6 @@
1
+ # Educate Ruby 1.8.7 about the Symbol#length method.
2
+ class Symbol
3
+ def length
4
+ to_s.length
5
+ end unless respond_to? :length
6
+ end
@@ -1,6 +1,5 @@
1
1
  module Asciidoctor
2
- # Public: Methods for parsing Asciidoc documents and rendering them
3
- # using erb templates.
2
+ # Public: Methods for parsing and converting AsciiDoc documents.
4
3
  #
5
4
  # There are several strategies for getting the title of the document:
6
5
  #
@@ -16,21 +15,55 @@ module Asciidoctor
16
15
  #
17
16
  # notitle - The h1 heading should not be shown
18
17
  # noheader - The header block (h1 heading, author, revision info) should not be shown
18
+ # nofooter - the footer block should not be shown
19
19
  class Document < AbstractBlock
20
20
 
21
- Footnote = Struct.new(:index, :id, :text)
22
- AttributeEntry = Struct.new(:name, :value, :negate) do
23
- def initialize(name, value, negate = nil)
24
- super(name, value, negate.nil? ? value.nil? : false)
21
+ Footnote = ::Struct.new :index, :id, :text
22
+
23
+ class AttributeEntry
24
+ attr_reader :name, :value, :negate
25
+
26
+ def initialize name, value, negate = nil
27
+ @name = name
28
+ @value = value
29
+ @negate = negate.nil? ? value.nil? : negate
25
30
  end
26
31
 
27
- def save_to(block_attributes)
32
+ def save_to block_attributes
28
33
  (block_attributes[:attribute_entries] ||= []) << self
29
34
  end
35
+ end
36
+
37
+ # Public Parsed and stores a partitioned title (i.e., title & subtitle).
38
+ class Title
39
+ attr_reader :main
40
+ attr_reader :subtitle
41
+ attr_reader :combined
42
+
43
+ def initialize val, opts = {}
44
+ # TODO separate sanitization by type (:cdata for HTML/XML, :plain for non-SGML, false for none)
45
+ if (@sanitized = opts[:sanitize]) && val.include?('<')
46
+ val = val.gsub(XmlSanitizeRx, '').tr_s(' ', ' ').strip
47
+ end
48
+ if (@combined = val).include? ': '
49
+ @main, _, @subtitle = val.rpartition ': '
50
+ else
51
+ @main = val
52
+ @subtitle = nil
53
+ end
54
+ end
30
55
 
31
- #def save_to_next_block(document)
32
- # (document.attributes[:pending_attribute_entries] ||= []) << self
33
- #end
56
+ def sanitized?
57
+ @sanitized
58
+ end
59
+
60
+ def subtitle?
61
+ !!@subtitle
62
+ end
63
+
64
+ def to_s
65
+ @combined
66
+ end
34
67
  end
35
68
 
36
69
  # Public A read-only integer value indicating the level of security that
@@ -45,7 +78,7 @@ class Document < AbstractBlock
45
78
  # of the source file and disables any macro other than the include macro.
46
79
  #
47
80
  # A value of 10 (SERVER) disallows the document from setting attributes that
48
- # would affect the rendering of the document, in addition to all the security
81
+ # would affect the conversion of the document, in addition to all the security
49
82
  # features of SafeMode::SAFE. For instance, this value disallows changing the
50
83
  # backend or the source-highlighter using an attribute defined in the source
51
84
  # document. This is the most fundamental level of security for server-side
@@ -68,6 +101,20 @@ class Document < AbstractBlock
68
101
  # this level is not currently implemented (and therefore not enforced)!
69
102
  attr_reader :safe
70
103
 
104
+ # Public: Get the Boolean AsciiDoc compatibility mode
105
+ #
106
+ # enabling this attribute activates the following syntax changes:
107
+ #
108
+ # * single quotes as constrained emphasis formatting marks
109
+ # * single backticks parsed as inline literal, formatted as monospace
110
+ # * single plus parsed as constrained, monospaced inline formatting
111
+ # * double plus parsed as constrained, monospaced inline formatting
112
+ #
113
+ attr_reader :compat_mode
114
+
115
+ # Public: Get the Boolean flag that indicates whether source map information is tracked by the parser
116
+ attr_reader :sourcemap
117
+
71
118
  # Public: Get the Hash of document references
72
119
  attr_reader :references
73
120
 
@@ -77,263 +124,342 @@ class Document < AbstractBlock
77
124
  # Public: Get the Hash of callouts
78
125
  attr_reader :callouts
79
126
 
80
- # Public: The section level 0 block
127
+ # Public: Get the level-0 Section
81
128
  attr_reader :header
82
129
 
83
- # Public: Base directory for rendering this document. Defaults to directory of the source file.
130
+ # Public: Get the String base directory for converting this document.
131
+ #
132
+ # Defaults to directory of the source file.
84
133
  # If the source is a string, defaults to the current directory.
85
134
  attr_reader :base_dir
86
135
 
87
- # Public: A reference to the parent document of this nested document.
136
+ # Public: Get a reference to the parent Document of this nested document.
88
137
  attr_reader :parent_document
89
138
 
90
- # Public: The extensions registry
139
+ # Public: Get the Reader associated with this document
140
+ attr_reader :reader
141
+
142
+ # Public: Get the Converter associated with this document
143
+ attr_reader :converter
144
+
145
+ # Public: Get the extensions registry
91
146
  attr_reader :extensions
92
147
 
93
- # Public: Initialize an Asciidoc object.
148
+ # Public: Initialize a {Document} object.
94
149
  #
95
- # data - The Array of Strings holding the Asciidoc source document. (default: [])
96
- # options - A Hash of options to control processing, such as setting the safe mode (:safe),
97
- # suppressing the header/footer (:header_footer) and attribute overrides (:attributes)
98
- # (default: {})
150
+ # data - The AsciiDoc source data as a String or String Array. (default: nil)
151
+ # options - A Hash of options to control processing (e.g., safe mode value (:safe), backend (:backend),
152
+ # header/footer toggle (:header_footer), custom attributes (:attributes)). (default: {})
99
153
  #
100
154
  # Examples
101
155
  #
102
- # data = File.readlines(filename)
103
- # doc = Asciidoctor::Document.new(data)
104
- # puts doc.render
105
- def initialize(data = [], options = {})
106
- super(self, :document)
107
-
108
- if options[:parent]
109
- @parent_document = options.delete(:parent)
110
- options[:base_dir] ||= @parent_document.base_dir
156
+ # data = File.read filename
157
+ # doc = Asciidoctor::Document.new data
158
+ # puts doc.convert
159
+ def initialize data = nil, options = {}
160
+ super self, :document
161
+
162
+ if (parent_doc = options.delete :parent)
163
+ @parent_document = parent_doc
164
+ options[:base_dir] ||= parent_doc.base_dir
165
+ @references = parent_doc.references.inject({}) do |accum, (key,ref)|
166
+ if key == :footnotes
167
+ accum[:footnotes] = []
168
+ else
169
+ accum[key] = ref
170
+ end
171
+ accum
172
+ end
111
173
  # QUESTION should we support setting attribute in parent document from nested document?
112
174
  # NOTE we must dup or else all the assignments to the overrides clobbers the real attributes
113
- @attribute_overrides = @parent_document.attributes.dup
114
- @safe = @parent_document.safe
115
- @renderer = @parent_document.renderer
175
+ attr_overrides = parent_doc.attributes.dup
176
+ attr_overrides.delete 'doctype'
177
+ attr_overrides.delete 'compat-mode'
178
+ @attribute_overrides = attr_overrides
179
+ @safe = parent_doc.safe
180
+ @compat_mode = parent_doc.compat_mode
181
+ @sourcemap = parent_doc.sourcemap
182
+ @converter = parent_doc.converter
116
183
  initialize_extensions = false
117
- @extensions = @parent_document.extensions
184
+ @extensions = parent_doc.extensions
118
185
  else
119
186
  @parent_document = nil
187
+ @references = {
188
+ :ids => {},
189
+ :footnotes => [],
190
+ :links => [],
191
+ :images => [],
192
+ :indexterms => [],
193
+ :includes => ::Set.new,
194
+ }
120
195
  # copy attributes map and normalize keys
121
196
  # attribute overrides are attributes that can only be set from the commandline
122
197
  # a direct assignment effectively makes the attribute a constant
123
198
  # a nil value or name with leading or trailing ! will result in the attribute being unassigned
124
- @attribute_overrides = (options[:attributes] || {}).inject({}) do |collector,(key,value)|
125
- if key.start_with?('!')
199
+ attr_overrides = {}
200
+ (options[:attributes] || {}).each do |key, value|
201
+ if key.start_with? '!'
126
202
  key = key[1..-1]
127
203
  value = nil
128
- elsif key.end_with?('!')
129
- key = key[0..-2]
204
+ elsif key.end_with? '!'
205
+ key = key.chop
130
206
  value = nil
131
207
  end
132
- collector[key.downcase] = value
133
- collector
208
+ attr_overrides[key.downcase] = value
134
209
  end
135
- @safe = nil
136
- @renderer = nil
137
- initialize_extensions = Asciidoctor.const_defined?('Extensions')
138
- @extensions = nil # initialize furthur down
139
- end
140
-
141
- @header = nil
142
- @references = {
143
- :ids => {},
144
- :footnotes => [],
145
- :links => [],
146
- :images => [],
147
- :indexterms => [],
148
- :includes => Set.new,
149
- }
150
- @counters = {}
151
- @callouts = Callouts.new
152
- @attributes_modified = Set.new
153
- @options = options
154
- unless @parent_document
210
+ @attribute_overrides = attr_overrides
155
211
  # safely resolve the safe mode from const, int or string
156
- if @safe.nil? && !(safe_mode = @options[:safe])
212
+ if !(safe_mode = options[:safe])
157
213
  @safe = SafeMode::SECURE
158
- elsif safe_mode.is_a?(Fixnum)
214
+ elsif ::Fixnum === safe_mode
159
215
  # be permissive in case API user wants to define new levels
160
216
  @safe = safe_mode
161
217
  else
218
+ # NOTE: not using infix rescue for performance reasons, see https://github.com/jruby/jruby/issues/1816
162
219
  begin
163
- @safe = SafeMode.const_get(safe_mode.to_s.upcase).to_i
220
+ @safe = SafeMode.const_get(safe_mode.to_s.upcase)
164
221
  rescue
165
- @safe = SafeMode::SECURE.to_i
222
+ @safe = SafeMode::SECURE
166
223
  end
167
224
  end
225
+ @sourcemap = options[:sourcemap]
226
+ @compat_mode = false
227
+ @converter = nil
228
+ initialize_extensions = defined? ::Asciidoctor::Extensions
229
+ @extensions = nil # initialize furthur down
168
230
  end
169
- @options[:header_footer] = @options.fetch(:header_footer, false)
170
231
 
171
- @attributes['encoding'] = 'UTF-8'
172
- @attributes['sectids'] = ''
173
- @attributes['notitle'] = '' unless @options[:header_footer]
174
- @attributes['toc-placement'] = 'auto'
175
- @attributes['stylesheet'] = ''
176
- @attributes['copycss'] = '' if @options[:header_footer]
177
- @attributes['prewrap'] = ''
178
- @attributes['attribute-undefined'] = COMPLIANCE[:attribute_undefined]
179
- @attributes['attribute-missing'] = COMPLIANCE[:attribute_missing]
232
+ @parsed = false
233
+ @header = nil
234
+ @counters = {}
235
+ @callouts = Callouts.new
236
+ @attributes_modified = ::Set.new
237
+ @options = options
238
+ header_footer = (options[:header_footer] ||= false)
239
+
240
+ attrs = @attributes
241
+ attrs['encoding'] = 'UTF-8'
242
+ attrs['sectids'] = ''
243
+ attrs['notitle'] = '' unless header_footer
244
+ attrs['toc-placement'] = 'auto'
245
+ attrs['stylesheet'] = ''
246
+ attrs['webfonts'] = ''
247
+ attrs['copycss'] = '' if header_footer
248
+ attrs['prewrap'] = ''
249
+ attrs['attribute-undefined'] = Compliance.attribute_undefined
250
+ attrs['attribute-missing'] = Compliance.attribute_missing
251
+ attrs['iconfont-remote'] = ''
180
252
 
181
253
  # language strings
182
254
  # TODO load these based on language settings
183
- @attributes['caution-caption'] = 'Caution'
184
- @attributes['important-caption'] = 'Important'
185
- @attributes['note-caption'] = 'Note'
186
- @attributes['tip-caption'] = 'Tip'
187
- @attributes['warning-caption'] = 'Warning'
188
- @attributes['appendix-caption'] = 'Appendix'
189
- @attributes['example-caption'] = 'Example'
190
- @attributes['figure-caption'] = 'Figure'
191
- #@attributes['listing-caption'] = 'Listing'
192
- @attributes['table-caption'] = 'Table'
193
- @attributes['toc-title'] = 'Table of Contents'
194
- @attributes['manname-title'] = 'NAME'
195
- @attributes['untitled-label'] = 'Untitled'
196
- @attributes['version-label'] = 'Version'
197
- @attributes['last-update-label'] = 'Last updated'
198
-
199
- @attribute_overrides['asciidoctor'] = ''
200
- @attribute_overrides['asciidoctor-version'] = VERSION
201
-
202
- safe_mode_name = SafeMode.constants.detect {|l| SafeMode.const_get(l) == @safe}.to_s.downcase
203
- @attribute_overrides['safe-mode-name'] = safe_mode_name
204
- @attribute_overrides["safe-mode-#{safe_mode_name}"] = ''
205
- @attribute_overrides['safe-mode-level'] = @safe
255
+ attrs['caution-caption'] = 'Caution'
256
+ attrs['important-caption'] = 'Important'
257
+ attrs['note-caption'] = 'Note'
258
+ attrs['tip-caption'] = 'Tip'
259
+ attrs['warning-caption'] = 'Warning'
260
+ attrs['appendix-caption'] = 'Appendix'
261
+ attrs['example-caption'] = 'Example'
262
+ attrs['figure-caption'] = 'Figure'
263
+ #attrs['listing-caption'] = 'Listing'
264
+ attrs['table-caption'] = 'Table'
265
+ attrs['toc-title'] = 'Table of Contents'
266
+ attrs['manname-title'] = 'NAME'
267
+ attrs['untitled-label'] = 'Untitled'
268
+ attrs['version-label'] = 'Version'
269
+ attrs['last-update-label'] = 'Last updated'
270
+
271
+ attr_overrides['asciidoctor'] = ''
272
+ attr_overrides['asciidoctor-version'] = VERSION
273
+
274
+ safe_mode_name = SafeMode.constants.detect {|l| SafeMode.const_get(l) == @safe }.to_s.downcase
275
+ attr_overrides['safe-mode-name'] = safe_mode_name
276
+ attr_overrides["safe-mode-#{safe_mode_name}"] = ''
277
+ attr_overrides['safe-mode-level'] = @safe
206
278
 
207
279
  # sync the embedded attribute w/ the value of options...do not allow override
208
- @attribute_overrides['embedded'] = @options[:header_footer] ? nil : ''
280
+ attr_overrides['embedded'] = header_footer ? nil : ''
209
281
 
210
282
  # the only way to set the max-include-depth attribute is via the document options
211
283
  # 64 is the AsciiDoc default
212
- @attribute_overrides['max-include-depth'] ||= 64
284
+ attr_overrides['max-include-depth'] ||= 64
213
285
 
214
286
  # the only way to enable uri reads is via the document options, disabled by default
215
- unless !@attribute_overrides['allow-uri-read'].nil?
216
- @attribute_overrides['allow-uri-read'] = nil
287
+ unless !attr_overrides['allow-uri-read'].nil?
288
+ attr_overrides['allow-uri-read'] = nil
217
289
  end
218
290
 
291
+ attr_overrides['user-home'] = USER_HOME
292
+
293
+ # legacy support for numbered attribute
294
+ attr_overrides['sectnums'] = attr_overrides.delete 'numbered' if attr_overrides.key? 'numbered'
295
+
219
296
  # if the base_dir option is specified, it overrides docdir as the root for relative paths
220
297
  # otherwise, the base_dir is the directory of the source file (docdir) or the current
221
298
  # directory of the input is a string
222
- if @options[:base_dir].nil?
223
- if @attribute_overrides['docdir']
224
- @base_dir = @attribute_overrides['docdir'] = File.expand_path(@attribute_overrides['docdir'])
299
+ if options[:base_dir]
300
+ @base_dir = attr_overrides['docdir'] = ::File.expand_path(options[:base_dir])
301
+ else
302
+ if attr_overrides['docdir']
303
+ @base_dir = attr_overrides['docdir'] = ::File.expand_path(attr_overrides['docdir'])
225
304
  else
226
305
  #warn 'asciidoctor: WARNING: setting base_dir is recommended when working with string documents' unless nested?
227
- @base_dir = @attribute_overrides['docdir'] = File.expand_path(Dir.pwd)
306
+ @base_dir = attr_overrides['docdir'] = ::File.expand_path(::Dir.pwd)
228
307
  end
229
- else
230
- @base_dir = @attribute_overrides['docdir'] = File.expand_path(@options[:base_dir])
231
308
  end
232
309
 
233
- # allow common attributes backend and doctype to be set using options hash
234
- unless @options[:backend].nil?
235
- @attribute_overrides['backend'] = @options[:backend].to_s
310
+ # allow common attributes backend and doctype to be set using options hash, coerce values to string
311
+ if (backend_val = options[:backend])
312
+ attr_overrides['backend'] = %(#{backend_val})
236
313
  end
237
314
 
238
- unless @options[:doctype].nil?
239
- @attribute_overrides['doctype'] = @options[:doctype].to_s
315
+ if (doctype_val = options[:doctype])
316
+ attr_overrides['doctype'] = %(#{doctype_val})
240
317
  end
241
318
 
242
319
  if @safe >= SafeMode::SERVER
243
320
  # restrict document from setting copycss, source-highlighter and backend
244
- @attribute_overrides['copycss'] ||= nil
245
- @attribute_overrides['source-highlighter'] ||= nil
246
- @attribute_overrides['backend'] ||= DEFAULT_BACKEND
321
+ attr_overrides['copycss'] ||= nil
322
+ attr_overrides['source-highlighter'] ||= nil
323
+ attr_overrides['backend'] ||= DEFAULT_BACKEND
247
324
  # restrict document from seeing the docdir and trim docfile to relative path
248
- if !@parent_document && @attribute_overrides.has_key?('docfile')
249
- @attribute_overrides['docfile'] = @attribute_overrides['docfile'][(@attribute_overrides['docdir'].length + 1)..-1]
325
+ if !parent_doc && attr_overrides.key?('docfile')
326
+ attr_overrides['docfile'] = attr_overrides['docfile'][(attr_overrides['docdir'].length + 1)..-1]
250
327
  end
251
- @attribute_overrides['docdir'] = ''
328
+ attr_overrides['docdir'] = ''
329
+ attr_overrides['user-home'] = '.'
252
330
  if @safe >= SafeMode::SECURE
253
331
  # assign linkcss (preventing css embedding) unless explicitly disabled from the commandline or API
254
332
  # effectively the same has "has key 'linkcss' and value == nil"
255
- unless @attribute_overrides.fetch('linkcss', '').nil?
256
- @attribute_overrides['linkcss'] = ''
333
+ unless attr_overrides.fetch('linkcss', '').nil?
334
+ attr_overrides['linkcss'] = ''
257
335
  end
258
336
  # restrict document from enabling icons
259
- @attribute_overrides['icons'] ||= nil
337
+ attr_overrides['icons'] ||= nil
260
338
  end
261
339
  end
262
340
 
263
- @attribute_overrides.delete_if {|key, val|
341
+ attr_overrides.delete_if do |key, val|
264
342
  verdict = false
265
343
  # a nil value undefines the attribute
266
344
  if val.nil?
267
- @attributes.delete(key)
268
- # a negative key (trailing !) undefines the attribute
269
- # NOTE already normalize above as key with nil value
270
- #elsif key.end_with? '!'
271
- # @attributes.delete(key[0..-2])
272
- # a negative key (leading !) undefines the attribute
273
- # NOTE already normalize above as key with nil value
274
- #elsif key.start_with? '!'
275
- # @attributes.delete(key[1..-1])
276
- # otherwise it's an attribute assignment
345
+ attrs.delete(key)
277
346
  else
278
347
  # a value ending in @ indicates this attribute does not override
279
348
  # an attribute with the same key in the document souce
280
- if val.is_a?(String) && val.end_with?('@')
349
+ if (val.is_a? ::String) && (val.end_with? '@')
281
350
  val = val.chop
282
351
  verdict = true
283
352
  end
284
- @attributes[key] = val
353
+ attrs[key] = val
285
354
  end
286
355
  verdict
287
- }
356
+ end
357
+
358
+ @compat_mode = true if attrs.key? 'compat-mode'
288
359
 
289
- if !@parent_document
360
+ if parent_doc
361
+ # setup default doctype (backend is fixed)
362
+ attrs['doctype'] ||= DEFAULT_DOCTYPE
363
+
364
+ # don't need to do the extra processing within our own document
365
+ # FIXME line info isn't reported correctly within include files in nested document
366
+ @reader = Reader.new data, options[:cursor]
367
+
368
+ # Now parse the lines in the reader into blocks
369
+ # Eagerly parse (for now) since a subdocument is not a publicly accessible object
370
+ Parser.parse @reader, self
371
+
372
+ # should we call rewind in some sort of post-parse function?
373
+ @callouts.rewind
374
+ @parsed = true
375
+ else
290
376
  # setup default backend and doctype
291
- @attributes['backend'] ||= DEFAULT_BACKEND
292
- @attributes['doctype'] ||= DEFAULT_DOCTYPE
293
- update_backend_attributes
377
+ attrs['backend'] ||= DEFAULT_BACKEND
378
+ attrs['doctype'] ||= DEFAULT_DOCTYPE
379
+ update_backend_attributes attrs['backend'], true
294
380
 
295
- #@attributes['indir'] = @attributes['docdir']
296
- #@attributes['infile'] = @attributes['docfile']
381
+ #attrs['indir'] = attrs['docdir']
382
+ #attrs['infile'] = attrs['docfile']
297
383
 
298
384
  # dynamic intrinstic attribute values
299
- now = Time.new
300
- @attributes['localdate'] ||= now.strftime('%Y-%m-%d')
301
- @attributes['localtime'] ||= now.strftime('%H:%M:%S %Z')
302
- @attributes['localdatetime'] ||= [@attributes['localdate'], @attributes['localtime']] * ' '
303
-
385
+ now = ::Time.now
386
+ localdate = (attrs['localdate'] ||= now.strftime('%Y-%m-%d'))
387
+ unless (localtime = attrs['localtime'])
388
+ begin
389
+ localtime = attrs['localtime'] = now.strftime('%H:%M:%S %Z')
390
+ rescue
391
+ localtime = attrs['localtime'] = now.strftime('%H:%M:%S')
392
+ end
393
+ end
394
+ attrs['localdatetime'] ||= %(#{localdate} #{localtime})
395
+
304
396
  # docdate, doctime and docdatetime should default to
305
397
  # localdate, localtime and localdatetime if not otherwise set
306
- @attributes['docdate'] ||= @attributes['localdate']
307
- @attributes['doctime'] ||= @attributes['localtime']
308
- @attributes['docdatetime'] ||= @attributes['localdatetime']
398
+ attrs['docdate'] ||= localdate
399
+ attrs['doctime'] ||= localtime
400
+ attrs['docdatetime'] ||= %(#{localdate} #{localtime})
309
401
 
310
402
  # fallback directories
311
- @attributes['stylesdir'] ||= '.'
312
- @attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', './images'), 'icons')
403
+ attrs['stylesdir'] ||= '.'
404
+ attrs['iconsdir'] ||= ::File.join(attrs.fetch('imagesdir', './images'), 'icons')
405
+
406
+ @extensions = if initialize_extensions
407
+ registry = if (ext_registry = options[:extensions_registry])
408
+ if (ext_registry.is_a? Extensions::Registry) ||
409
+ (::RUBY_ENGINE_JRUBY && (ext_registry.is_a? ::AsciidoctorJ::Extensions::ExtensionRegistry))
410
+ ext_registry
411
+ end
412
+ elsif (ext_block = options[:extensions]).is_a? ::Proc
413
+ Extensions.build_registry(&ext_block)
414
+ end
415
+ (registry ||= Extensions::Registry.new).activate self
416
+ end
313
417
 
314
- @extensions = initialize_extensions ? Extensions::Registry.new(self) : nil
315
- @reader = PreprocessorReader.new self, data, Asciidoctor::Reader::Cursor.new(@attributes['docfile'], @base_dir)
418
+ @reader = PreprocessorReader.new self, data, Reader::Cursor.new(attrs['docfile'], @base_dir)
419
+ end
420
+ end
316
421
 
317
- if @extensions && @extensions.preprocessors?
318
- @extensions.load_preprocessors(self).each do |processor|
319
- @reader = processor.process(@reader, @reader.lines) || @reader
422
+ # Public: Parse the AsciiDoc source stored in the {Reader} into an abstract syntax tree.
423
+ #
424
+ # If the data parameter is not nil, create a new {PreprocessorReader} and assigned it to the reader
425
+ # property of this object. Otherwise, continue with the reader that was created in {#initialize}.
426
+ # Pass the reader to {Parser.parse} to parse the source data into an abstract syntax tree.
427
+ #
428
+ # If parsing has already been performed, this method returns without performing any processing.
429
+ #
430
+ # data - The optional replacement AsciiDoc source data as a String or String Array. (default: nil)
431
+ #
432
+ # Returns this [Document]
433
+ def parse data = nil
434
+ if @parsed
435
+ self
436
+ else
437
+ doc = self
438
+ # create reader if data is provided (used when data is not known at the time the Document object is created)
439
+ @reader = PreprocessorReader.new doc, data, Reader::Cursor.new(@attributes['docfile'], @base_dir) if data
440
+
441
+ if (exts = @parent_document ? nil : @extensions) && exts.preprocessors?
442
+ exts.preprocessors.each do |ext|
443
+ @reader = ext.process_method[doc, @reader] || @reader
320
444
  end
321
445
  end
322
- else
323
- # don't need to do the extra processing within our own document
324
- # FIXME line info isn't reported correctly within include files in nested document
325
- @reader = Reader.new data, options[:cursor]
326
- end
327
446
 
328
- # Now parse the lines in the reader into blocks
329
- Lexer.parse(@reader, self, :header_only => @options.fetch(:parse_header_only, false))
447
+ # Now parse the lines in the reader into blocks
448
+ Parser.parse @reader, doc, :header_only => !!@options[:parse_header_only]
330
449
 
331
- @callouts.rewind
450
+ # should we call rewind in some sort of post-parse function?
451
+ @callouts.rewind
332
452
 
333
- if !@parent_document && @extensions && @extensions.treeprocessors?
334
- @extensions.load_treeprocessors(self).each do |processor|
335
- processor.process
453
+ if exts && exts.treeprocessors?
454
+ exts.treeprocessors.each do |ext|
455
+ if (result = ext.process_method[doc]) && Document === result && result != doc
456
+ doc = result
457
+ end
458
+ end
336
459
  end
460
+
461
+ @parsed = true
462
+ doc
337
463
  end
338
464
  end
339
465
 
@@ -344,15 +470,15 @@ class Document < AbstractBlock
344
470
  #
345
471
  # returns the next number in the sequence for the specified counter
346
472
  def counter(name, seed = nil)
347
- if !@counters.has_key? name
473
+ if (attr_is_seed = !(attr_val = @attributes[name]).nil_or_empty?) && @counters.key?(name)
474
+ @counters[name] = nextval(attr_val)
475
+ else
348
476
  if seed.nil?
349
- seed = nextval(@attributes.has_key?(name) ? @attributes[name] : 0)
477
+ seed = nextval(attr_is_seed ? attr_val : 0)
350
478
  elsif seed.to_i.to_s == seed
351
479
  seed = seed.to_i
352
480
  end
353
481
  @counters[name] = seed
354
- else
355
- @counters[name] = nextval(@counters[name])
356
482
  end
357
483
 
358
484
  (@attributes[name] = @counters[name])
@@ -378,7 +504,7 @@ class Document < AbstractBlock
378
504
  #
379
505
  # returns the next value in the sequence according to the current value's type
380
506
  def nextval(current)
381
- if current.is_a?(Integer)
507
+ if current.is_a?(::Integer)
382
508
  current + 1
383
509
  else
384
510
  intval = current.to_i
@@ -393,7 +519,7 @@ class Document < AbstractBlock
393
519
  def register(type, value)
394
520
  case type
395
521
  when :ids
396
- if value.is_a?(Array)
522
+ if value.is_a?(::Array)
397
523
  @references[:ids][value[0]] = (value[1] || '[' + value[0] + ']')
398
524
  else
399
525
  @references[:ids][value] = '[' + value + ']'
@@ -408,7 +534,7 @@ class Document < AbstractBlock
408
534
  end
409
535
 
410
536
  def footnotes?
411
- not @references[:footnotes].empty?
537
+ !@references[:footnotes].empty?
412
538
  end
413
539
 
414
540
  def footnotes
@@ -416,16 +542,16 @@ class Document < AbstractBlock
416
542
  end
417
543
 
418
544
  def nested?
419
- @parent_document ? true : false
545
+ !!@parent_document
420
546
  end
421
547
 
422
548
  def embedded?
423
549
  # QUESTION should this be !@options[:header_footer] ?
424
- @attributes.has_key? 'embedded'
550
+ @attributes.key? 'embedded'
425
551
  end
426
552
 
427
553
  def extensions?
428
- @extensions ? true : false
554
+ !!@extensions
429
555
  end
430
556
 
431
557
  # Make the raw source for the Document available.
@@ -439,11 +565,11 @@ class Document < AbstractBlock
439
565
  end
440
566
 
441
567
  def doctype
442
- @attributes['doctype']
568
+ @doctype ||= @attributes['doctype']
443
569
  end
444
570
 
445
571
  def backend
446
- @attributes['backend']
572
+ @backend ||= @attributes['backend']
447
573
  end
448
574
 
449
575
  def basebackend? base
@@ -460,18 +586,38 @@ class Document < AbstractBlock
460
586
  @header.title = title
461
587
  end
462
588
 
463
- # We need to be able to return some semblance of a title
464
- def doctitle(opts = {})
465
- if !(val = @attributes.fetch('title', '')).empty?
589
+ # Public: Resolves the primary title for the document
590
+ #
591
+ # Searches the locations to find the first non-empty
592
+ # value:
593
+ #
594
+ # * document-level attribute named title
595
+ # * header title (known as the document title)
596
+ # * title of the first section
597
+ # * document-level attribute named untitled-label (if :use_fallback option is set)
598
+ #
599
+ # If no value can be resolved, nil is returned.
600
+ #
601
+ # If the :partition attribute is specified, the value is parsed into an Document::Title object.
602
+ # If the :sanitize attribute is specified, XML elements are removed from the value.
603
+ #
604
+ # Returns the resolved title as a [Title] if the :partition option is passed or a [String] if not
605
+ # or nil if no value can be resolved.
606
+ def doctitle opts = {}
607
+ if !(val = @attributes['title'].nil_or_empty?)
466
608
  val = title
467
- elsif !(sect = first_section).nil? && sect.title?
609
+ elsif (sect = first_section) && sect.title?
468
610
  val = sect.title
611
+ elsif opts[:use_fallback] && (val = @attributes['untitled-label'])
612
+ # use val set in condition
469
613
  else
470
- return nil
614
+ return
471
615
  end
472
616
 
473
- if opts[:sanitize] && val.include?('<')
474
- val.gsub(/<[^>]+>/, '').tr_s(' ', ' ').strip
617
+ if opts[:partition]
618
+ Title.new val, opts
619
+ elsif opts[:sanitize] && val.include?('<')
620
+ val.gsub(XmlSanitizeRx, '').tr_s(' ', ' ').strip
475
621
  else
476
622
  val
477
623
  end
@@ -493,21 +639,26 @@ class Document < AbstractBlock
493
639
  end
494
640
 
495
641
  def notitle
496
- !@attributes.has_key?('showtitle') && @attributes.has_key?('notitle')
642
+ !@attributes.key?('showtitle') && @attributes.key?('notitle')
497
643
  end
498
644
 
499
645
  def noheader
500
- @attributes.has_key? 'noheader'
646
+ @attributes.key? 'noheader'
647
+ end
648
+
649
+ def nofooter
650
+ @attributes.key? 'nofooter'
501
651
  end
502
652
 
503
653
  # QUESTION move to AbstractBlock?
504
654
  def first_section
505
- has_header? ? @header : (@blocks || []).detect{|e| e.is_a? Section}
655
+ has_header? ? @header : (@blocks || []).detect{|e| e.context == :section }
506
656
  end
507
657
 
508
658
  def has_header?
509
659
  @header ? true : false
510
660
  end
661
+ alias :header? :has_header?
511
662
 
512
663
  # Public: Append a content Block to this Document.
513
664
  #
@@ -538,58 +689,75 @@ class Document < AbstractBlock
538
689
  # Internal: Branch the attributes so that the original state can be restored
539
690
  # at a future time.
540
691
  def save_attributes
541
- # enable toc and numbered by default in DocBook backend
692
+ # enable toc and sectnums (i.e., numbered) by default in DocBook backend
542
693
  # NOTE the attributes_modified should go away once we have a proper attribute storage & tracking facility
543
- if @attributes['basebackend'] == 'docbook'
544
- @attributes['toc'] = '' unless attribute_locked?('toc') || @attributes_modified.include?('toc')
545
- @attributes['numbered'] = '' unless attribute_locked?('numbered') || @attributes_modified.include?('numbered')
694
+ if (attrs = @attributes)['basebackend'] == 'docbook'
695
+ attrs['toc'] = '' unless attribute_locked?('toc') || @attributes_modified.include?('toc')
696
+ attrs['sectnums'] = '' unless attribute_locked?('sectnums') || @attributes_modified.include?('sectnums')
546
697
  end
547
698
 
548
- unless @attributes.has_key?('doctitle') || (val = doctitle).nil?
549
- @attributes['doctitle'] = val
699
+ unless attrs.key?('doctitle') || !(val = doctitle)
700
+ attrs['doctitle'] = val
550
701
  end
551
702
 
552
703
  # css-signature cannot be updated after header attributes are processed
553
- if !@id && @attributes.has_key?('css-signature')
554
- @id = @attributes['css-signature']
555
- end
704
+ @id = attrs['css-signature'] unless @id
556
705
 
557
- toc_val = @attributes['toc']
558
- toc2_val = @attributes['toc2']
559
- toc_position_val = @attributes['toc-position']
706
+ toc_position_val = if (toc_val = (attrs.delete('toc2') ? 'left' : attrs['toc']))
707
+ # toc-placement allows us to separate position from using fitted slot vs macro
708
+ (toc_placement = attrs.fetch('toc-placement', 'macro')) && toc_placement != 'auto' ? toc_placement : attrs['toc-position']
709
+ else
710
+ nil
711
+ end
560
712
 
561
- if (!toc_val.nil? && (toc_val != '' || toc_position_val.to_s != '')) || !toc2_val.nil?
713
+ if toc_val && (!toc_val.empty? || !toc_position_val.nil_or_empty?)
562
714
  default_toc_position = 'left'
715
+ # TODO rename toc2 to aside-toc
563
716
  default_toc_class = 'toc2'
564
- position = [toc_position_val, toc2_val, toc_val].find {|pos| pos.to_s != ''}
565
- position = default_toc_position if !position && !toc2_val.nil?
566
- @attributes['toc'] = ''
717
+ if !toc_position_val.nil_or_empty?
718
+ position = toc_position_val
719
+ elsif !toc_val.empty?
720
+ position = toc_val
721
+ else
722
+ position = default_toc_position
723
+ end
724
+ attrs['toc'] = ''
725
+ attrs['toc-placement'] = 'auto'
567
726
  case position
568
727
  when 'left', '<', '&lt;'
569
- @attributes['toc-position'] = 'left'
728
+ attrs['toc-position'] = 'left'
570
729
  when 'right', '>', '&gt;'
571
- @attributes['toc-position'] = 'right'
730
+ attrs['toc-position'] = 'right'
572
731
  when 'top', '^'
573
- @attributes['toc-position'] = 'top'
732
+ attrs['toc-position'] = 'top'
574
733
  when 'bottom', 'v'
575
- @attributes['toc-position'] = 'bottom'
576
- when 'center'
577
- @attributes.delete('toc2')
734
+ attrs['toc-position'] = 'bottom'
735
+ when 'preamble', 'macro'
736
+ attrs['toc-position'] = 'content'
737
+ attrs['toc-placement'] = position
738
+ default_toc_class = nil
739
+ else
740
+ attrs.delete 'toc-position'
578
741
  default_toc_class = nil
579
- default_toc_position = 'center'
580
742
  end
581
- @attributes['toc-class'] ||= default_toc_class if default_toc_class
582
- @attributes['toc-position'] ||= default_toc_position if default_toc_position
743
+ attrs['toc-class'] ||= default_toc_class if default_toc_class
583
744
  end
584
745
 
585
- @original_attributes = @attributes.dup
746
+ if attrs.key? 'compat-mode'
747
+ attrs['source-language'] = attrs['language'] if attrs.has_key? 'language'
748
+ @compat_mode = true
749
+ else
750
+ @compat_mode = false
751
+ end
752
+
753
+ @original_attributes = attrs.dup
586
754
 
587
755
  # unfreeze "flexible" attributes
588
756
  unless nested?
589
757
  FLEXIBLE_ATTRIBUTES.each do |name|
590
758
  # turning a flexible attribute off should be permanent
591
759
  # (we may need more config if that's not always the case)
592
- if @attribute_overrides.has_key?(name) && !@attribute_overrides[name].nil?
760
+ if @attribute_overrides.key?(name) && @attribute_overrides[name]
593
761
  @attribute_overrides.delete(name)
594
762
  end
595
763
  end
@@ -597,7 +765,10 @@ class Document < AbstractBlock
597
765
  end
598
766
 
599
767
  # Internal: Restore the attributes to the previously saved state
768
+ #--
769
+ # QUESTION should we restore attributes after parse?
600
770
  def restore_attributes
771
+ # QUESTION shouldn't this be a dup in case we convert again?
601
772
  @attributes = @original_attributes
602
773
  end
603
774
 
@@ -608,12 +779,15 @@ class Document < AbstractBlock
608
779
 
609
780
  # Internal: Replay attribute assignments at the block level
610
781
  def playback_attributes(block_attributes)
611
- if block_attributes.has_key? :attribute_entries
782
+ if block_attributes.key? :attribute_entries
612
783
  block_attributes[:attribute_entries].each do |entry|
784
+ name = entry.name
613
785
  if entry.negate
614
- @attributes.delete(entry.name)
786
+ @attributes.delete name
787
+ @compat_mode = false if name == 'compat-mode'
615
788
  else
616
- @attributes[entry.name] = entry.value
789
+ @attributes[name] = entry.value
790
+ @compat_mode = true if name == 'compat-mode'
617
791
  end
618
792
  end
619
793
  end
@@ -634,11 +808,15 @@ class Document < AbstractBlock
634
808
  if attribute_locked?(name)
635
809
  false
636
810
  else
637
- @attributes[name] = apply_attribute_value_subs(value)
638
- @attributes_modified << name
639
- if name == 'backend'
640
- update_backend_attributes()
811
+ case name
812
+ when 'backend'
813
+ update_backend_attributes apply_attribute_value_subs(value)
814
+ when 'doctype'
815
+ update_doctype_attributes apply_attribute_value_subs(value)
816
+ else
817
+ @attributes[name] = apply_attribute_value_subs(value)
641
818
  end
819
+ @attributes_modified << name
642
820
  true
643
821
  end
644
822
  end
@@ -666,124 +844,226 @@ class Document < AbstractBlock
666
844
  #
667
845
  # Returns true if the attribute is locked, false otherwise
668
846
  def attribute_locked?(name)
669
- @attribute_overrides.has_key?(name)
847
+ @attribute_overrides.key?(name)
670
848
  end
671
849
 
672
850
  # Internal: Apply substitutions to the attribute value
673
851
  #
674
- # If the value is an inline passthrough macro (e.g., pass:[text]), then
675
- # apply the substitutions defined on the macro to the text. Otherwise,
676
- # apply the verbatim substitutions to the value.
852
+ # If the value is an inline passthrough macro (e.g., pass:<subs>[value]),
853
+ # apply the substitutions defined in <subs> to the value, or leave the value
854
+ # unmodified if no substitutions are specified. If the value is not an
855
+ # inline passthrough macro, apply header substitutions to the value.
677
856
  #
678
857
  # value - The String attribute value on which to perform substitutions
679
858
  #
680
- # Returns The String value with substitutions performed.
859
+ # Returns The String value with substitutions performed
681
860
  def apply_attribute_value_subs(value)
682
- if value.match(REGEXP[:pass_macro_basic])
683
- # copy match for Ruby 1.8.7 compat
684
- m = $~
861
+ if (m = AttributeEntryPassMacroRx.match(value))
685
862
  if !m[1].empty?
686
863
  subs = resolve_pass_subs m[1]
687
- subs.empty? ? m[2] : apply_subs(m[2], subs)
864
+ subs.empty? ? m[2] : (apply_subs m[2], subs)
688
865
  else
689
866
  m[2]
690
867
  end
691
868
  else
692
- apply_header_subs(value)
869
+ apply_header_subs value
693
870
  end
694
871
  end
695
872
 
696
873
  # Public: Update the backend attributes to reflect a change in the selected backend
697
- def update_backend_attributes()
698
- backend = @attributes['backend']
699
- if BACKEND_ALIASES.has_key? backend
700
- backend = @attributes['backend'] = BACKEND_ALIASES[backend]
701
- end
702
- basebackend = backend.sub(REGEXP[:trailing_digit], '')
703
- page_width = DEFAULT_PAGE_WIDTHS[basebackend]
704
- if page_width
705
- @attributes['pagewidth'] = page_width
706
- else
707
- @attributes.delete('pagewidth')
708
- end
709
- @attributes["backend-#{backend}"] = ''
710
- @attributes['basebackend'] = basebackend
711
- @attributes["basebackend-#{basebackend}"] = ''
712
- # REVIEW cases for the next two assignments
713
- @attributes["#{backend}-#{@attributes['doctype']}"] = ''
714
- @attributes["#{basebackend}-#{@attributes['doctype']}"] = ''
715
- ext = DEFAULT_EXTENSIONS[basebackend] || '.html'
716
- @attributes['outfilesuffix'] = ext
717
- file_type = ext[1..-1]
718
- @attributes['filetype'] = file_type
719
- @attributes["filetype-#{file_type}"] = ''
720
- end
721
-
722
- def renderer(opts = {})
723
- return @renderer if @renderer
724
-
725
- render_options = {}
874
+ #
875
+ # This method also handles updating the related doctype attributes if the
876
+ # doctype attribute is assigned at the time this method is called.
877
+ def update_backend_attributes new_backend, force = false
878
+ if force || (new_backend && new_backend != @attributes['backend'])
879
+ attrs = @attributes
880
+ current_backend = attrs['backend']
881
+ current_basebackend = attrs['basebackend']
882
+ current_doctype = attrs['doctype']
883
+ if new_backend.start_with? 'xhtml'
884
+ attrs['htmlsyntax'] = 'xml'
885
+ new_backend = new_backend[1..-1]
886
+ elsif new_backend.start_with? 'html'
887
+ attrs['htmlsyntax'] = 'html'
888
+ end
889
+ if (resolved_name = BACKEND_ALIASES[new_backend])
890
+ new_backend = resolved_name
891
+ end
892
+ if current_backend
893
+ attrs.delete %(backend-#{current_backend})
894
+ if current_doctype
895
+ attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype})
896
+ end
897
+ end
898
+ if current_doctype
899
+ attrs[%(doctype-#{current_doctype})] = ''
900
+ attrs[%(backend-#{new_backend}-doctype-#{current_doctype})] = ''
901
+ end
902
+ attrs['backend'] = new_backend
903
+ attrs[%(backend-#{new_backend})] = ''
904
+ # (re)initialize converter
905
+ if (@converter = create_converter).is_a? Converter::BackendInfo
906
+ new_basebackend = @converter.basebackend
907
+ attrs['outfilesuffix'] = @converter.outfilesuffix unless attribute_locked? 'outfilesuffix'
908
+ new_filetype = @converter.filetype
909
+ else
910
+ new_basebackend = new_backend.sub TrailingDigitsRx, ''
911
+ # QUESTION should we be forcing the basebackend to html if unknown?
912
+ new_outfilesuffix = DEFAULT_EXTENSIONS[new_basebackend] || '.html'
913
+ new_filetype = new_outfilesuffix[1..-1]
914
+ attrs['outfilesuffix'] = new_outfilesuffix unless attribute_locked? 'outfilesuffix'
915
+ end
916
+ if (current_filetype = attrs['filetype'])
917
+ attrs.delete %(filetype-#{current_filetype})
918
+ end
919
+ attrs['filetype'] = new_filetype
920
+ attrs[%(filetype-#{new_filetype})] = ''
921
+ if (page_width = DEFAULT_PAGE_WIDTHS[new_basebackend])
922
+ attrs['pagewidth'] = page_width
923
+ else
924
+ attrs.delete 'pagewidth'
925
+ end
926
+ if new_basebackend != current_basebackend
927
+ if current_basebackend
928
+ attrs.delete %(basebackend-#{current_basebackend})
929
+ if current_doctype
930
+ attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype})
931
+ end
932
+ end
933
+ attrs['basebackend'] = new_basebackend
934
+ attrs[%(basebackend-#{new_basebackend})] = ''
935
+ attrs[%(basebackend-#{new_basebackend}-doctype-#{current_doctype})] = '' if current_doctype
936
+ end
937
+ # clear cached backend value
938
+ @backend = nil
939
+ end
940
+ end
726
941
 
727
- # Load up relevant Document @options
728
- if @options.has_key? :template_dir
729
- render_options[:template_dirs] = [@options[:template_dir]]
730
- elsif @options.has_key? :template_dirs
731
- render_options[:template_dirs] = @options[:template_dirs]
942
+ def update_doctype_attributes new_doctype
943
+ if new_doctype && new_doctype != @attributes['doctype']
944
+ attrs = @attributes
945
+ current_doctype = attrs['doctype']
946
+ current_backend = attrs['backend']
947
+ current_basebackend = attrs['basebackend']
948
+ if current_doctype
949
+ attrs.delete %(doctype-#{current_doctype})
950
+ attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype}) if current_backend
951
+ attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype}) if current_basebackend
952
+ end
953
+ attrs['doctype'] = new_doctype
954
+ attrs[%(doctype-#{new_doctype})] = ''
955
+ attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = '' if current_backend
956
+ attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = '' if current_basebackend
957
+ # clear cached doctype value
958
+ @doctype = nil
732
959
  end
733
-
734
- render_options[:template_cache] = @options.fetch(:template_cache, true)
735
- render_options[:backend] = @attributes.fetch('backend', 'html5')
736
- render_options[:template_engine] = @options[:template_engine]
737
- render_options[:eruby] = @options.fetch(:eruby, 'erb')
738
- render_options[:compact] = @options.fetch(:compact, false)
739
-
740
- # Override Document @option settings with options passed in
741
- render_options.merge! opts
960
+ end
742
961
 
743
- @renderer = Renderer.new(render_options)
962
+ # TODO document me
963
+ def create_converter
964
+ converter_opts = {}
965
+ converter_opts[:htmlsyntax] = @attributes['htmlsyntax']
966
+ template_dirs = if (template_dir = @options[:template_dir])
967
+ converter_opts[:template_dirs] = [template_dir]
968
+ elsif (template_dirs = @options[:template_dirs])
969
+ converter_opts[:template_dirs] = template_dirs
970
+ end
971
+ if template_dirs
972
+ converter_opts[:template_cache] = @options.fetch :template_cache, true
973
+ converter_opts[:template_engine] = @options[:template_engine]
974
+ converter_opts[:template_engine_options] = @options[:template_engine_options]
975
+ converter_opts[:eruby] = @options[:eruby]
976
+ end
977
+ converter_factory = if (converter = @options[:converter])
978
+ Converter::Factory.new ::Hash[backend, converter]
979
+ else
980
+ Converter::Factory.default false
981
+ end
982
+ # QUESTION should we honor the convert_opts?
983
+ # QUESTION should we pass through all options and attributes too?
984
+ #converter_opts.update opts
985
+ converter_factory.create backend, converter_opts
744
986
  end
745
987
 
746
- # Public: Render the Asciidoc document using the templates
747
- # loaded by Renderer. If a :template_dir is not specified,
748
- # or a template is missing, the renderer will fall back to
988
+ # Public: Convert the AsciiDoc document using the templates
989
+ # loaded by the Converter. If a :template_dir is not specified,
990
+ # or a template is missing, the converter will fall back to
749
991
  # using the appropriate built-in template.
750
- def render(opts = {})
992
+ def convert opts = {}
993
+ parse unless @parsed
751
994
  restore_attributes
752
- r = renderer(opts)
753
995
 
754
- # QUESTION should we add Preserializeprocessors? is it the right name?
755
- #if !@parent_document && @extensions && @extensions.preserializeprocessors?
756
- # @extensions.load_preserializeprocessors(self).each do |processor|
757
- # processor.process r
758
- # end
759
- #end
996
+ # QUESTION should we add processors that execute before conversion begins?
997
+ unless @converter
998
+ fail %(asciidoctor: FAILED: missing converter for backend '#{backend}'. Processing aborted.)
999
+ end
760
1000
 
761
1001
  if doctype == 'inline'
762
1002
  # QUESTION should we warn if @blocks.size > 0 and the first block is not a paragraph?
763
- if !(block = @blocks.first).nil? && block.content_model != :compound
1003
+ if (block = @blocks[0]) && block.content_model != :compound
764
1004
  output = block.content
765
1005
  else
766
1006
  output = ''
767
1007
  end
768
1008
  else
769
- output = @options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self)
1009
+ transform = ((opts.key? :header_footer) ? opts[:header_footer] : @options[:header_footer]) ? 'document' : 'embedded'
1010
+ output = @converter.convert self, transform
770
1011
  end
771
1012
 
772
- if !@parent_document && @extensions
773
- if @extensions.postprocessors?
774
- @extensions.load_postprocessors(self).each do |processor|
775
- output = processor.process output
1013
+ unless @parent_document
1014
+ if (exts = @extensions) && exts.postprocessors?
1015
+ exts.postprocessors.each do |ext|
1016
+ output = ext.process_method[self, output]
776
1017
  end
777
1018
  end
778
- @extensions.reset
779
1019
  end
780
1020
 
781
1021
  output
782
1022
  end
783
1023
 
1024
+ # Alias render to convert to maintain backwards compatibility
1025
+ alias :render :convert
1026
+
1027
+ # Public: Write the output to the specified file
1028
+ #
1029
+ # If the converter responds to :write, delegate the work of writing the file
1030
+ # to that method. Otherwise, write the output the specified file.
1031
+ def write output, target
1032
+ if @converter.is_a? Writer
1033
+ @converter.write output, target
1034
+ else
1035
+ if target.respond_to? :write
1036
+ target.write output.chomp
1037
+ # ensure there's a trailing endline
1038
+ target.write EOL
1039
+ else
1040
+ ::File.open(target, 'w') {|f| f.write output }
1041
+ end
1042
+ nil
1043
+ end
1044
+ end
1045
+
1046
+ =begin
1047
+ def convert_to target, opts = {}
1048
+ start = ::Time.now.to_f if (monitor = opts[:monitor])
1049
+ output = (r = converter opts).convert
1050
+ monitor[:convert] = ::Time.now.to_f - start if monitor
1051
+
1052
+ unless target.respond_to? :write
1053
+ @attributes['outfile'] = target = ::File.expand_path target
1054
+ @attributes['outdir'] = ::File.dirname target
1055
+ end
1056
+
1057
+ start = ::Time.now.to_f if monitor
1058
+ r.write output, target
1059
+ monitor[:write] = ::Time.now.to_f - start if monitor
1060
+
1061
+ output
1062
+ end
1063
+ =end
1064
+
784
1065
  def content
785
- # per AsciiDoc-spec, remove the title before rendering the body,
786
- # regardless of whether the header is rendered)
1066
+ # NOTE per AsciiDoc-spec, remove the title before converting the body
787
1067
  @attributes.delete('title')
788
1068
  super
789
1069
  end
@@ -814,22 +1094,28 @@ class Document < AbstractBlock
814
1094
 
815
1095
  content = nil
816
1096
 
817
- docinfo = @attributes.has_key?('docinfo')
818
- docinfo1 = @attributes.has_key?('docinfo1')
819
- docinfo2 = @attributes.has_key?('docinfo2')
1097
+ docinfo = @attributes.key?('docinfo')
1098
+ docinfo1 = @attributes.key?('docinfo1')
1099
+ docinfo2 = @attributes.key?('docinfo2')
820
1100
  docinfo_filename = "docinfo#{qualifier}#{ext}"
821
1101
  if docinfo1 || docinfo2
822
1102
  docinfo_path = normalize_system_path(docinfo_filename)
823
1103
  content = read_asset(docinfo_path)
824
- content = sub_attributes(content.lines.entries).join unless content.nil?
1104
+ unless content.nil?
1105
+ # FIXME normalize these lines!
1106
+ content.force_encoding ::Encoding::UTF_8 if FORCE_ENCODING
1107
+ content = sub_attributes(content.split EOL) * EOL
1108
+ end
825
1109
  end
826
1110
 
827
- if (docinfo || docinfo2) && @attributes.has_key?('docname')
1111
+ if (docinfo || docinfo2) && @attributes.key?('docname')
828
1112
  docinfo_path = normalize_system_path("#{@attributes['docname']}-#{docinfo_filename}")
829
1113
  content2 = read_asset(docinfo_path)
830
1114
  unless content2.nil?
831
- content2 = sub_attributes(content2.lines.entries).join
832
- content = content.nil? ? content2 : "#{content}\n#{content2}"
1115
+ # FIXME normalize these lines!
1116
+ content2.force_encoding ::Encoding::UTF_8 if FORCE_ENCODING
1117
+ content2 = sub_attributes(content2.split EOL) * EOL
1118
+ content = content.nil? ? content2 : "#{content}#{EOL}#{content2}"
833
1119
  end
834
1120
  end
835
1121
 
@@ -839,7 +1125,7 @@ class Document < AbstractBlock
839
1125
  end
840
1126
 
841
1127
  def to_s
842
- %[#{super.to_s} - #{doctitle}]
1128
+ %(#<#{self.class}@#{object_id} {doctype: #{doctype.inspect}, doctitle: #{(@header != nil ? @header.title : nil).inspect}, blocks: #{@blocks.size}}>)
843
1129
  end
844
1130
 
845
1131
  end