asciidoctor 0.1.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of asciidoctor might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +209 -25
- data/{LICENSE → LICENSE.adoc} +4 -3
- data/README.adoc +392 -395
- data/Rakefile +94 -137
- data/benchmark/benchmark.rb +127 -0
- data/benchmark/sample-data/mdbasics.adoc +334 -0
- data/bin/asciidoctor +5 -8
- data/bin/asciidoctor-safe +4 -8
- data/compat/asciidoc.conf +78 -11
- data/compat/font-awesome-3-compat.css +397 -0
- data/data/stylesheets/asciidoctor-default.css +399 -0
- data/data/stylesheets/coderay-asciidoctor.css +89 -0
- data/features/open_block.feature +92 -0
- data/features/pass_block.feature +66 -0
- data/features/step_definitions.rb +42 -0
- data/features/text_formatting.feature +55 -0
- data/features/xref.feature +116 -0
- data/lib/asciidoctor.rb +1155 -605
- data/lib/asciidoctor/abstract_block.rb +157 -71
- data/lib/asciidoctor/abstract_node.rb +150 -93
- data/lib/asciidoctor/attribute_list.rb +85 -90
- data/lib/asciidoctor/block.rb +51 -24
- data/lib/asciidoctor/callouts.rb +4 -7
- data/lib/asciidoctor/cli.rb +3 -0
- data/lib/asciidoctor/cli/invoker.rb +86 -76
- data/lib/asciidoctor/cli/options.rb +111 -61
- data/lib/asciidoctor/converter.rb +232 -0
- data/lib/asciidoctor/converter/base.rb +58 -0
- data/lib/asciidoctor/converter/composite.rb +66 -0
- data/lib/asciidoctor/converter/docbook45.rb +94 -0
- data/lib/asciidoctor/converter/docbook5.rb +684 -0
- data/lib/asciidoctor/converter/factory.rb +225 -0
- data/lib/asciidoctor/converter/html5.rb +1081 -0
- data/lib/asciidoctor/converter/template.rb +296 -0
- data/lib/asciidoctor/core_ext.rb +7 -0
- data/lib/asciidoctor/core_ext/object/nil_or_empty.rb +23 -0
- data/lib/asciidoctor/core_ext/string/chr.rb +6 -0
- data/lib/asciidoctor/core_ext/symbol/length.rb +6 -0
- data/lib/asciidoctor/document.rb +590 -304
- data/lib/asciidoctor/extensions.rb +1100 -308
- data/lib/asciidoctor/helpers.rb +109 -46
- data/lib/asciidoctor/inline.rb +16 -9
- data/lib/asciidoctor/list.rb +23 -15
- data/lib/asciidoctor/opal_ext.rb +4 -0
- data/lib/asciidoctor/opal_ext/comparable.rb +38 -0
- data/lib/asciidoctor/opal_ext/dir.rb +13 -0
- data/lib/asciidoctor/opal_ext/error.rb +2 -0
- data/lib/asciidoctor/opal_ext/file.rb +125 -0
- data/lib/asciidoctor/{lexer.rb → parser.rb} +646 -455
- data/lib/asciidoctor/path_resolver.rb +141 -77
- data/lib/asciidoctor/reader.rb +257 -187
- data/lib/asciidoctor/section.rb +12 -16
- data/lib/asciidoctor/stylesheets.rb +91 -0
- data/lib/asciidoctor/substitutors.rb +1548 -0
- data/lib/asciidoctor/table.rb +73 -57
- data/lib/asciidoctor/timings.rb +39 -0
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +22 -14
- data/man/asciidoctor.adoc +18 -10
- data/test/attributes_test.rb +314 -14
- data/test/blocks_test.rb +763 -118
- data/test/converter_test.rb +352 -0
- data/test/document_test.rb +518 -199
- data/test/extensions_test.rb +273 -103
- data/test/fixtures/asciidoc_index.txt +27 -13
- data/test/fixtures/basic-docinfo.xml +1 -1
- data/test/fixtures/chapter-a.adoc +3 -0
- data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
- data/test/fixtures/docinfo.xml +1 -1
- data/test/fixtures/include-file.asciidoc +2 -0
- data/test/fixtures/master.adoc +5 -0
- data/test/invoker_test.rb +173 -61
- data/test/links_test.rb +97 -21
- data/test/lists_test.rb +181 -22
- data/test/options_test.rb +86 -2
- data/test/paragraphs_test.rb +47 -5
- data/test/{lexer_test.rb → parser_test.rb} +128 -57
- data/test/paths_test.rb +36 -1
- data/test/preamble_test.rb +25 -17
- data/test/reader_test.rb +404 -249
- data/test/sections_test.rb +623 -58
- data/test/substitutions_test.rb +609 -132
- data/test/tables_test.rb +198 -24
- data/test/test_helper.rb +101 -31
- data/test/text_test.rb +88 -31
- metadata +160 -64
- data/Gemfile +0 -12
- data/Guardfile +0 -18
- data/asciidoctor.gemspec +0 -143
- data/lib/asciidoctor/backends/_stylesheets.rb +0 -466
- data/lib/asciidoctor/backends/base_template.rb +0 -114
- data/lib/asciidoctor/backends/docbook45.rb +0 -774
- data/lib/asciidoctor/backends/docbook5.rb +0 -103
- data/lib/asciidoctor/backends/html5.rb +0 -1214
- data/lib/asciidoctor/renderer.rb +0 -259
- data/lib/asciidoctor/substituters.rb +0 -1083
- data/test/fixtures/asciidoc.txt +0 -105
- data/test/fixtures/ascshort.txt +0 -32
- data/test/fixtures/list_elements.asciidoc +0 -10
- 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,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
         | 
    
        data/lib/asciidoctor/document.rb
    CHANGED
    
    | @@ -1,6 +1,5 @@ | |
