diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/asciidoctor.rb | 1 | ||||
| -rw-r--r-- | lib/asciidoctor/converter/template.rb | 15 | ||||
| -rw-r--r-- | lib/asciidoctor/document.rb | 334 |
3 files changed, 188 insertions, 162 deletions
diff --git a/lib/asciidoctor.rb b/lib/asciidoctor.rb index 77eaa654..7f4b5a4a 100644 --- a/lib/asciidoctor.rb +++ b/lib/asciidoctor.rb @@ -1236,6 +1236,7 @@ module Asciidoctor timings.start :parse end + options[:parse] = true unless options.key? :parse doc = Document.new lines, options timings.record :parse if timings doc diff --git a/lib/asciidoctor/converter/template.rb b/lib/asciidoctor/converter/template.rb index 765f5d0c..160f9772 100644 --- a/lib/asciidoctor/converter/template.rb +++ b/lib/asciidoctor/converter/template.rb @@ -248,15 +248,17 @@ module Asciidoctor name = name[6..-1] end ext_name = path_segments[-1] + template_class = ::Tilt + extra_engine_options = {} if ext_name == 'slim' # slim doesn't get loaded by Tilt, so we have to load it explicitly Helpers.require_library 'slim' unless defined? ::Slim elsif ext_name == 'erb' - eruby_loaded = load_eruby @eruby unless eruby_loaded + template_class, extra_engine_options = (eruby_loaded ||= load_eruby @eruby) end next unless ::Tilt.registered? ext_name unless template_cache && (template = template_cache[file]) - template = ::Tilt.new file, 1, @engine_options[ext_name.to_sym] + template = template_class.new file, 1, (@engine_options[ext_name.to_sym] || {}).merge(extra_engine_options) end result[name] = template end @@ -270,14 +272,15 @@ module Asciidoctor # # name - the String name of the eRuby implementation # - # Returns the eRuby implementation [Class] + # Returns an [Array] containing the Tilt template Class for the eRuby implementation + # and a Hash of additional options to pass to the initializer def load_eruby name if !name || name == 'erb' require 'erb' unless defined? ::ERB - ::ERB + [::Tilt::ERBTemplate, {}] elsif name == 'erubis' - Helpers.require_library 'erubis' unless defined? ::Erubis - ::Erubis::FastEruby + Helpers.require_library 'erubis' unless defined? ::Erubis::FastEruby + [::Tilt::ErubisTemplate, { :engine_class => ::Erubis::FastEruby }] else raise ::ArgumentError, %(Unknown ERB implementation: #{name}) end diff --git a/lib/asciidoctor/document.rb b/lib/asciidoctor/document.rb index e133c116..eb45358c 100644 --- a/lib/asciidoctor/document.rb +++ b/lib/asciidoctor/document.rb @@ -18,7 +18,7 @@ module Asciidoctor # nofooter - the footer block should not be shown class Document < AbstractBlock - Footnote = Struct.new :index, :id, :text + Footnote = ::Struct.new :index, :id, :text class AttributeEntry attr_reader :name, :value, :negate @@ -88,46 +88,49 @@ class Document < AbstractBlock # Public: A reference to the parent document of this nested document. attr_reader :parent_document + # Public: The Reader associated with this document + attr_reader :reader + # Public: The Converter associated with this document attr_reader :converter # Public: The extensions registry attr_reader :extensions - # Public: Initialize an Asciidoc object. + # Public: Initialize a {Document} object. # - # data - The Array of Strings holding the Asciidoc source document. (default: []) - # options - A Hash of options to control processing, such as setting the safe mode (:safe), - # suppressing the header/footer (:header_footer) and attribute overrides (:attributes) - # (default: {}) + # data - The AsciiDoc source data as a String or String Array. (default: nil) + # options - A Hash of options to control processing (e.g., safe mode value (:safe), backend (:backend), + # header/footer toggle (:header_footer), custom attributes (:attributes)). (default: {}) # # Examples # - # data = File.readlines(filename) - # doc = Asciidoctor::Document.new(data) + # data = File.read filename + # doc = Asciidoctor::Document.new data # puts doc.convert - def initialize(data = [], options = {}) - super(self, :document) + def initialize data = nil, options = {} + super self, :document - if options[:parent] - @parent_document = options.delete(:parent) - options[:base_dir] ||= @parent_document.base_dir - @references = @parent_document.references.inject({}) do |collector,(key,ref)| + if (parent_doc = options.delete :parent) + @parent_document = parent_doc + options[:base_dir] ||= parent_doc.base_dir + @references = parent_doc.references.inject({}) do |accum, (key,ref)| if key == :footnotes - collector[:footnotes] = [] + accum[:footnotes] = [] else - collector[key] = ref + accum[key] = ref end - collector + accum end # 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 = @parent_document.attributes.dup - @attribute_overrides.delete 'doctype' - @safe = @parent_document.safe - @converter = @parent_document.converter + attr_overrides = parent_doc.attributes.dup + attr_overrides.delete 'doctype' + @attribute_overrides = attr_overrides + @safe = parent_doc.safe + @converter = parent_doc.converter initialize_extensions = false - @extensions = @parent_document.extensions + @extensions = parent_doc.extensions else @parent_document = nil @references = { @@ -142,195 +145,192 @@ class Document < AbstractBlock # 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 - overrides = {} + attr_overrides = {} (options[:attributes] || {}).each do |key, value| - if key.start_with?('!') + if key.start_with? '!' key = key[1..-1] value = nil - elsif key.end_with?('!') + elsif key.end_with? '!' key = key.chop value = nil end - overrides[key.downcase] = value + attr_overrides[key.downcase] = value end - @attribute_overrides = overrides + @attribute_overrides = attr_overrides @safe = nil @converter = nil initialize_extensions = defined? ::Asciidoctor::Extensions @extensions = nil # initialize furthur down end + @parsed = false @header = nil @counters = {} @callouts = Callouts.new @attributes_modified = ::Set.new @options = options - unless @parent_document + unless parent_doc # safely resolve the safe mode from const, int or string - if !@safe && !(safe_mode = options[:safe]) + if !(safe_mode = options[:safe]) @safe = SafeMode::SECURE - elsif safe_mode.is_a?(::Fixnum) + elsif safe_mode.is_a? ::Fixnum # be permissive in case API user wants to define new levels @safe = safe_mode else - begin - @safe = SafeMode.const_get(safe_mode.to_s.upcase).to_i - rescue - @safe = SafeMode::SECURE.to_i - end + @safe = SafeMode.const_get(safe_mode.to_s.upcase).to_i rescue SafeMode::SECURE.to_i end end - options[:header_footer] ||= false + header_footer = (options[:header_footer] ||= false) - @attributes['encoding'] = 'UTF-8' - @attributes['sectids'] = '' - @attributes['notitle'] = '' unless options[:header_footer] - @attributes['toc-placement'] = 'auto' - @attributes['stylesheet'] = '' - @attributes['copycss'] = '' if options[:header_footer] - @attributes['prewrap'] = '' - @attributes['attribute-undefined'] = Compliance.attribute_undefined - @attributes['attribute-missing'] = Compliance.attribute_missing + attrs = @attributes + attrs['encoding'] = 'UTF-8' + attrs['sectids'] = '' + attrs['notitle'] = '' unless header_footer + attrs['toc-placement'] = 'auto' + attrs['stylesheet'] = '' + attrs['copycss'] = '' if header_footer + attrs['prewrap'] = '' + attrs['attribute-undefined'] = Compliance.attribute_undefined + attrs['attribute-missing'] = Compliance.attribute_missing # language strings # TODO load these based on language settings - @attributes['caution-caption'] = 'Caution' - @attributes['important-caption'] = 'Important' - @attributes['note-caption'] = 'Note' - @attributes['tip-caption'] = 'Tip' - @attributes['warning-caption'] = 'Warning' - @attributes['appendix-caption'] = 'Appendix' - @attributes['example-caption'] = 'Example' - @attributes['figure-caption'] = 'Figure' - #@attributes['listing-caption'] = 'Listing' - @attributes['table-caption'] = 'Table' - @attributes['toc-title'] = 'Table of Contents' - @attributes['manname-title'] = 'NAME' - @attributes['untitled-label'] = 'Untitled' - @attributes['version-label'] = 'Version' - @attributes['last-update-label'] = 'Last updated' - - @attribute_overrides['asciidoctor'] = '' - @attribute_overrides['asciidoctor-version'] = VERSION - - safe_mode_name = SafeMode.constants.detect {|l| SafeMode.const_get(l) == @safe}.to_s.downcase - @attribute_overrides['safe-mode-name'] = safe_mode_name - @attribute_overrides["safe-mode-#{safe_mode_name}"] = '' - @attribute_overrides['safe-mode-level'] = @safe + attrs['caution-caption'] = 'Caution' + attrs['important-caption'] = 'Important' + attrs['note-caption'] = 'Note' + attrs['tip-caption'] = 'Tip' + attrs['warning-caption'] = 'Warning' + attrs['appendix-caption'] = 'Appendix' + attrs['example-caption'] = 'Example' + attrs['figure-caption'] = 'Figure' + #attrs['listing-caption'] = 'Listing' + attrs['table-caption'] = 'Table' + attrs['toc-title'] = 'Table of Contents' + attrs['manname-title'] = 'NAME' + attrs['untitled-label'] = 'Untitled' + attrs['version-label'] = 'Version' + attrs['last-update-label'] = 'Last updated' + + attr_overrides['asciidoctor'] = '' + attr_overrides['asciidoctor-version'] = VERSION + + safe_mode_name = SafeMode.constants.detect {|l| SafeMode.const_get(l) == @safe }.to_s.downcase + attr_overrides['safe-mode-name'] = safe_mode_name + attr_overrides["safe-mode-#{safe_mode_name}"] = '' + attr_overrides['safe-mode-level'] = @safe # sync the embedded attribute w/ the value of options...do not allow override - @attribute_overrides['embedded'] = options[:header_footer] ? nil : '' + attr_overrides['embedded'] = header_footer ? nil : '' # the only way to set the max-include-depth attribute is via the document options # 64 is the AsciiDoc default - @attribute_overrides['max-include-depth'] ||= 64 + attr_overrides['max-include-depth'] ||= 64 # the only way to enable uri reads is via the document options, disabled by default - unless !@attribute_overrides['allow-uri-read'].nil? - @attribute_overrides['allow-uri-read'] = nil + unless !attr_overrides['allow-uri-read'].nil? + attr_overrides['allow-uri-read'] = nil end - @attribute_overrides['user-home'] = USER_HOME + attr_overrides['user-home'] = USER_HOME # 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] - if @attribute_overrides['docdir'] - @base_dir = @attribute_overrides['docdir'] = ::File.expand_path(@attribute_overrides['docdir']) + 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 = @attribute_overrides['docdir'] = ::File.expand_path(::Dir.pwd) + @base_dir = attr_overrides['docdir'] = ::File.expand_path(::Dir.pwd) end - else - @base_dir = @attribute_overrides['docdir'] = ::File.expand_path(options[:base_dir]) end - # allow common attributes backend and doctype to be set using options hash - if (value = options[:backend]) - @attribute_overrides['backend'] = %(#{value}) + # 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 (value = options[:doctype]) - @attribute_overrides['doctype'] = %(#{value}) + if (doctype_val = options[:doctype]) + attr_overrides['doctype'] = %(#{doctype_val}) end if @safe >= SafeMode::SERVER # restrict document from setting copycss, source-highlighter and backend - @attribute_overrides['copycss'] ||= nil - @attribute_overrides['source-highlighter'] ||= nil - @attribute_overrides['backend'] ||= DEFAULT_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_document && @attribute_overrides.has_key?('docfile') - @attribute_overrides['docfile'] = @attribute_overrides['docfile'][(@attribute_overrides['docdir'].length + 1)..-1] + if !parent_doc && attr_overrides.key?('docfile') + attr_overrides['docfile'] = attr_overrides['docfile'][(attr_overrides['docdir'].length + 1)..-1] end - @attribute_overrides['docdir'] = '' - @attribute_overrides['user-home'] = '.' + attr_overrides['docdir'] = '' + attr_overrides['user-home'] = '.' if @safe >= SafeMode::SECURE # assign linkcss (preventing css embedding) unless explicitly disabled from the commandline or API # effectively the same has "has key 'linkcss' and value == nil" - unless @attribute_overrides.fetch('linkcss', '').nil? - @attribute_overrides['linkcss'] = '' + unless attr_overrides.fetch('linkcss', '').nil? + attr_overrides['linkcss'] = '' end # restrict document from enabling icons - @attribute_overrides['icons'] ||= nil + attr_overrides['icons'] ||= nil end end - @attribute_overrides.delete_if {|key, val| + attr_overrides.delete_if do |key, val| verdict = false # a nil value undefines the attribute if val.nil? - @attributes.delete(key) - # a negative key (trailing !) undefines the attribute - # NOTE already normalize above as key with nil value - #elsif key.end_with? '!' - # @attributes.delete(key[0..-2]) - # a negative key (leading !) undefines the attribute - # NOTE already normalize above as key with nil value - #elsif key.start_with? '!' - # @attributes.delete(key[1..-1]) - # otherwise it's an attribute assignment + 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 val.is_a?(::String) && val.end_with?('@') + if (val.is_a? ::String) && (val.end_with? '@') val = val.chop verdict = true end - @attributes[key] = val + attrs[key] = val end verdict - } + end - if @parent_document + if parent_doc # 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 rewind in some sort of post-parse function? + @callouts.rewind + @parsed = true else # setup default backend and doctype - @attributes['backend'] ||= DEFAULT_BACKEND - @attributes['doctype'] ||= DEFAULT_DOCTYPE - update_backend_attributes @attributes['backend'], true + attrs['backend'] ||= DEFAULT_BACKEND + attrs['doctype'] ||= DEFAULT_DOCTYPE + update_backend_attributes attrs['backend'], true - #@attributes['indir'] = @attributes['docdir'] - #@attributes['infile'] = @attributes['docfile'] + #attrs['indir'] = attrs['docdir'] + #attrs['infile'] = attrs['docfile'] # dynamic intrinstic attribute values - now = Time.new - @attributes['localdate'] ||= now.strftime('%Y-%m-%d') - @attributes['localtime'] ||= now.strftime('%H:%M:%S %Z') - @attributes['localdatetime'] ||= %(#{@attributes['localdate']} #{@attributes['localtime']}) + now = ::Time.now + localdate = (attrs['localdate'] ||= now.strftime('%Y-%m-%d')) + localtime = (attrs['localtime'] ||= now.strftime('%H:%M:%S %Z')) + attrs['localdatetime'] ||= %(#{localdate} #{localtime}) # docdate, doctime and docdatetime should default to # localdate, localtime and localdatetime if not otherwise set - @attributes['docdate'] ||= @attributes['localdate'] - @attributes['doctime'] ||= @attributes['localtime'] - @attributes['docdatetime'] ||= @attributes['localdatetime'] + attrs['docdate'] ||= localdate + attrs['doctime'] ||= localtime + attrs['docdatetime'] ||= %(#{localdate} #{localtime}) # fallback directories - @attributes['stylesdir'] ||= '.' - @attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', './images'), 'icons') + attrs['stylesdir'] ||= '.' + attrs['iconsdir'] ||= ::File.join(attrs.fetch('imagesdir', './images'), 'icons') @extensions = if initialize_extensions registry = if (ext_registry = options[:extensions_registry]) @@ -338,31 +338,54 @@ class Document < AbstractBlock (::RUBY_ENGINE_JRUBY && (ext_registry.is_a? ::AsciidoctorJ::Extensions::ExtensionRegistry)) ext_registry end - elsif (ext_block = options[:extensions]) && (ext_block.is_a? ::Proc) + elsif (ext_block = options[:extensions]).is_a? ::Proc Extensions.build_registry(&ext_block) end (registry ||= Extensions::Registry.new).activate self end - @reader = PreprocessorReader.new self, data, Reader::Cursor.new(@attributes['docfile'], @base_dir) + @reader = PreprocessorReader.new self, data, Reader::Cursor.new(attrs['docfile'], @base_dir) + parse if data && options[:parse] + end + end + + # Public: Parse the AsciiDoc source stored in the {Reader} into an abstract syntax tree. + # + # If the data parameter is not nil, create a new {PreprocessorReader} and assigned it to the reader + # property of this object. Otherwise, continue with the reader that was created in {#initialize}. + # Pass the reader to {Parser.parse} to parse the source data into an abstract syntax tree. + # + # If parsing has already been performed, this method returns without performing any processing. + # + # data - The optional replacement AsciiDoc source data as a String or String Array. (default: nil) + # + # Returns this [Document] + def parse data = nil + unless @parsed + # create reader if data is provided (used when data is not known at the time the Document object is created) + @reader = PreprocessorReader.new self, data, Reader::Cursor.new(@attributes['docfile'], @base_dir) if data - if @extensions && @extensions.preprocessors? - @extensions.preprocessors.each do |ext| + if (exts = @parent_document ? nil : @extensions) && exts.preprocessors? + exts.preprocessors.each do |ext| @reader = ext.process_method[self, @reader] || @reader end end - end - # Now parse the lines in the reader into blocks - Parser.parse @reader, self, :header_only => !!options[:parse_header_only] + # Now parse the lines in the reader into blocks + Parser.parse @reader, self, :header_only => !!@options[:parse_header_only] - @callouts.rewind + # should we call rewind in some sort of post-parse function? + @callouts.rewind - if @extensions && !@parent_document && @extensions.treeprocessors? - @extensions.treeprocessors.each do |ext| - ext.process_method[self] + if exts && exts.treeprocessors? + exts.treeprocessors.each do |ext| + ext.process_method[self] + end end + + @parsed = true end + self end # Public: Get the named counter and take the next number in the sequence. @@ -372,7 +395,7 @@ class Document < AbstractBlock # # returns the next number in the sequence for the specified counter def counter(name, seed = nil) - if (attr_is_seed = !(attr_val = @attributes[name]).nil_or_empty?) && @counters.has_key?(name) + if (attr_is_seed = !(attr_val = @attributes[name]).nil_or_empty?) && @counters.key?(name) @counters[name] = nextval(attr_val) else if seed.nil? @@ -436,7 +459,7 @@ class Document < AbstractBlock end def footnotes? - not @references[:footnotes].empty? + !@references[:footnotes].empty? end def footnotes @@ -449,7 +472,7 @@ class Document < AbstractBlock def embedded? # QUESTION should this be !@options[:header_footer] ? - @attributes.has_key? 'embedded' + @attributes.key? 'embedded' end def extensions? @@ -521,15 +544,15 @@ class Document < AbstractBlock end def notitle - !@attributes.has_key?('showtitle') && @attributes.has_key?('notitle') + !@attributes.key?('showtitle') && @attributes.key?('notitle') end def noheader - @attributes.has_key? 'noheader' + @attributes.key? 'noheader' end def nofooter - @attributes.has_key? 'nofooter' + @attributes.key? 'nofooter' end # QUESTION move to AbstractBlock? @@ -578,14 +601,12 @@ class Document < AbstractBlock @attributes['numbered'] = '' unless attribute_locked?('numbered') || @attributes_modified.include?('numbered') end - unless @attributes.has_key?('doctitle') || !(val = doctitle) + unless @attributes.key?('doctitle') || !(val = doctitle) @attributes['doctitle'] = val end # css-signature cannot be updated after header attributes are processed - if !@id && @attributes.has_key?('css-signature') - @id = @attributes['css-signature'] - end + @id = @attributes['css-signature'] unless @id toc_val = @attributes['toc'] toc2_val = @attributes['toc2'] @@ -627,7 +648,7 @@ class Document < AbstractBlock FLEXIBLE_ATTRIBUTES.each do |name| # turning a flexible attribute off should be permanent # (we may need more config if that's not always the case) - if @attribute_overrides.has_key?(name) && !@attribute_overrides[name].nil? + if @attribute_overrides.key?(name) && !@attribute_overrides[name].nil? @attribute_overrides.delete(name) end end @@ -647,8 +668,8 @@ class Document < AbstractBlock # Internal: Replay attribute assignments at the block level def playback_attributes(block_attributes) - if (entries = block_attributes[:attribute_entries]) - entries.each do |entry| + if block_attributes.key? :attribute_entries + block_attributes[:attribute_entries].each do |entry| if entry.negate @attributes.delete(entry.name) else @@ -709,7 +730,7 @@ class Document < AbstractBlock # # Returns true if the attribute is locked, false otherwise def attribute_locked?(name) - @attribute_overrides.has_key?(name) + @attribute_overrides.key?(name) end # Internal: Apply substitutions to the attribute value @@ -832,7 +853,7 @@ class Document < AbstractBlock converter_opts[:eruby] = @options[:eruby] end converter_factory = if (converter = @options[:converter]) - Converter::Factory.new Hash[backend, converter] + Converter::Factory.new ::Hash[backend, converter] else Converter::Factory.default false end @@ -847,6 +868,7 @@ class Document < AbstractBlock # or a template is missing, the converter will fall back to # using the appropriate built-in template. def convert opts = {} + parse unless @parsed restore_attributes # QUESTION should we add processors that execute before conversion begins? @@ -863,9 +885,9 @@ class Document < AbstractBlock output = @converter.convert self, transform end - if @extensions && !@parent_document - if @extensions.postprocessors? - @extensions.postprocessors.each do |ext| + unless @parent_document + if (exts = @extensions) && exts.postprocessors? + exts.postprocessors.each do |ext| output = ext.process_method[self, output] end end @@ -947,9 +969,9 @@ class Document < AbstractBlock content = nil - docinfo = @attributes.has_key?('docinfo') - docinfo1 = @attributes.has_key?('docinfo1') - docinfo2 = @attributes.has_key?('docinfo2') + docinfo = @attributes.key?('docinfo') + docinfo1 = @attributes.key?('docinfo1') + docinfo2 = @attributes.key?('docinfo2') docinfo_filename = "docinfo#{qualifier}#{ext}" if docinfo1 || docinfo2 docinfo_path = normalize_system_path(docinfo_filename) @@ -961,7 +983,7 @@ class Document < AbstractBlock end end - if (docinfo || docinfo2) && @attributes.has_key?('docname') + if (docinfo || docinfo2) && @attributes.key?('docname') docinfo_path = normalize_system_path("#{@attributes['docname']}-#{docinfo_filename}") content2 = read_asset(docinfo_path) unless content2.nil? |
