summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorDan Allen <dan.j.allen@gmail.com>2013-04-25 12:34:12 -0700
committerDan Allen <dan.j.allen@gmail.com>2013-04-25 12:34:12 -0700
commitdb3659b1b458f16f5d37d6002147fce441cd9209 (patch)
treeabe9b5178cab1b16a489b3c37968e3ef6ae8e3fc /lib
parent666a4f71ecbae62f8b98c69411e22bce59be14a9 (diff)
parent51b14a34ecb0178c914ced0f653b92adfb321713 (diff)
Merge pull request #276 from mojavelinux/default-stylesheet
resolves #76 add a default stylesheet
Diffstat (limited to 'lib')
-rwxr-xr-xlib/asciidoctor.rb99
-rw-r--r--lib/asciidoctor/abstract_node.rb161
-rw-r--r--lib/asciidoctor/backends/html5.rb19
-rw-r--r--lib/asciidoctor/cli/invoker.rb72
-rw-r--r--lib/asciidoctor/cli/options.rb6
-rw-r--r--lib/asciidoctor/document.rb42
-rw-r--r--lib/asciidoctor/errors.rb5
-rw-r--r--lib/asciidoctor/path_resolver.rb360
-rw-r--r--lib/asciidoctor/reader.rb2
9 files changed, 605 insertions, 161 deletions
diff --git a/lib/asciidoctor.rb b/lib/asciidoctor.rb
index 6f599e88..acd3493a 100755
--- a/lib/asciidoctor.rb
+++ b/lib/asciidoctor.rb
@@ -88,6 +88,9 @@ module Asciidoctor
end
+ # The root path of the Asciidoctor gem
+ ROOT_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+
# The default document type
# Can influence markup generated by render templates
DEFAULT_DOCTYPE = 'article'
@@ -95,6 +98,12 @@ module Asciidoctor
# The backend determines the format of the rendered output, default to html5
DEFAULT_BACKEND = 'html5'
+ DEFAULT_STYLESHEET_PATH = File.join(ROOT_PATH, 'stylesheets', 'asciidoctor.css')
+
+ DEFAULT_STYLESHEET_KEYS = ['', 'DEFAULT'].to_set
+
+ DEFAULT_STYLESHEET_NAME = File.basename(DEFAULT_STYLESHEET_PATH)
+
# Pointers to the preferred version for a given backend.
BACKEND_ALIASES = {
'html' => 'html5',
@@ -661,6 +670,10 @@ module Asciidoctor
#
# returns the Asciidoctor::Document
def self.load(input, options = {}, &block)
+ if (monitor = options.fetch(:monitor, false))
+ start = Time.now
+ end
+
lines = nil
if input.is_a?(File)
options[:attributes] ||= {}
@@ -686,7 +699,19 @@ module Asciidoctor
raise "Unsupported input type: #{input.class}"
end
- Document.new(lines, options, &block)
+ if monitor
+ read_time = Time.now - start
+ start = Time.now
+ end
+
+ doc = Document.new(lines, options, &block)
+ if monitor
+ parse_time = Time.now - start
+ monitor[:read] = read_time
+ monitor[:parse] = parse_time
+ monitor[:load] = read_time + parse_time
+ end
+ doc
end
# Public: Parse the contents of the AsciiDoc source file into an Asciidoctor::Document
@@ -732,18 +757,22 @@ module Asciidoctor
# see Asciidoctor::Document#initialize for details
# block - a callback block for handling include::[] directives
#
- # returns nothing if the rendered output String is written to a file,
- # otherwise the rendered output String is returned
+ # returns the Document object if the rendered result String is written to a
+ # file, otherwise the rendered result String
def self.render(input, options = {}, &block)
in_place = options.delete(:in_place) || false
to_file = options.delete(:to_file)
to_dir = options.delete(:to_dir)
mkdirs = options.delete(:mkdirs) || false
+ monitor = options.fetch(:monitor, false)
write_in_place = in_place && input.is_a?(File)
write_to_target = to_file || to_dir
+ stream_output = !to_file.nil? && to_file.respond_to?(:write)
- raise ArgumentError, ':in_place with input file must not accompany :to_dir or :to_file' if write_in_place && write_to_target
+ if write_in_place && write_to_target
+ raise ArgumentError, 'the option :in_place cannot be used with either the :to_dir or :to_file option'
+ end
if !options.has_key?(:header_footer) && (write_in_place || write_to_target)
options[:header_footer] = true
@@ -751,21 +780,26 @@ module Asciidoctor
doc = Asciidoctor.load(input, options, &block)
- if write_in_place
+ if to_file == '/dev/null'
+ return doc
+ elsif write_in_place
to_file = File.join(File.dirname(input.path), "#{doc.attributes['docname']}#{doc.attributes['outfilesuffix']}")
- elsif write_to_target
+ elsif !stream_output && write_to_target
+ working_dir = options.has_key?(:base_dir) ? File.expand_path(opts[:base_dir]) : File.expand_path(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
- to_dir = doc.normalize_asset_path(to_dir, 'to_dir', false)
+ to_dir = doc.normalize_system_path(to_dir, working_dir, jail, :target_name => 'to_dir', :recover => false)
if to_file
- # normalize again, to_file could have dirty bits
- to_file = doc.normalize_asset_path(File.expand_path(to_file, to_dir), 'to_file', false)
+ to_file = doc.normalize_system_path(to_file, to_dir, nil, :target_name => 'to_dir', :recover => false)
# reestablish to_dir as the final target directory (in the case to_file had directory segments)
to_dir = File.dirname(to_file)
else
to_file = File.join(to_dir, "#{doc.attributes['docname']}#{doc.attributes['outfilesuffix']}")
end
elsif to_file
- to_file = doc.normalize_asset_path(to_file, 'to_file', false)
+ to_file = doc.normalize_system_path(to_file, working_dir, jail, :target_name => 'to_dir', :recover => false)
+ # establish to_dir as the final target directory (in the case to_file had directory segments)
to_dir = File.dirname(to_file)
end
@@ -779,11 +813,44 @@ module Asciidoctor
end
end
+ start = Time.now if monitor
+ output = doc.render
+
+ if monitor
+ render_time = Time.now - start
+ monitor[:render] = render_time
+ monitor[:load_render] = monitor[:load] + render_time
+ end
+
if to_file
- File.open(to_file, 'w') {|file| file.write doc.render }
- nil
+ start = Time.now if monitor
+ if stream_output
+ to_file.write output
+ else
+ File.open(to_file, 'w') {|file| file.write output }
+ # these assignments primarily for testing, diagnostics or reporting
+ doc.attributes['outfile'] = outfile = File.expand_path(to_file)
+ doc.attributes['outdir'] = File.dirname(outfile)
+ end
+ if monitor
+ write_time = Time.now - start
+ monitor[:write] = write_time
+ monitor[:total] = monitor[:load_render] + write_time
+ end
+
+ # NOTE document cannot control this behavior if safe >= SafeMode::SERVER
+ if !stream_output && doc.attr?('copycss') &&
+ doc.attr?('linkcss') && DEFAULT_STYLESHEET_KEYS.include?(doc.attr('stylesheet'))
+ Helpers.require_library 'fileutils'
+ outdir = doc.attr('outdir')
+ stylesdir = doc.normalize_system_path(doc.attr('stylesdir'), outdir,
+ doc.safe >= SafeMode::SAFE ? outdir : nil)
+ FileUtils.mkdir_p stylesdir
+ FileUtils.cp DEFAULT_STYLESHEET_PATH, stylesdir, :preserve => true
+ end
+ doc
else
- doc.render
+ output
end
end
@@ -795,8 +862,8 @@ module Asciidoctor
# see Asciidoctor::Document#initialize for details
# block - a callback block for handling include::[] directives
#
- # returns nothing if the rendered output String is written to a file,
- # otherwise the rendered output String is returned
+ # returns the Document object if the rendered result String is written to a
+ # file, otherwise the rendered result String
def self.render_file(filename, options = {}, &block)
Asciidoctor.render(File.new(filename), options, &block)
end
@@ -824,10 +891,10 @@ module Asciidoctor
require 'asciidoctor/block'
require 'asciidoctor/callouts'
require 'asciidoctor/document'
- #require 'asciidoctor/errors'
require 'asciidoctor/inline'
require 'asciidoctor/lexer'
require 'asciidoctor/list_item'
+ require 'asciidoctor/path_resolver'
require 'asciidoctor/reader'
require 'asciidoctor/renderer'
require 'asciidoctor/section'
diff --git a/lib/asciidoctor/abstract_node.rb b/lib/asciidoctor/abstract_node.rb
index 84ab5757..d9d0c04b 100644
--- a/lib/asciidoctor/abstract_node.rb
+++ b/lib/asciidoctor/abstract_node.rb
@@ -177,7 +177,7 @@ class AbstractNode
if attr? 'icon'
image_uri(attr('icon'), nil)
else
- image_uri(name + '.' + @document.attr('icontype', 'png'), 'iconsdir')
+ image_uri("#{name}.#{@document.attr('icontype', 'png')}", 'iconsdir')
end
end
@@ -199,9 +199,9 @@ class AbstractNode
if target.include?(':') && target.match(Asciidoctor::REGEXP[:uri_sniff])
target
elsif asset_dir_key && attr?(asset_dir_key)
- File.join(@document.attr(asset_dir_key), target)
+ normalize_web_path(target, @document.attr(asset_dir_key))
else
- target
+ normalize_web_path(target)
end
end
@@ -230,9 +230,9 @@ class AbstractNode
elsif @document.safe < Asciidoctor::SafeMode::SECURE && @document.attr?('data-uri')
generate_data_uri(target_image, asset_dir_key)
elsif asset_dir_key && attr?(asset_dir_key)
- File.join(@document.attr(asset_dir_key), target_image)
+ normalize_web_path(target_image, @document.attr(asset_dir_key))
else
- target_image
+ normalize_web_path(target_image)
end
end
@@ -253,9 +253,17 @@ class AbstractNode
mimetype = 'image/' + File.extname(target_image)[1..-1]
if asset_dir_key
- image_path = File.join(normalize_asset_path(@document.attr(asset_dir_key, '.'), asset_dir_key), target_image)
+ #asset_dir_path = normalize_system_path(@document.attr(asset_dir_key), nil, nil, :target_name => asset_dir_key)
+ #image_path = normalize_system_path(target_image, asset_dir_path, nil, :target_name => 'image')
+ image_path = normalize_system_path(target_image, @document.attr(asset_dir_key), nil, :target_name => 'image')
else
- image_path = normalize_asset_path(target_image)
+ image_path = normalize_system_path(target_image)
+ end
+
+ if !File.readable? image_path
+ puts "asciidoctor: WARNING: image to embed not found or not readable: #{image_path}"
+ return "data:#{mimetype}:base64,"
+ #return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
end
bindata = nil
@@ -264,88 +272,79 @@ class AbstractNode
else
bindata = File.open(image_path, 'rb') {|file| file.read }
end
- 'data:' + mimetype + ';base64,' + Base64.encode64(bindata).delete("\n")
+ "data:#{mimetype};base64,#{Base64.encode64(bindata).delete("\n")}"
end
- # Public: Normalize the asset file or directory to a concrete and rinsed path
+ # Public: Read the contents of the file at the specified path.
+ # This method assumes that the path is safe to read. It checks
+ # that the file is readable before attempting to read it.
#
- # The most important functionality in this method is to prevent the asset
- # reference from resolving to a directory outside of the chroot directory
- # (which defaults to the directory of the source file, stored in the base_dir
- # instance variable on Document) if the document safe level is set to
- # SafeMode::SAFE or greater (a condition which is true by default).
- #
- # asset_ref - the String asset file or directory referenced in the document
- # or configuration attribute
- # asset_name - the String name of the file or directory being resolved (for use in
- # the warning message) (default: 'path')
- #
- # Examples
- #
- # # given these fixtures
- # document.base_dir
- # # => "/path/to/chroot"
- # document.safe >= Asciidoctor::SafeMode::SAFE
- # # => true
- #
- # # then
- # normalize_asset_path('images')
- # # => "/path/to/chroot/images"
- # normalize_asset_path('/etc/images')
- # # => "/path/to/chroot/images"
- # normalize_asset_path('../images')
- # # => "/path/to/chroot/images"
- #
- # # given these fixtures
- # document.base_dir
- # # => "/path/to/chroot"
- # document.safe >= Asciidoctor::SafeMode::SAFE
- # # => false
- #
- # # then
- # normalize_asset_path('images')
- # # => "/path/to/chroot/images"
- # normalize_asset_path('/etc/images')
- # # => "/etc/images"
- # normalize_asset_path('../images')
- # # => "/path/to/images"
- #
- # Returns The normalized asset file or directory as a String path
- #--
- # TODO this method is missing a coordinate; it should be able to resolve
- # both the directory reference and the path to an asset in it; callers
- # of this method are still doing a File.join to finish the task
- def normalize_asset_path(asset_ref, asset_name = 'path', autocorrect = true)
- # TODO we may use pathname enough to make it a top-level require
- Helpers.require_library 'pathname'
-
- input_path = @document.base_dir
- asset_path = Pathname.new(asset_ref)
-
- if asset_path.relative?
- asset_path = File.expand_path(File.join(input_path, asset_ref))
+ # path - the String path from which to read the contents
+ #
+ # returns the contents of the file at the specified path, or nil
+ # if the file does not exist.
+ def read_asset(path)
+ if File.readable? path
+ File.read path
else
- asset_path = asset_path.cleanpath.to_s
+ nil
end
+ end
- if @document.safe >= SafeMode::SAFE
- relative_asset_path = Pathname.new(asset_path).relative_path_from(Pathname.new(input_path)).to_s
- if relative_asset_path.start_with?('..')
- if autocorrect
- puts "asciidoctor: WARNING: #{asset_name} has illegal reference to ancestor of base directory"
- else
- raise SecurityError, "#{asset_name} has reference to path outside of base directory, disallowed in safe mode: #{asset_path}"
- end
- relative_asset_path.sub!(REGEXP[:leading_parent_dirs], '')
- # just to be absolutely sure ;)
- if relative_asset_path[0..0] == '.'
- raise 'Substitution of parent path references failed for ' + relative_asset_path
- end
- asset_path = File.expand_path(File.join(input_path, relative_asset_path))
- end
+ # Public: Normalize the web page using the PathResolver.
+ #
+ # See PathResolver::web_path(target, start) for details.
+ #
+ # target - the String target path
+ # start - the String start (i.e, parent) path (optional, default: nil)
+ #
+ # returns the resolved String path
+ def normalize_web_path(target, start = nil)
+ PathResolver.new.web_path(target, start)
+ end
+
+ # Public: Resolve and normalize a secure path from the target and start paths
+ # using the PathResolver.
+ #
+ # See PathResolver::system_path(target, start, jail, opts) for details.
+ #
+ # The most important functionality in this method is to prevent resolving a
+ # path outside of the jail (which defaults to the directory of the source
+ # file, stored in the base_dir instance variable on Document) if the document
+ # safe level is set to SafeMode::SAFE or greater (a condition which is true
+ # by default).
+ #
+ # target - the String target path
+ # start - the String start (i.e., parent) path
+ # jail - the String jail path to confine the resolved path
+ # opts - an optional Hash of options to control processing (default: {}):
+ # * :recover is used to control whether the processor should auto-recover
+ # when an illegal path is encountered
+ # * :target_name is used in messages to refer to the path being resolved
+ #
+ # raises a SecurityError if a jail is specified and the resolved path is
+ # outside the jail.
+ #
+ # returns a String path resolved from the start and target paths, with any
+ # parent references resolved and self references removed. If a jail is provided,
+ # this path will be guaranteed to be contained within the jail.
+ def normalize_system_path(target, start = nil, jail = nil, opts = {})
+ if start.nil?
+ start = @document.base_dir
end
+ if jail.nil? && @document.safe >= SafeMode::SAFE
+ jail = @document.base_dir
+ end
+ PathResolver.new.system_path(target, start, jail, opts)
+ end
- asset_path
+ # Public: Normalize the asset file or directory to a concrete and rinsed path
+ #
+ # Delegates to normalize_system_path, with the start path set to the value of
+ # the base_dir instance variable on the Document object.
+ def normalize_asset_path(asset_ref, asset_name = 'path', autocorrect = true)
+ normalize_system_path(asset_ref, @document.base_dir, nil,
+ :target_name => asset_name, :recover => autocorrect)
end
end
diff --git a/lib/asciidoctor/backends/html5.rb b/lib/asciidoctor/backends/html5.rb
index 5dd0a154..84f5d18b 100644
--- a/lib/asciidoctor/backends/html5.rb
+++ b/lib/asciidoctor/backends/html5.rb
@@ -71,8 +71,22 @@ class DocumentTemplate < BaseTemplate
<meta name="keywords" content="<%= attr :keywords %>">
<% end %>
<title><%= doctitle %></title>
- <% unless attr(:stylesheet, '').empty? %>
- <link rel="stylesheet" href="<%= (attr? :stylesdir) ? File.join((attr :stylesdir), (attr :stylesheet)) : (attr :stylesheet) %>">
+ <% if DEFAULT_STYLESHEET_KEYS.include?(attr 'stylesheet') %>
+ <% if attr? 'linkcss' %>
+ <link rel="stylesheet" href="<%= normalize_web_path(DEFAULT_STYLESHEET_NAME, (attr :stylesdir, '')) %>">
+ <% else %>
+ <style>
+<%= read_asset DEFAULT_STYLESHEET_PATH %>
+ </style>
+ <% end %>
+ <% elsif attr? :stylesheet %>
+ <% if attr? 'linkcss' %>
+ <link rel="stylesheet" href="<%= normalize_web_path(attr(:stylesdir, ''), (attr :stylesheet)) %>">
+ <% else %>
+ <style>
+<%= read_asset normalize_system_path(attr(:stylesheet), attr(:stylesdir, '')) %>
+ </style>
+ <% end %>
<% end %>
<% case attr 'source-highlighter' %><%
when 'coderay' %>
@@ -151,7 +165,6 @@ class EmbeddedTemplate < BaseTemplate
end
class BlockTocTemplate < BaseTemplate
- # id must be unique if this macro is used > once or when built-in one is present
def result(node)
doc = node.document
diff --git a/lib/asciidoctor/cli/invoker.rb b/lib/asciidoctor/cli/invoker.rb
index 3b1b3e30..7ef5ec75 100644
--- a/lib/asciidoctor/cli/invoker.rb
+++ b/lib/asciidoctor/cli/invoker.rb
@@ -5,14 +5,12 @@ module Asciidoctor
attr_reader :options
attr_reader :document
attr_reader :code
- attr_reader :timings
def initialize(*options)
@document = nil
@out = nil
@err = nil
@code = 0
- @timings = {}
options = options.flatten
if !options.empty? && options.first.is_a?(Cli::Options)
@options = options.first
@@ -32,46 +30,52 @@ module Asciidoctor
return if @options.nil?
begin
- @timings = {}
- infile = @options[:input_file]
- outfile = @options[:output_file]
+ opts = {}
+ monitor = {}
+ infile = nil
+ outfile = nil
+ @options.map {|k, v|
+ case k
+ when :input_file
+ infile = v
+ when :output_file
+ outfile = v
+ when :destination_dir
+ #opts[:to_dir] = File.expand_path(v) unless v.nil?
+ opts[:to_dir] = v unless v.nil?
+ when :attributes
+ opts[:attributes] = v.dup
+ when :verbose
+ opts[:monitor] = monitor if v
+ when :trace
+ # currently, nothing
+ else
+ opts[k] = v unless v.nil?
+ end
+ }
+
if infile == '-'
# allow use of block to supply stdin, particularly useful for tests
input = block_given? ? yield : STDIN
else
input = File.new(infile)
end
- start = Time.now
- @document = Asciidoctor.load(input, @options)
- timings[:parse] = Time.now - start
- start = Time.now
- output = @document.render
- timings[:render] = Time.now - start
- if @options[:verbose]
- puts "Time to read and parse source: #{timings[:parse]}"
- puts "Time to render document: #{timings[:render]}"
- puts "Total time to read, parse and render: #{timings.reduce(0) {|sum, (_, v)| sum += v}}"
- end
- if outfile == '/dev/null'
- # output nothing
- elsif outfile == '-' || (infile == '-' && (outfile.nil? || outfile.empty?))
- (@out || $stdout).puts output
+
+ if outfile == '-' || (infile == '-' && (outfile.to_s.empty? || outfile != '/dev/null'))
+ opts[:to_file] = (@out || $stdout)
+ elsif !outfile.nil?
+ opts[:to_file] = outfile
else
- if outfile.nil? || outfile.empty?
- if @options[:destination_dir]
- destination_dir = File.expand_path(@options[:destination_dir])
- else
- destination_dir = @document.base_dir
- end
- outfile = File.join(destination_dir, "#{@document.attributes['docname']}#{@document.attributes['outfilesuffix']}")
- else
- outfile = @document.normalize_asset_path outfile
- end
+ opts[:in_place] = true unless opts.has_key? :to_dir
+ end
- # this assignment is primarily for testing or other post analysis
- @document.attributes['outfile'] = outfile
- @document.attributes['outdir'] = File.dirname(outfile)
- File.open(outfile, 'w') {|file| file.write output }
+ @document = Asciidoctor.render(input, opts)
+
+ # FIXME this should be :monitor, :profile or :timings rather than :verbose
+ if @options[:verbose]
+ puts "Time to read and parse source: #{'%05.5f' % monitor[:parse]}"
+ puts "Time to render document: #{'%05.5f' % monitor[:render]}"
+ puts "Total time to read, parse and render: #{'%05.5f' % monitor[:load_render]}"
end
rescue Exception => e
raise e if @options[:trace] || SystemExit === e
diff --git a/lib/asciidoctor/cli/options.rb b/lib/asciidoctor/cli/options.rb
index eee4e413..58bbbb11 100644
--- a/lib/asciidoctor/cli/options.rb
+++ b/lib/asciidoctor/cli/options.rb
@@ -22,7 +22,7 @@ module Asciidoctor
self[:eruby] = options[:eruby] || nil
self[:compact] = options[:compact] || false
self[:verbose] = options[:verbose] || false
- self[:base_dir] = options[:base_dir] || nil
+ self[:base_dir] = options[:base_dir]
self[:destination_dir] = options[:destination_dir] || nil
self[:trace] = false
end
@@ -125,8 +125,8 @@ Example: asciidoctor -b html5 source.asciidoc
if self[:input_file].nil? || self[:input_file].empty?
$stderr.puts opts_parser
return 1
- elsif self[:input_file] != '-' && !File.exist?(self[:input_file])
- $stderr.puts "asciidoctor: FAILED: input file #{self[:input_file]} missing"
+ elsif self[:input_file] != '-' && !File.readable?(self[:input_file])
+ $stderr.puts "asciidoctor: FAILED: input file #{self[:input_file]} missing or cannot be read"
return 1
end
rescue OptionParser::MissingArgument
diff --git a/lib/asciidoctor/document.rb b/lib/asciidoctor/document.rb
index bbe6a02b..1cbbc717 100644
--- a/lib/asciidoctor/document.rb
+++ b/lib/asciidoctor/document.rb
@@ -127,13 +127,12 @@ class Document < AbstractBlock
@safe = @options.fetch(:safe, SafeMode::SECURE).to_i
@options[:header_footer] = @options.fetch(:header_footer, false)
- @attributes['asciidoctor'] = ''
- @attributes['asciidoctor-version'] = VERSION
@attributes['encoding'] = 'UTF-8'
@attributes['sectids'] = ''
@attributes['notitle'] = '' unless @options[:header_footer]
- @attributes['embedded'] = '' unless @options[:header_footer]
@attributes['toc-placement'] = 'auto'
+ @attributes['stylesheet'] = ''
+ @attributes['linkcss'] = ''
# language strings
# TODO load these based on language settings
@@ -150,8 +149,16 @@ class Document < AbstractBlock
@attributes['toc-title'] = 'Table of Contents'
# attribute overrides are attributes that can only be set from the commandline
+ # a direct assignment effectively makes the attribute a constant
+ # assigning a nil value will result in the attribute being unset
@attribute_overrides = options[:attributes] || {}
+ @attribute_overrides['asciidoctor'] = ''
+ @attribute_overrides['asciidoctor-version'] = VERSION
+
+ # sync the embedded attribute w/ the value of options...do not allow override
+ @attribute_overrides['embedded'] = @options[:header_footer] ? nil : ''
+
# the only way to set the include-depth attribute is via the document options
# 10 is the AsciiDoc default, though currently Asciidoctor only supports 1 level
@attribute_overrides['include-depth'] ||= 10
@@ -163,8 +170,8 @@ class Document < AbstractBlock
if @attribute_overrides['docdir']
@base_dir = @attribute_overrides['docdir'] = File.expand_path(@attribute_overrides['docdir'])
else
- # perhaps issue a warning here?
- @base_dir = @attribute_overrides['docdir'] = Dir.pwd
+ #puts 'asciidoctor: WARNING: setting base_dir is recommended when working with string documents' unless nested?
+ @base_dir = @attribute_overrides['docdir'] = File.expand_path(Dir.pwd)
end
else
@base_dir = @attribute_overrides['docdir'] = File.expand_path(options[:base_dir])
@@ -180,7 +187,9 @@ class Document < AbstractBlock
end
if @safe >= SafeMode::SERVER
- # restrict document from setting source-highlighter and backend
+ # restrict document from setting linkcss, copycss, source-highlighter and backend
+ @attribute_overrides['copycss'] ||= nil
+ @attribute_overrides['linkcss'] ||= ''
@attribute_overrides['source-highlighter'] ||= nil
@attribute_overrides['backend'] ||= DEFAULT_BACKEND
# restrict document from seeing the docdir and trim docfile to relative path
@@ -234,7 +243,9 @@ class Document < AbstractBlock
@attributes['docdate'] ||= @attributes['localdate']
@attributes['doctime'] ||= @attributes['localtime']
@attributes['docdatetime'] ||= @attributes['localdatetime']
-
+
+ # fallback directories
+ @attributes['stylesdir'] ||= '.'
@attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', './images'), 'icons')
# Now parse the lines in the reader into blocks
@@ -639,20 +650,15 @@ class Document < AbstractBlock
docinfo2 = @attributes.has_key?('docinfo2')
docinfo_filename = "docinfo#{ext}"
if docinfo1 || docinfo2
- docinfo_path = normalize_asset_path(docinfo_filename, 'shared docinfo file')
- if File.exist?(docinfo_path)
- content = File.read(docinfo_path)
- end
+ docinfo_path = normalize_system_path(docinfo_filename)
+ content = read_asset(docinfo_path)
end
if (docinfo || docinfo2) && @attributes.has_key?('docname')
- docinfo_path = normalize_asset_path("#{@attributes['docname']}-#{docinfo_filename}", 'document-specific docinfo file')
- if File.exist?(docinfo_path)
- if content.nil?
- content = File.read(docinfo_path)
- else
- content = [content, File.read(docinfo_path)] * "\n"
- end
+ docinfo_path = normalize_system_path("#{@attributes['docname']}-#{docinfo_filename}")
+ content2 = read_asset(docinfo_path)
+ unless content2.nil?
+ content = content.nil? ? content2 : "#{content}\n#{content2}"
end
end
diff --git a/lib/asciidoctor/errors.rb b/lib/asciidoctor/errors.rb
deleted file mode 100644
index acb577b0..00000000
--- a/lib/asciidoctor/errors.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Base project exception
-module Asciidoctor
-class ProjectError < StandardError; end
-end
-
diff --git a/lib/asciidoctor/path_resolver.rb b/lib/asciidoctor/path_resolver.rb
new file mode 100644
index 00000000..ed4f6abe
--- /dev/null
+++ b/lib/asciidoctor/path_resolver.rb
@@ -0,0 +1,360 @@
+module Asciidoctor
+# Public: Handles all operations for resolving, cleaning and joining paths.
+# This class includes operations for handling both web paths (request URIs) and
+# system paths.
+#
+# The main emphasis of the class is on creating clean and secure paths. Clean
+# paths are void of duplicate parent and current directory references in the
+# path name. Secure paths are paths which are restricted from accessing
+# directories outside of a jail root, if specified.
+#
+# Since joining two paths can result in an insecure path, this class also
+# handles the task of joining a parent (start) and child (target) path.
+#
+# This class makes no use of path utilities from the Ruby libraries. Instead,
+# it handles all aspects of path manipulation. The main benefit of
+# internalizing these operations is that the class is able to handle both posix
+# and windows paths independent of the operating system on which it runs. This
+# makes the class both deterministic and easier to test.
+#
+# Examples
+#
+# resolver = PathResolver.new
+#
+# # Web Paths
+#
+# resolver.web_path('images')
+# => 'images'
+#
+# resolver.web_path('./images')
+# => './images'
+#
+# resolver.web_path('/images')
+# => '/images'
+#
+# resolver.web_path('./images/../assets/images')
+# => './assets/images'
+#
+# resolver.web_path('/../images')
+# => '/images'
+#
+# resolver.web_path('images', 'assets')
+# => 'assets/images'
+#
+# resolver.web_path('tiger.png', '../assets/images')
+# => '../assets/images/tiger.png'
+#
+# # System Paths
+#
+# resolver.working_dir
+# => '/path/to/docs'
+#
+# resolver.system_path('images')
+# => '/path/to/docs/images'
+#
+# resolver.system_path('../images')
+# => '/path/to/images'
+#
+# resolver.system_path('/etc/images')
+# => '/etc/images'
+#
+# resolver.system_path('images', '/etc')
+# => '/etc/images'
+#
+# resolver.system_path('', '/etc/images')
+# => '/etc/images'
+#
+# resolver.system_path(nil, nil, '/path/to/docs')
+# => '/path/to/docs'
+#
+# resolver.system_path('..', nil, '/path/to/docs')
+# => '/path/to/docs'
+#
+# resolver.system_path('../../../css', nil, '/path/to/docs')
+# => '/path/to/docs/css'
+#
+# resolver.system_path('../../../css', '../../..', '/path/to/docs')
+# => '/path/to/docs/css'
+#
+# begin
+# resolver.system_path('../../../css', '../../..', '/path/to/docs', :recover => false)
+# rescue SecurityError => e
+# puts e.message
+# end
+# => 'path ../../../../../../css refers to location outside jail: /path/to/docs (disallowed in safe mode)'
+#
+# resolver.system_path('/path/to/docs/images', nil, '/path/to/docs')
+# => '/path/to/docs/images'
+#
+# begin
+# resolver.system_path('images', '/etc', '/path/to/docs')
+# rescue SecurityError => e
+# puts e.message
+# end
+# => Start path /etc is outside of jail: /path/to/docs'
+#
+class PathResolver
+ DOT = '.'
+ DOT_DOT = '..'
+ SLASH = '/'
+ BACKSLASH = '\\'
+ PARTITION_RE = /\/+/
+ WIN_ROOT_RE = /^[[:alpha:]]:(?:\\|\/)/
+
+ attr_accessor :file_separator
+ attr_accessor :working_dir
+
+ # Public: Construct a new instance of PathResolver, optionally specifying the
+ # path separator (to override the system default) and the working directory
+ # (to override the present working directory). The working directory will be
+ # expanded to an absolute path inside the constructor.
+ #
+ # file_separator - the String file separator to use for path operations
+ # (optional, default: File::FILE_SEPARATOR)
+ # working_dir - the String working directory (optional, default: Dir.pwd)
+ #
+ def initialize(file_separator = nil, working_dir = nil)
+ @file_separator = file_separator.nil? ? File::SEPARATOR : file_separator
+ if working_dir.nil?
+ @working_dir = File.expand_path(Dir.pwd)
+ else
+ @working_dir = is_root?(working_dir) ? working_dir : File.expand_path(working_dir)
+ end
+ end
+
+ # Public: Check if the specified path is an absolute root path
+ # This operation correctly handles both posix and windows paths.
+ #
+ # path - the String path to check
+ #
+ # returns a Boolean indicating whether the path is an absolute root path
+ def is_root?(path)
+ if @file_separator == BACKSLASH && path.match(WIN_ROOT_RE)
+ true
+ elsif path.start_with? SLASH
+ true
+ else
+ false
+ end
+ end
+
+ # Public: Determine if the path is an absolute (root) web path
+ #
+ # path - the String path to check
+ #
+ # returns a Boolean indicating whether the path is an absolute (root) web path
+ def is_web_root?(path)
+ path.start_with? SLASH
+ end
+
+ # Public: Normalize path by converting any backslashes to forward slashes
+ #
+ # path - the String path to normalize
+ #
+ # returns a String path with any backslashes replaced with forward slashes
+ def posixfy(path)
+ return '' if path.to_s.empty?
+ path.include?(BACKSLASH) ? path.tr(BACKSLASH, SLASH) : path
+ end
+
+ # Public: Expand the path by resolving any parent references (..)
+ # and cleaning self references (.).
+ #
+ # The result will be relative if the path is relative and
+ # absolute if the path is absolute. The file separator used
+ # in the expanded path is the one specified when the class
+ # was constructed.
+ #
+ # path - the String path to expand
+ #
+ # returns a String path with any parent or self references resolved.
+ def expand_path(path)
+ path_segments, path_root, _ = partition_path(path)
+ join_path(path_segments, path_root)
+ end
+
+ # Public: Partition the path into path segments and remove any empty segments
+ # or segments that are self references (.). The path is split on either posix
+ # or windows file separators.
+ #
+ # path - the String path to partition
+ # web_path - a Boolean indicating whether the path should be handled
+ # as a web path (optional, default: false)
+ #
+ # returns a 3-item Array containing the Array of String path segments, the
+ # path root, if the path is absolute, and the posix version of the path.
+ def partition_path(path, web_path = false)
+ posix_path = posixfy path
+ is_root = web_path ? is_web_root?(posix_path) : is_root?(posix_path)
+ path_segments = posix_path.split(PARTITION_RE)
+ # capture relative root
+ root = path_segments.first == DOT ? DOT : nil
+ path_segments.delete(DOT)
+ # capture absolute root, preserving relative root if set
+ root = is_root ? path_segments.shift : root
+
+ [path_segments, root, posix_path]
+ end
+
+ # Public: Join the segments using the file separator specified in the
+ # constructor. Use the root, if specified, to construct an absolute path.
+ # Otherwise join the segments as a relative path.
+ #
+ # segments - a String Array of path segments
+ # root - a String path root (optional, default: nil)
+ #
+ # returns a String path formed by joining the segments and prepending
+ # the root, if specified
+ def join_path(segments, root = nil)
+ if root
+ "#{root}#{@file_separator}#{segments * @file_separator}"
+ else
+ segments * @file_separator
+ end
+ end
+
+ # Public: Resolve a system path from the target and start paths. If a jail
+ # path is specified, enforce that the resolved directory is contained within
+ # the jail path. If a jail path is not provided, the resolved path may be
+ # any location on the system. If the resolved path is absolute, use it as is.
+ # If the resolved path is relative, resolve it relative to the working_dir
+ # specified in the constructor.
+ #
+ # target - the String target path
+ # start - the String start (i.e., parent) path
+ # jail - the String jail path to confine the resolved path
+ # opts - an optional Hash of options to control processing (default: {}):
+ # * :recover is used to control whether the processor should auto-recover
+ # when an illegal path is encountered
+ # * :target_name is used in messages to refer to the path being resolved
+ #
+ # returns a String path that joins the target path with the start path with
+ # any parent references resolved and self references removed and enforces
+ # that the resolved path be contained within the jail, if provided
+ def system_path(target, start, jail = nil, opts = {})
+ recover = opts.fetch(:recover, true)
+ unless jail.nil?
+ unless is_root? jail
+ raise SecurityError, "Jail is not an absolute path: #{jail}"
+ end
+ jail = posixfy jail
+ end
+
+ if target.to_s.empty?
+ target_segments = []
+ else
+ target_segments, target_root, _ = partition_path(target)
+ end
+
+ if target_segments.empty?
+ if start.to_s.empty?
+ return jail.nil? ? @working_dir : jail
+ elsif is_root? start
+ if jail.nil?
+ return expand_path start
+ end
+ else
+ return system_path(start, jail, jail)
+ end
+ end
+
+ if target_root && target_root != DOT
+ resolved_target = join_path target_segments, target_root
+ # if target is absolute and a sub-directory of jail, or
+ # a jail is not in place, let it slide
+ if jail.nil? || resolved_target.start_with?(jail)
+ return resolved_target
+ end
+ end
+
+ if start.to_s.empty?
+ start = jail.nil? ? @working_dir : jail
+ elsif is_root? start
+ start = posixfy start
+ else
+ start = system_path(start, jail, jail)
+ end
+
+ # both jail and start have been posixfied at this point
+ if jail == start
+ jail_segments, jail_root, _ = partition_path(jail)
+ start_segments = jail_segments.dup
+ elsif !jail.nil?
+ if !start.start_with?(jail)
+ raise SecurityError, "#{opts[:target_name] || 'Start path'} #{start} is outside of jail: #{jail} (disallowed in safe mode)"
+ end
+
+ start_segments, start_root, _ = partition_path(start)
+ jail_segments, jail_root, _ = partition_path(jail)
+
+ # Already checked for this condition
+ #if start_root != jail_root
+ # raise SecurityError, "Jail root #{jail_root} does not match root of #{opts[:target_name] || 'start path'}: #{start_root}"
+ #end
+ else
+ start_segments, start_root, _ = partition_path(start)
+ jail_root = start_root
+ end
+
+ resolved_segments = start_segments.dup
+ warned = false
+ target_segments.each do |segment|
+ if segment == DOT_DOT
+ if !jail.nil?
+ if resolved_segments.length > jail_segments.length
+ resolved_segments.pop
+ elsif !recover
+ raise SecurityError, "#{opts[:target_name] || 'path'} #{target} refers to location outside jail: #{jail} (disallowed in safe mode)"
+ elsif !warned
+ puts "asciidoctor: WARNING: #{opts[:target_name] || 'path'} has illegal reference to ancestor of jail, auto-recovering"
+ warned = true
+ end
+ else
+ resolved_segments.pop
+ end
+ else
+ resolved_segments.push segment
+ end
+ end
+
+ join_path resolved_segments, jail_root
+ end
+
+ # Public: Resolve a web path from the target and start paths.
+ # The main function of this operation is to resolve any parent
+ # references and remove any self references.
+ #
+ # target - the String target path
+ # start - the String start (i.e., parent) path
+ #
+ # returns a String path that joins the target path with the
+ # start path with any parent references resolved and self
+ # references removed
+ def web_path(target, start = nil)
+ target = posixfy(target)
+ start = posixfy(start)
+
+ unless is_web_root?(target) || start.empty?
+ target = "#{start}#{SLASH}#{target}"
+ end
+
+ target_segments, target_root, _ = partition_path(target, true)
+ resolved_segments = target_segments.inject([]) do |accum, segment|
+ if segment == DOT_DOT
+ if accum.empty?
+ accum.push segment unless target_root && target_root != DOT
+ elsif accum[-1] == DOT_DOT
+ accum.push segment
+ else
+ accum.pop
+ end
+ else
+ accum.push segment
+ end
+ accum
+ end
+
+ join_path resolved_segments, target_root
+ end
+end
+end
diff --git a/lib/asciidoctor/reader.rb b/lib/asciidoctor/reader.rb
index def2885b..e9e4f4e0 100644
--- a/lib/asciidoctor/reader.rb
+++ b/lib/asciidoctor/reader.rb
@@ -442,7 +442,7 @@ class Reader
elsif @document.attributes.fetch('include-depth', 0).to_i > 0
advance
# FIXME this borks line numbers
- include_file = @document.normalize_asset_path(target, 'include file')
+ include_file = @document.normalize_system_path(target, nil, nil, :target_name => 'include file')
if !File.file?(include_file)
puts "asciidoctor: WARNING: line #{@lineno}: include file not found: #{include_file}"
return true