| 1 1 | 
             
            module Asciidoctor
         | 
| 2 | 
            -
            # Public: Methods for parsing  | 
| 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 | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 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 | 
| 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 | 
            -
                 | 
| 32 | 
            -
             | 
| 33 | 
            -
                 | 
| 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  | 
| 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:  | 
| 127 | 
            +
              # Public: Get the level-0 Section
         | 
| 81 128 | 
             
              attr_reader :header
         | 
| 82 129 |  | 
| 83 | 
            -
              # Public:  | 
| 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:  | 
| 136 | 
            +
              # Public: Get a reference to the parent Document of this nested document.
         | 
| 88 137 | 
             
              attr_reader :parent_document
         | 
| 89 138 |  | 
| 90 | 
            -
              # Public:  | 
| 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  | 
| 148 | 
            +
              # Public: Initialize a {Document} object.
         | 
| 94 149 | 
             
              #
         | 
| 95 | 
            -
              # data    - The  | 
| 96 | 
            -
              # options - A Hash of options to control processing | 
| 97 | 
            -
              #            | 
| 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. | 
| 103 | 
            -
              #   doc | 
| 104 | 
            -
              #   puts doc. | 
| 105 | 
            -
              def initialize | 
| 106 | 
            -
                super | 
| 107 | 
            -
             | 
| 108 | 
            -
                if options | 
| 109 | 
            -
                  @parent_document =  | 
| 110 | 
            -
                  options[: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 | 
            -
                   | 
| 114 | 
            -
                   | 
| 115 | 
            -
                   | 
| 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 =  | 
| 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 | 
            -
                   | 
| 125 | 
            -
             | 
| 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 | 
| 204 | 
            +
                    elsif key.end_with? '!'
         | 
| 205 | 
            +
                      key = key.chop
         | 
| 130 206 | 
             
                      value = nil
         | 
| 131 207 | 
             
                    end
         | 
| 132 | 
            -
                     | 
| 133 | 
            -
                    collector
         | 
| 208 | 
            +
                    attr_overrides[key.downcase] = value
         | 
| 134 209 | 
             
                  end
         | 
| 135 | 
            -
                  @ | 
| 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  | 
| 212 | 
            +
                  if !(safe_mode = options[:safe])
         | 
| 157 213 | 
             
                    @safe = SafeMode::SECURE
         | 
| 158 | 
            -
                  elsif safe_mode | 
| 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) | 
| 220 | 
            +
                      @safe = SafeMode.const_get(safe_mode.to_s.upcase)
         | 
