# frozen_string_literal: true
module Asciidoctor
class << self
# 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 to_file, to_dir
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') == '' || stylesheet_src == true
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.to_s
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')
if (stylesheet_outdir = ::File.dirname stylesheet_dest) != stylesoutdir && !(::File.directory? stylesheet_outdir)
if mkdirs
Helpers.mkdir_p stylesheet_outdir
else
raise ::IOError, %(target stylesheet directory does not exist: #{stylesheet_outdir} (hint: set :mkdirs option))
end
end
::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
# Deprecated: Use {Asciidoctor.convert_file} instead.
alias render_file convert_file
end
end