# File lib/asciidoctor/document.rb, line 180
  def initialize data = nil, options = {}
    super self, :document

    if (parent_doc = options.delete :parent)
      @parent_document = parent_doc
      options[:base_dir] ||= parent_doc.base_dir
      @catalog = parent_doc.catalog.inject({}) do |accum, (key, table)|
        accum[key] = (key == :footnotes ? [] : table)
        accum
      end
      @callouts = parent_doc.callouts
      # QUESTION should we support setting attribute in parent document from nested document?
      # NOTE we must dup or else all the assignments to the overrides clobbers the real attributes
      @attribute_overrides = attr_overrides = parent_doc.attributes.dup
      parent_doctype = attr_overrides.delete 'doctype'
      attr_overrides.delete 'compat-mode'
      attr_overrides.delete 'toc'
      attr_overrides.delete 'toc-placement'
      attr_overrides.delete 'toc-position'
      @safe = parent_doc.safe
      @attributes['compat-mode'] = '' if (@compat_mode = parent_doc.compat_mode)
      @sourcemap = parent_doc.sourcemap
      @converter = parent_doc.converter
      initialize_extensions = false
      @extensions = parent_doc.extensions
    else
      @parent_document = nil
      @catalog = {
        :ids => {},
        :refs => {},
        :footnotes => [],
        :links => [],
        :images => [],
        :indexterms => [],
        :includes => ::Set.new,
      }
      @callouts = Callouts.new
      # copy attributes map and normalize keys
      # attribute overrides are attributes that can only be set from the commandline
      # a direct assignment effectively makes the attribute a constant
      # a nil value or name with leading or trailing ! will result in the attribute being unassigned
      attr_overrides = {}
      (options[:attributes] || {}).each do |key, value|
        if key.start_with? '!'
          key = key[1..-1]
          value = nil
        elsif key.end_with? '!'
          key = key.chop
          value = nil
        end
        attr_overrides[key.downcase] = value
      end
      @attribute_overrides = attr_overrides
      # safely resolve the safe mode from const, int or string
      if !(safe_mode = options[:safe])
        @safe = SafeMode::SECURE
      elsif ::Integer === safe_mode
        # be permissive in case API user wants to define new levels
        @safe = safe_mode
      else
        # NOTE: not using infix rescue for performance reasons, see https://github.com/jruby/jruby/issues/1816
        begin
          @safe = SafeMode.value_for_name safe_mode.to_s
        rescue
          @safe = SafeMode::SECURE
        end
      end
      @compat_mode = attr_overrides.key? 'compat-mode'
      @sourcemap = options[:sourcemap]
      @converter = nil
      initialize_extensions = defined? ::Asciidoctor::Extensions
      @extensions = nil # initialize furthur down
    end

    @parsed = false
    @header = nil
    @counters = {}
    @attributes_modified = ::Set.new
    @docinfo_processor_extensions = {}
    header_footer = (options[:header_footer] ||= false)
    (@options = options).freeze

    attrs = @attributes
    #attrs['encoding'] = 'UTF-8'
    attrs['sectids'] = ''
    attrs['toc-placement'] = 'auto'
    if header_footer
      attrs['copycss'] = ''
      # sync embedded attribute with :header_footer option value
      attr_overrides['embedded'] = nil
    else
      attrs['notitle'] = ''
      # sync embedded attribute with :header_footer option value
      attr_overrides['embedded'] = ''
    end
    attrs['stylesheet'] = ''
    attrs['webfonts'] = ''
    attrs['prewrap'] = ''
    attrs['attribute-undefined'] = Compliance.attribute_undefined
    attrs['attribute-missing'] = Compliance.attribute_missing
    attrs['iconfont-remote'] = ''

    # language strings
    # TODO load these based on language settings
    attrs['caution-caption'] = 'Caution'
    attrs['important-caption'] = 'Important'
    attrs['note-caption'] = 'Note'
    attrs['tip-caption'] = 'Tip'
    attrs['warning-caption'] = 'Warning'
    attrs['example-caption'] = 'Example'
    attrs['figure-caption'] = 'Figure'
    #attrs['listing-caption'] = 'Listing'
    attrs['table-caption'] = 'Table'
    attrs['toc-title'] = 'Table of Contents'
    #attrs['preface-title'] = 'Preface'
    attrs['manname-title'] = 'NAME'
    attrs['section-refsig'] = 'Section'
    #attrs['part-refsig'] = 'Part'
    attrs['chapter-refsig'] = 'Chapter'
    attrs['appendix-caption'] = attrs['appendix-refsig'] = 'Appendix'
    attrs['untitled-label'] = 'Untitled'
    attrs['version-label'] = 'Version'
    attrs['last-update-label'] = 'Last updated'

    attr_overrides['asciidoctor'] = ''
    attr_overrides['asciidoctor-version'] = VERSION

    attr_overrides['safe-mode-name'] = (safe_mode_name = SafeMode.name_for_value @safe)
    attr_overrides["safe-mode-#{safe_mode_name}"] = ''
    attr_overrides['safe-mode-level'] = @safe

    # the only way to set the max-include-depth attribute is via the API; default to 64 like AsciiDoc Python
    attr_overrides['max-include-depth'] ||= 64

    # the only way to set the allow-uri-read attribute is via the API; disabled by default
    attr_overrides['allow-uri-read'] ||= nil

    attr_overrides['user-home'] = USER_HOME

    # legacy support for numbered attribute
    attr_overrides['sectnums'] = attr_overrides.delete 'numbered' if attr_overrides.key? 'numbered'

    # if the base_dir option is specified, it overrides docdir as the root for relative paths
    # otherwise, the base_dir is the directory of the source file (docdir) or the current
    # directory of the input is a string
    if options[:base_dir]
      @base_dir = attr_overrides['docdir'] = ::File.expand_path(options[:base_dir])
    else
      if attr_overrides['docdir']
        @base_dir = attr_overrides['docdir'] = ::File.expand_path(attr_overrides['docdir'])
      else
        #warn 'asciidoctor: WARNING: setting base_dir is recommended when working with string documents' unless nested?
        @base_dir = attr_overrides['docdir'] = ::File.expand_path(::Dir.pwd)
      end
    end

    # allow common attributes backend and doctype to be set using options hash, coerce values to string
    if (backend_val = options[:backend])
      attr_overrides['backend'] = %(#{backend_val})
    end

    if (doctype_val = options[:doctype])
      attr_overrides['doctype'] = %(#{doctype_val})
    end

    if @safe >= SafeMode::SERVER
      # restrict document from setting copycss, source-highlighter and backend
      attr_overrides['copycss'] ||= nil
      attr_overrides['source-highlighter'] ||= nil
      attr_overrides['backend'] ||= DEFAULT_BACKEND
      # restrict document from seeing the docdir and trim docfile to relative path
      if !parent_doc && attr_overrides.key?('docfile')
        attr_overrides['docfile'] = attr_overrides['docfile'][(attr_overrides['docdir'].length + 1)..-1]
      end
      attr_overrides['docdir'] = ''
      attr_overrides['user-home'] = '.'
      if @safe >= SafeMode::SECURE
        attr_overrides['max-attribute-value-size'] = 4096 unless attr_overrides.key? 'max-attribute-value-size'
        # assign linkcss (preventing css embedding) unless explicitly disabled from the commandline or API
        # effectively the same has "has key 'linkcss' and value == nil"
        unless attr_overrides.fetch('linkcss', '').nil?
          attr_overrides['linkcss'] = ''
        end
        # restrict document from enabling icons
        attr_overrides['icons'] ||= nil
      end
    end

    # the only way to set the max-attribute-value-size attribute is via the API; disabled by default
    @max_attribute_value_size = (size = (attr_overrides['max-attribute-value-size'] ||= nil)) ? size.to_i.abs : nil

    attr_overrides.delete_if do |key, val|
      verdict = false
      # a nil value undefines the attribute
      if val.nil?
        attrs.delete(key)
      else
        # a value ending in @ indicates this attribute does not override
        # an attribute with the same key in the document souce
        if ::String === val && (val.end_with? '@')
          val = val.chop
          verdict = true
        end
        attrs[key] = val
      end
      verdict
    end

    if parent_doc
      @backend = attrs['backend']
      # reset doctype unless it matches the default value
      unless (@doctype = attrs['doctype'] = parent_doctype) == DEFAULT_DOCTYPE
        update_doctype_attributes DEFAULT_DOCTYPE
      end

      # don't need to do the extra processing within our own document
      # FIXME line info isn't reported correctly within include files in nested document
      @reader = Reader.new data, options[:cursor]

      # Now parse the lines in the reader into blocks
      # Eagerly parse (for now) since a subdocument is not a publicly accessible object
      Parser.parse @reader, self

      # should we call some sort of post-parse function?
      restore_attributes
      @parsed = true
    else
      # setup default backend and doctype
      @backend = nil
      if (attrs['backend'] ||= DEFAULT_BACKEND) == 'manpage'
        @doctype = attrs['doctype'] = attr_overrides['doctype'] = 'manpage'
      else
        @doctype = (attrs['doctype'] ||= DEFAULT_DOCTYPE)
      end
      update_backend_attributes attrs['backend'], true

      #attrs['indir'] = attrs['docdir']
      #attrs['infile'] = attrs['docfile']

      # dynamic intrinstic attribute values

      # See https://reproducible-builds.org/specs/source-date-epoch/
      # NOTE Opal can't call key? on ENV
      now = ::ENV['SOURCE_DATE_EPOCH'] ? ::Time.at(Integer ::ENV['SOURCE_DATE_EPOCH']).utc : ::Time.now
      if (localdate = attrs['localdate'])
        localyear = (attrs['localyear'] ||= ((localdate.index '-') == 4 ? (localdate.slice 0, 4) : nil))
      else
        localdate = attrs['localdate'] = (now.strftime '%Y-%m-%d')
        localyear = (attrs['localyear'] ||= now.year.to_s)
      end
      localtime = (attrs['localtime'] ||= begin
          now.strftime '%H:%M:%S %Z'
        rescue # Asciidoctor.js fails if timezone string has characters outside basic Latin (see asciidoctor.js#23)
          now.strftime '%H:%M:%S %z'
        end)
      attrs['localdatetime'] ||= %(#{localdate} #{localtime})

      # docdate, doctime and docdatetime should default to
      # localdate, localtime and localdatetime if not otherwise set
      attrs['docdate'] ||= localdate
      attrs['docyear'] ||= localyear
      attrs['doctime'] ||= localtime
      attrs['docdatetime'] ||= %(#{localdate} #{localtime})

      # fallback directories
      attrs['stylesdir'] ||= '.'
      attrs['iconsdir'] ||= ::File.join(attrs.fetch('imagesdir', './images'), 'icons')

      if initialize_extensions
        if (ext_registry = options[:extension_registry])
          # QUESTION should we warn the value type of the option is not a registry or boolean?
          unless Extensions::Registry === ext_registry || (::RUBY_ENGINE_JRUBY &&
              ::AsciidoctorJ::Extensions::ExtensionRegistry === ext_registry)
            ext_registry = Extensions::Registry.new
          end
        elsif ::Proc === (ext_block = options[:extensions])
          ext_registry = Extensions.create(&ext_block)
        else
          ext_registry = Extensions::Registry.new
        end
        @extensions = ext_registry.activate self
      end

      @reader = PreprocessorReader.new self, data, (Reader::Cursor.new attrs['docfile'], @base_dir), :normalize => true
    end
  end