| 164 221 | 
             
                    rescue
         | 
| 165 | 
            -
                      @safe = SafeMode::SECURE | 
| 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 | 
            -
                @ | 
| 172 | 
            -
                @ | 
| 173 | 
            -
                @ | 
| 174 | 
            -
                @ | 
| 175 | 
            -
                @ | 
| 176 | 
            -
                @ | 
| 177 | 
            -
                 | 
| 178 | 
            -
             | 
| 179 | 
            -
                 | 
| 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 | 
            -
                 | 
| 184 | 
            -
                 | 
| 185 | 
            -
                 | 
| 186 | 
            -
                 | 
| 187 | 
            -
                 | 
| 188 | 
            -
                 | 
| 189 | 
            -
                 | 
| 190 | 
            -
                 | 
| 191 | 
            -
                 | 
| 192 | 
            -
                 | 
| 193 | 
            -
                 | 
| 194 | 
            -
                 | 
| 195 | 
            -
                 | 
| 196 | 
            -
                 | 
| 197 | 
            -
                 | 
| 198 | 
            -
             | 
| 199 | 
            -
                 | 
| 200 | 
            -
                 | 
| 201 | 
            -
             | 
| 202 | 
            -
                safe_mode_name = SafeMode.constants.detect {|l| SafeMode.const_get(l) == @safe}.to_s.downcase
         | 
| 203 | 
            -
                 | 
| 204 | 
            -
                 | 
| 205 | 
            -
                 | 
| 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 | 
            -
                 | 
| 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 | 
            -
                 | 
| 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  | 
| 216 | 
            -
                   | 
| 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  | 
| 223 | 
            -
                   | 
| 224 | 
            -
             | 
| 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 =  | 
| 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 | 
            -
                 | 
| 235 | 
            -
                   | 
| 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 | 
            -
                 | 
| 239 | 
            -
                   | 
| 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 | 
            -
                   | 
| 245 | 
            -
                   | 
| 246 | 
            -
                   | 
| 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  | 
| 249 | 
            -
                     | 
| 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 | 
            -
                   | 
| 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  | 
| 256 | 
            -
                       | 
| 333 | 
            +
                    unless attr_overrides.fetch('linkcss', '').nil?
         | 
| 334 | 
            +
                      attr_overrides['linkcss'] = ''
         | 
| 257 335 | 
             
                    end
         | 
| 258 336 | 
             
                    # restrict document from enabling icons
         | 
| 259 | 
            -
                     | 
| 337 | 
            +
                    attr_overrides['icons'] ||= nil
         | 
| 260 338 | 
             
                  end
         | 
| 261 339 | 
             
                end
         | 
| 262 340 |  | 
| 263 | 
            -
                 | 
| 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 | 
            -
                     | 
| 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? | 
| 349 | 
            +
                    if (val.is_a? ::String) && (val.end_with? '@')
         | 
| 281 350 | 
             
                      val = val.chop
         | 
| 282 351 | 
             
                      verdict = true
         | 
| 283 352 | 
             
                    end
         | 
| 284 | 
            -
                     | 
| 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  | 
| 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 | 
            -
                   | 
| 292 | 
            -
                   | 
| 293 | 
            -
                  update_backend_attributes
         | 
| 377 | 
            +
                  attrs['backend'] ||= DEFAULT_BACKEND
         | 
| 378 | 
            +
                  attrs['doctype'] ||= DEFAULT_DOCTYPE
         | 
| 379 | 
            +
                  update_backend_attributes attrs['backend'], true
         | 
| 294 380 |  | 
| 295 | 
            -
                   | 
