lib/asciidoctor/convert.rb



module Asciidoctor
  module_function

  # Public: Parse the AsciiDoc source input into an Asciidoctor::Document and
  # convert it to the specified backend format.
  #
  # Accepts input as an IO (or StringIO), String or String Array object. If the
  # input is a File, the object is expected to be opened for reading and is not
  # closed afterwards by this method. Information about the file (filename,
  # directory name, etc) gets assigned to attributes on the Document object.
  #
  # If the :to_file option is true, and the input is a File, the output is
  # written to a file adjacent to the input file, having an extension that
  # corresponds to the backend format. Otherwise, if the :to_file option is
  # specified, the file is written to that file. If :to_file is not an absolute
  # path, it is resolved relative to :to_dir, if given, otherwise the
  # Document#base_dir. If the target directory does not exist, it will not be
  # created unless the :mkdirs option is set to true. If the file cannot be
  # written because the target directory does not exist, or because it falls
  # outside of the Document#base_dir in safe mode, an IOError is raised.
  #
  # If the output is going to be written to a file, the header and footer are
  # included unless specified otherwise (writing to a file implies creating a
  # standalone document). Otherwise, the header and footer are not included by
  # default and the converted result is returned.
  #
  # input   - the String AsciiDoc source filename
  # options - a String, Array or Hash of options to control processing (default: {})
  #           String and Array values are converted into a Hash.
  #           See Asciidoctor::Document#initialize for details about options.
  #
  # Returns the Document object if the converted String is written to a
  # file, otherwise the converted String
  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 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] = options.fetch :header_footer, 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 = 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

  # Public: Parse the contents of the AsciiDoc source file into an
  # Asciidoctor::Document and convert it to the specified backend format.
  #
  # input   - the String AsciiDoc source filename
  # options - a String, Array or Hash of options to control processing (default: {})
  #           String and Array values are converted into a Hash.
  #           See Asciidoctor::Document#initialize for details about options.
  #
  # Returns the Document object if the converted String is written to a
  # file, otherwise the converted String
  def convert_file filename, options = {}
    ::File.open(filename, FILE_READ_MODE) {|file| convert file, options }
  end

  # Deprecated: Use {Asciidoctor.convert} instead.
  alias render convert
  module_function :render

  # Deprecated: Use {Asciidoctor.convert_file} instead.
  alias render_file convert_file
  module_function :render_file
end