module Asciidoctor

def const_missing name

Returns the resolved constant, if resolved, otherwise nothing.

defined prior to it being loaded.
Asciidoctor::Extensions, except that the constant isn't recognized as
This method provides the same functionality as using autoload on

Otherwise, delegates to the super method.
Requires the Asciidoctor::Extensions module if the name is :Extensions.

Internal: Automatically load the Asciidoctor::Extensions module.
def const_missing name
  if name == :Extensions
    require_relative 'asciidoctor/extensions'
    Extensions
  else
    super
  end
end unless RUBY_ENGINE == 'opal'

def convert input, options = {}

file, otherwise the converted String
Returns the Document object if the converted String is written to a

See Asciidoctor::Document#initialize for details about options.
String and Array values are converted into a Hash.
options - a String, Array or Hash of options to control processing (default: {})
input - the String AsciiDoc source filename

default and the converted result is returned.
standalone document). Otherwise, the header and footer are not included by
included unless specified otherwise (writing to a file implies creating a
If the output is going to be written to a file, the header and footer are

outside of the Document#base_dir in safe mode, an IOError is raised.
written because the target directory does not exist, or because it falls
created unless the :mkdirs option is set to true. If the file cannot be
Document#base_dir. If the target directory does not exist, it will not be
path, it is resolved relative to :to_dir, if given, otherwise the
specified, the file is written to that file. If :to_file is not an absolute
corresponds to the backend format. Otherwise, if the :to_file option is
written to a file adjacent to the input file, having an extension that
If the :to_file option is true, and the input is a File, the output is

directory name, etc) gets assigned to attributes on the Document object.
closed afterwards by this method. Information about the file (filename,
input is a File, the object is expected to be opened for reading and is not
Accepts input as an IO (or StringIO), String or String Array object. If the

convert it to the specified backend format.
Public: Parse the AsciiDoc source input into an Asciidoctor::Document and
def convert input, options = {}
  (options = options.merge).delete :parse
  to_dir = options.delete :to_dir
  mkdirs = options.delete :mkdirs
  case (to_file = options.delete :to_file)
  when true, nil
    unless (write_to_target = to_dir)
      sibling_path = ::File.absolute_path input.path if ::File === input
    end
    to_file = nil
  when false
    to_file = nil
  when '/dev/null'
    return self.load input, options
  else
    options[:to_file] = write_to_target = to_file unless (stream_output = to_file.respond_to? :write)
  end
  unless options.key? :standalone
    if sibling_path || write_to_target
      options[:standalone] = true
    elsif options.key? :header_footer
      options[:standalone] = options[:header_footer]
    end
  end
  # NOTE outfile may be controlled by document attributes, so resolve outfile after loading
  if sibling_path
    options[:to_dir] = outdir = ::File.dirname sibling_path
  elsif write_to_target
    if to_dir
      if to_file
        options[:to_dir] = ::File.dirname ::File.expand_path ::File.join to_dir, to_file
      else
        options[:to_dir] = ::File.expand_path to_dir
      end
    elsif to_file
      options[:to_dir] = ::File.dirname ::File.expand_path to_file
    end
  end
  # NOTE :to_dir is always set when outputting to a file
  # NOTE :to_file option only passed if assigned an explicit path
  doc = self.load input, options
  if sibling_path # write to file in same directory
    outfile = ::File.join outdir, %(#{doc.attributes['docname']}#{doc.outfilesuffix})
    raise ::IOError, %(input file and output file cannot be the same: #{outfile}) if outfile == sibling_path
  elsif write_to_target # write to explicit file or directory
    working_dir = (options.key? :base_dir) ? (::File.expand_path options[:base_dir]) : ::Dir.pwd
    # QUESTION should the jail be the working_dir or doc.base_dir???
    jail = doc.safe >= SafeMode::SAFE ? working_dir : nil
    if to_dir
      outdir = doc.normalize_system_path(to_dir, working_dir, jail, target_name: 'to_dir', recover: false)
      if to_file
        outfile = doc.normalize_system_path(to_file, outdir, nil, target_name: 'to_dir', recover: false)
        # reestablish outdir as the final target directory (in the case to_file had directory segments)
        outdir = ::File.dirname outfile
      else
        outfile = ::File.join outdir, %(#{doc.attributes['docname']}#{doc.outfilesuffix})
      end
    elsif to_file
      outfile = doc.normalize_system_path(to_file, working_dir, jail, target_name: 'to_dir', recover: false)
      # establish outdir as the final target directory (in the case to_file had directory segments)
      outdir = ::File.dirname outfile
    end
    if ::File === input && outfile == (::File.absolute_path input.path)
      raise ::IOError, %(input file and output file cannot be the same: #{outfile})
    end
    if mkdirs
      Helpers.mkdir_p outdir
    else
      # NOTE we intentionally refer to the directory as it was passed to the API
      raise ::IOError, %(target directory does not exist: #{to_dir} (hint: set :mkdirs option)) unless ::File.directory? outdir
    end
  else # write to stream
    outfile = to_file
    outdir = nil
  end
  if outfile && !stream_output
    output = doc.convert 'outfile' => outfile, 'outdir' => outdir
  else
    output = doc.convert
  end
  if outfile
    doc.write output, outfile
    # NOTE document cannot control this behavior if safe >= SafeMode::SERVER
    # NOTE skip if stylesdir is a URI
    if !stream_output && doc.safe < SafeMode::SECURE && (doc.attr? 'linkcss') && (doc.attr? 'copycss') &&
        (doc.basebackend? 'html') && !((stylesdir = (doc.attr 'stylesdir')) && (Helpers.uriish? stylesdir))
      if (stylesheet = doc.attr 'stylesheet')
        if DEFAULT_STYLESHEET_KEYS.include? stylesheet
          copy_asciidoctor_stylesheet = true
        elsif !(Helpers.uriish? stylesheet)
          copy_user_stylesheet = true
        end
      end
      copy_syntax_hl_stylesheet = (syntax_hl = doc.syntax_highlighter) && (syntax_hl.write_stylesheet? doc)
      if copy_asciidoctor_stylesheet || copy_user_stylesheet || copy_syntax_hl_stylesheet
        stylesoutdir = doc.normalize_system_path(stylesdir, outdir, doc.safe >= SafeMode::SAFE ? outdir : nil)
        if mkdirs
          Helpers.mkdir_p stylesoutdir
        else
          raise ::IOError, %(target stylesheet directory does not exist: #{stylesoutdir} (hint: set :mkdirs option)) unless ::File.directory? stylesoutdir
        end
        if copy_asciidoctor_stylesheet
          Stylesheets.instance.write_primary_stylesheet stylesoutdir
        # FIXME should Stylesheets also handle the user stylesheet?
        elsif copy_user_stylesheet
          if (stylesheet_src = doc.attr 'copycss').empty?
            stylesheet_src = doc.normalize_system_path stylesheet
          else
            # NOTE in this case, copycss is a source location (but cannot be a URI)
            stylesheet_src = doc.normalize_system_path stylesheet_src
          end
          stylesheet_dest = doc.normalize_system_path stylesheet, stylesoutdir, (doc.safe >= SafeMode::SAFE ? outdir : nil)
          # NOTE don't warn if src can't be read and dest already exists (see #2323)
          if stylesheet_src != stylesheet_dest && (stylesheet_data = doc.read_asset stylesheet_src,
              warn_on_failure: !(::File.file? stylesheet_dest), label: 'stylesheet')
            ::File.write stylesheet_dest, stylesheet_data, mode: FILE_WRITE_MODE
          end
        end
        syntax_hl.write_stylesheet doc, stylesoutdir if copy_syntax_hl_stylesheet
      end
    end
    doc
  else
    output
  end
end

def convert_file filename, options = {}

file, otherwise the converted String
Returns the Document object if the converted String is written to a

See Asciidoctor::Document#initialize for details about options.
String and Array values are converted into a Hash.
options - a String, Array or Hash of options to control processing (default: {})
input - the String AsciiDoc source filename

Asciidoctor::Document and convert it to the specified backend format.
Public: Parse the contents of the AsciiDoc source file into an
def convert_file filename, options = {}
  ::File.open(filename, FILE_READ_MODE) {|file| self.convert file, options }
end

def load input, options = {}

Returns the Document

See {Document#initialize} for details about these options.
String and Array values are converted into a Hash.
options - a String, Array or Hash of options to control processing (default: {})
input - the AsciiDoc source as a IO, String or Array.

directory name, etc) gets assigned to attributes on the Document object.
closed afterwards by this method. Information about the file (filename,
input is a File, the object is expected to be opened for reading and is not
Accepts input as an IO (or StringIO), String or String Array object. If the

Public: Parse the AsciiDoc source input into a {Document}
def load input, options = {}
  options = options.merge
  if (timings = options[:timings])
    timings.start :read
  end
  if (logger = options[:logger]) && logger != LoggerManager.logger
    LoggerManager.logger = logger
  end
  if !(attrs = options[:attributes])
    attrs = {}
  elsif ::Hash === attrs
    attrs = attrs.merge
  elsif (defined? ::Java::JavaUtil::Map) && ::Java::JavaUtil::Map === attrs
    attrs = attrs.dup
  elsif ::Array === attrs
    attrs = {}.tap do |accum|
      attrs.each do |entry|
        k, _, v = entry.partition '='
        accum[k] = v
      end
    end
  elsif ::String === attrs
    # condense and convert non-escaped spaces to null, unescape escaped spaces, then split on null
    attrs = {}.tap do |accum|
      attrs.gsub(SpaceDelimiterRx, '\1' + NULL).gsub(EscapedSpaceRx, '\1').split(NULL).each do |entry|
        k, _, v = entry.partition '='
        accum[k] = v
      end
    end
  elsif (attrs.respond_to? :keys) && (attrs.respond_to? :[])
    # coerce attrs to a real Hash
    attrs = {}.tap {|accum| attrs.keys.each {|k| accum[k] = attrs[k] } }
  else
    raise ::ArgumentError, %(illegal type for attributes option: #{attrs.class.ancestors.join ' < '})
  end
  if ::File === input
    options[:input_mtime] = input.mtime
    # NOTE defer setting infile and indir until we get a better sense of their purpose
    # TODO cli checks if input path can be read and is file, but might want to add check to API too
    attrs['docfile'] = input_path = ::File.absolute_path input.path
    attrs['docdir'] = ::File.dirname input_path
    attrs['docname'] = Helpers.basename input_path, (attrs['docfilesuffix'] = ::File.extname input_path)
    source = input.read
  elsif input.respond_to? :read
    # NOTE tty, pipes & sockets can't be rewound, but can't be sniffed easily either
    # just fail the rewind operation silently to handle all cases
    input.rewind rescue nil
    source = input.read
  elsif ::String === input
    source = input
  elsif ::Array === input
    source = input.drop 0
  elsif input
    raise ::ArgumentError, %(unsupported input type: #{input.class})
  end
  if timings
    timings.record :read
    timings.start :parse
  end
  options[:attributes] = attrs
  doc = options[:parse] == false ? (Document.new source, options) : (Document.new source, options).parse
  timings.record :parse if timings
  doc
rescue => ex
  begin
    context = %(asciidoctor: FAILED: #{attrs['docfile'] || '<stdin>'}: Failed to load AsciiDoc document)
    if ex.respond_to? :exception
      # The original message must be explicitly preserved when wrapping a Ruby exception
      wrapped_ex = ex.exception %(#{context} - #{ex.message})
      # JRuby automatically sets backtrace; MRI did not until 2.6
      wrapped_ex.set_backtrace ex.backtrace
    else
      # Likely a Java exception class
      wrapped_ex = ex.class.new context, ex
      wrapped_ex.stack_trace = ex.stack_trace
    end
  rescue
    wrapped_ex = ex
  end
  raise wrapped_ex
end

def load_file filename, options = {}

Returns the Asciidoctor::Document

See Asciidoctor::Document#initialize for details about options.
String and Array values are converted into a Hash.
options - a String, Array or Hash of options to control processing (default: {})
input - the String AsciiDoc source filename

Public: Parse the contents of the AsciiDoc source file into an Asciidoctor::Document
def load_file filename, options = {}
  ::File.open(filename, FILE_READ_MODE) {|file| self.load file, options }
end