| 296 | 
            -
                   | 
| 381 | 
            +
                  #attrs['indir'] = attrs['docdir']
         | 
| 382 | 
            +
                  #attrs['infile'] = attrs['docfile']
         | 
| 297 383 |  | 
| 298 384 | 
             
                  # dynamic intrinstic attribute values
         | 
| 299 | 
            -
                  now = Time. | 
| 300 | 
            -
                   | 
| 301 | 
            -
                   | 
| 302 | 
            -
             | 
| 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 | 
            -
                   | 
| 307 | 
            -
                   | 
| 308 | 
            -
                   | 
| 398 | 
            +
                  attrs['docdate'] ||= localdate
         | 
| 399 | 
            +
                  attrs['doctime'] ||= localtime
         | 
| 400 | 
            +
                  attrs['docdatetime'] ||= %(#{localdate} #{localtime})
         | 
| 309 401 |  | 
| 310 402 | 
             
                  # fallback directories
         | 
| 311 | 
            -
                   | 
| 312 | 
            -
                   | 
| 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 | 
            -
                  @ | 
| 315 | 
            -
             | 
| 418 | 
            +
                  @reader = PreprocessorReader.new self, data, Reader::Cursor.new(attrs['docfile'], @base_dir)
         | 
| 419 | 
            +
                end
         | 
| 420 | 
            +
              end
         | 
| 316 421 |  | 
| 317 | 
            -
             | 
| 318 | 
            -
             | 
| 319 | 
            -
             | 
| 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 | 
            -
             | 
| 329 | 
            -
             | 
| 447 | 
            +
                  # Now parse the lines in the reader into blocks
         | 
| 448 | 
            +
                  Parser.parse @reader, doc, :header_only => !!@options[:parse_header_only]
         | 
| 330 449 |  | 
| 331 | 
            -
             | 
| 450 | 
            +
                  # should we call rewind in some sort of post-parse function?
         | 
| 451 | 
            +
                  @callouts.rewind
         | 
| 332 452 |  | 
| 333 | 
            -
             | 
| 334 | 
            -
             | 
| 335 | 
            -
             | 
| 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  | 
| 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( | 
| 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 | 
            -
                 | 
| 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 | 
            -
                 | 
| 545 | 
            +
                !!@parent_document
         | 
| 420 546 | 
             
              end
         | 
| 421 547 |  | 
| 422 548 | 
             
              def embedded?
         | 
| 423 549 | 
             
                # QUESTION should this be !@options[:header_footer] ?
         | 
| 424 | 
            -
                @attributes. | 
| 550 | 
            +
                @attributes.key? 'embedded'
         | 
| 425 551 | 
             
              end
         | 
| 426 552 |  | 
| 427 553 | 
             
              def extensions?
         | 
| 428 | 
            -
                 | 
| 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 | 
            -
              #  | 
| 464 | 
            -
               | 
| 465 | 
            -
             | 
| 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  | 
| 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 | 
| 614 | 
            +
                  return
         | 
| 471 615 | 
             
                end
         | 
| 472 616 |  | 
| 473 | 
            -
                if opts[: | 
| 474 | 
            -
                   | 
| 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. | 
| 642 | 
            +
                !@attributes.key?('showtitle') && @attributes.key?('notitle')
         | 
| 497 643 | 
             
              end
         | 
| 498 644 |  | 
| 499 645 | 
             
              def noheader
         | 
| 500 | 
            -
                @attributes. | 
| 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. | 
| 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 | 
            -
                   | 
| 545 | 
            -
                   | 
| 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  | 
| 549 | 
            -
                   | 
| 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 | 
            -
                 | 
| 554 | 
            -
                  @id = @attributes['css-signature']
         | 
| 555 | 
            -
                end
         | 
| 704 | 
            +
                @id = attrs['css-signature'] unless @id
         | 
| 556 705 |  | 
| 557 | 
            -
                toc_val =  | 
| 558 | 
            -
             | 
| 559 | 
            -
             | 
| 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  | 
| 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 | 
            -
                   | 
| 565 | 
            -
             | 
| 566 | 
            -
                   | 
| 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', '<', '<'
         | 
| 569 | 
            -
                     | 
| 728 | 
            +
                    attrs['toc-position'] = 'left'
         | 
| 570 729 | 
             
                  when 'right', '>', '>'
         | 
| 571 | 
            -
                     | 
| 730 | 
            +
                    attrs['toc-position'] = 'right'
         | 
| 572 731 | 
             
                  when 'top', '^'
         | 
| 573 | 
            -
                     | 
| 732 | 
            +
                    attrs['toc-position'] = 'top'
         | 
| 574 733 | 
             
                  when 'bottom', 'v'
         | 
| 575 | 
            -
                     | 
| 576 | 
            -
                  when ' | 
| 577 | 
            -
                     | 
| 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 | 
            -
                   | 
| 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 | 
            -
                 | 
| 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. | 
| 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. | 
| 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 | 
| 786 | 
            +
                      @attributes.delete name
         | 
| 787 | 
            +
                      @compat_mode = false if name == 'compat-mode'
         | 
| 615 788 | 
             
                    else
         | 
| 616 | 
            -
                      @attributes[ | 
| 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 | 
            -
                   | 
| 638 | 
            -
                   | 
| 639 | 
            -
             | 
| 640 | 
            -
             | 
| 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. | 
| 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 | 
| 675 | 
            -
              # apply the substitutions defined  | 
| 676 | 
            -
              #  | 
| 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  | 
| 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 | 
| 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 | 
| 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 | 
            -
               | 
| 698 | 
            -
             | 
| 699 | 
            -
             | 
| 700 | 
            -
             | 
| 701 | 
            -
                 | 
| 702 | 
            -
             | 
| 703 | 
            -
             | 
| 704 | 
            -
             | 
| 705 | 
            -
                   | 
| 706 | 
            -
             | 
| 707 | 
            -
             | 
| 708 | 
            -
             | 
| 709 | 
            -
             | 
| 710 | 
            -
             | 
| 711 | 
            -
             | 
| 712 | 
            -
             | 
| 713 | 
            -
             | 
| 714 | 
            -
             | 
| 715 | 
            -
             | 
| 716 | 
            -
             | 
| 717 | 
            -
             | 
| 718 | 
            -
             | 
| 719 | 
            -
             | 
| 720 | 
            -
             | 
| 721 | 
            -
             | 
| 722 | 
            -
             | 
| 723 | 
            -
             | 
| 724 | 
            -
             | 
| 725 | 
            -
             | 
| 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 | 
            -
             | 
| 728 | 
            -
                if @ | 
| 729 | 
            -
                   | 
| 730 | 
            -
             | 
| 731 | 
            -
                   | 
| 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 | 
            -
             | 
| 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:  | 
| 747 | 
            -
              # loaded by  | 
| 748 | 
            -
              # or a template is missing, the  | 
| 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  | 
| 992 | 
            +
              def convert opts = {}
         | 
| 993 | 
            +
                parse unless @parsed
         | 
| 751 994 | 
             
                restore_attributes
         | 
| 752 | 
            -
                r = renderer(opts)
         | 
| 753 995 |  | 
| 754 | 
            -
                # QUESTION should we add  | 
| 755 | 
            -
                 | 
| 756 | 
            -
             | 
| 757 | 
            -
                 | 
| 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  | 
| 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 | 
            -
                   | 
| 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 | 
            -
                 | 
| 773 | 
            -
                  if @extensions.postprocessors?
         | 
| 774 | 
            -
                     | 
| 775 | 
            -
                      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  | 
| 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. | 
| 818 | 
            -
                  docinfo1 = @attributes. | 
| 819 | 
            -
                  docinfo2 = @attributes. | 
| 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 | 
            -
                     | 
| 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. | 
| 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 | 
            -
                       | 
| 832 | 
            -
                       | 
| 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 | 
            -
                % | 
| 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
         |