lib/asciidoctor/converter/template.rb in asciidoctor-1.5.8 vs lib/asciidoctor/converter/template.rb in asciidoctor-2.0.0.rc.1

- old
+ new

@@ -1,309 +1,268 @@ -# encoding: UTF-8 +# frozen_string_literal: true module Asciidoctor - # A {Converter} implementation that uses templates composed in template - # languages supported by {https://github.com/rtomayko/tilt Tilt} to convert - # {AbstractNode} objects from a parsed AsciiDoc document tree to the backend - # format. - # - # The converter scans the specified directories for template files that are - # supported by Tilt. If an engine name (e.g., "slim") is specified in the - # options Hash passed to the constructor, the scan is restricted to template - # files that have a matching extension (e.g., ".slim"). The scanner trims any - # extensions from the basename of the file and uses the resulting name as the - # key under which to store the template. When the {Converter#convert} method - # is invoked, the transform argument is used to select the template from this - # table and use it to convert the node. - # - # For example, the template file "path/to/templates/paragraph.html.slim" will - # be registered as the "paragraph" transform. The template is then used to - # convert a paragraph {Block} object from the parsed AsciiDoc tree to an HTML - # backend format (e.g., "html5"). - # - # As an optimization, scan results and templates are cached for the lifetime - # of the Ruby process. If the {https://rubygems.org/gems/concurrent-ruby - # concurrent-ruby} gem is installed, these caches are guaranteed to be thread - # safe. If this gem is not present, there is no such guarantee and a warning - # will be issued. - class Converter::TemplateConverter < Converter::Base - DEFAULT_ENGINE_OPTIONS = { - :erb => { :trim => '<' }, - # TODO line 466 of haml/compiler.rb sorts the attributes; file an issue to make this configurable - # NOTE AsciiDoc syntax expects HTML/XML output to use double quotes around attribute values - :haml => { :format => :xhtml, :attr_wrapper => '"', :escape_attrs => false, :ugly => true }, - :slim => { :disable_escape => true, :sort_attrs => false, :pretty => false } - } +# A {Converter} implementation that uses templates composed in template +# languages supported by {https://github.com/rtomayko/tilt Tilt} to convert +# {AbstractNode} objects from a parsed AsciiDoc document tree to the backend +# format. +# +# The converter scans the specified directories for template files that are +# supported by Tilt. If an engine name (e.g., "slim") is specified in the +# options Hash passed to the constructor, the scan is restricted to template +# files that have a matching extension (e.g., ".slim"). The scanner trims any +# extensions from the basename of the file and uses the resulting name as the +# key under which to store the template. When the {Converter#convert} method +# is invoked, the transform argument is used to select the template from this +# table and use it to convert the node. +# +# For example, the template file "path/to/templates/paragraph.html.slim" will +# be registered as the "paragraph" transform. The template is then used to +# convert a paragraph {Block} object from the parsed AsciiDoc tree to an HTML +# backend format (e.g., "html5"). +# +# As an optimization, scan results and templates are cached for the lifetime +# of the Ruby process. If the {https://rubygems.org/gems/concurrent-ruby +# concurrent-ruby} gem is installed, these caches are guaranteed to be thread +# safe. If this gem is not present, there is no such guarantee and a warning +# will be issued. +class Converter::TemplateConverter < Converter::Base + DEFAULT_ENGINE_OPTIONS = { + erb: { trim: 0 }, + # TODO line 466 of haml/compiler.rb sorts the attributes; file an issue to make this configurable + # NOTE AsciiDoc syntax expects HTML/XML output to use double quotes around attribute values + haml: { format: :xhtml, attr_wrapper: '"', escape_attrs: false, ugly: true }, + slim: { disable_escape: true, sort_attrs: false, pretty: false }, + } - begin - unless defined? ::Concurrent::Hash - require ::RUBY_MIN_VERSION_1_9 ? 'concurrent/hash' : 'asciidoctor/core_ext/1.8.7/concurrent/hash' - end - @caches = { :scans => ::Concurrent::Hash.new, :templates => ::Concurrent::Hash.new } - rescue ::LoadError - @caches = { :scans => {}, :templates => {} } - end + begin + require 'concurrent/hash' unless defined? ::Concurrent::Hash + @caches = { scans: ::Concurrent::Hash.new, templates: ::Concurrent::Hash.new } + rescue ::LoadError + @caches = { scans: {}, templates: {} } + end - def self.caches - @caches - end + def self.caches + @caches + end - def self.clear_caches - @caches[:scans].clear if @caches[:scans] - @caches[:templates].clear if @caches[:templates] - end + def self.clear_caches + @caches[:scans].clear if @caches[:scans] + @caches[:templates].clear if @caches[:templates] + end - def initialize backend, template_dirs, opts = {} - Helpers.require_library 'tilt' unless defined? ::Tilt - @backend = backend - @templates = {} - @template_dirs = template_dirs - @eruby = opts[:eruby] - @safe = opts[:safe] - @active_engines = {} - @engine = opts[:template_engine] - @engine_options = DEFAULT_ENGINE_OPTIONS.inject({}) do |accum, (engine, default_opts)| - accum[engine] = default_opts.dup - accum + def initialize backend, template_dirs, opts = {} + Helpers.require_library 'tilt' unless defined? ::Tilt.new + @backend = backend + @templates = {} + @template_dirs = template_dirs + @eruby = opts[:eruby] + @safe = opts[:safe] + @active_engines = {} + @engine = opts[:template_engine] + @engine_options = {}.tap {|accum| DEFAULT_ENGINE_OPTIONS.each {|engine, engine_opts| accum[engine] = engine_opts.dup } } + if opts[:htmlsyntax] == 'html' # if not set, assume xml since this converter is also used for DocBook (which doesn't specify htmlsyntax) + @engine_options[:haml][:format] = :html5 + @engine_options[:slim][:format] = :html + end + @engine_options[:slim][:include_dirs] = template_dirs.reverse.map {|dir| ::File.expand_path dir } + if (overrides = opts[:template_engine_options]) + overrides.each do |engine, override_opts| + (@engine_options[engine] ||= {}).update override_opts end - if opts[:htmlsyntax] == 'html' - @engine_options[:haml][:format] = :html5 - @engine_options[:slim][:format] = :html - end - @engine_options[:slim][:include_dirs] = template_dirs.reverse.map {|dir| ::File.expand_path dir } - if (overrides = opts[:template_engine_options]) - overrides.each do |engine, override_opts| - (@engine_options[engine] ||= {}).update override_opts - end - end - case opts[:template_cache] - when true - logger.warn 'gem \'concurrent-ruby\' is not installed. This gem is recommended when using the built-in template cache.' unless defined? ::Concurrent::Hash - @caches = self.class.caches - when ::Hash - @caches = opts[:template_cache] - else - @caches = {} # the empty Hash effectively disables caching - end - scan - #create_handlers end + case opts[:template_cache] + when true + logger.warn 'gem \'concurrent-ruby\' is not installed. This gem is recommended when using the built-in template cache.' unless defined? ::Concurrent::Hash + @caches = self.class.caches + when ::Hash + @caches = opts[:template_cache] + else + @caches = {} # the empty Hash effectively disables caching + end + scan + end -=begin - # Public: Called when this converter is added to a composite converter. - def composed parent - # TODO set the backend info determined during the scan + # Public: Convert an {AbstractNode} to the backend format using the named template. + # + # Looks for a template that matches the value of the template name or, if the template name is not specified, the + # value of the {AbstractNode#node_name} property. + # + # node - the AbstractNode to convert + # template_name - the String name of the template to use, or the value of + # the node_name property on the node if a template name is + # not specified. (optional, default: nil) + # opts - an optional Hash that is passed as local variables to the + # template. (optional, default: nil) + # + # Returns the [String] result from rendering the template + def convert node, template_name = nil, opts = nil + unless (template = @templates[template_name ||= node.node_name]) + raise %(Could not find a custom template to handle transform: #{template_name}) end -=end - # Internal: Scans the template directories specified in the constructor for Tilt-supported - # templates, loads the templates and stores the in a Hash that is accessible via the - # {TemplateConverter#templates} method. - # - # Returns nothing - def scan - path_resolver = PathResolver.new - backend = @backend - engine = @engine - @template_dirs.each do |template_dir| - # FIXME need to think about safe mode restrictions here - next unless ::File.directory?(template_dir = (path_resolver.system_path template_dir)) + # Slim doesn't include helpers in the template's execution scope (like HAML), so do it ourselves + node.extend ::Slim::Helpers if (defined? ::Slim::Helpers) && (::Slim::Template === template) - if engine - file_pattern = %(*.#{engine}) - # example: templates/haml - if ::File.directory?(engine_dir = %(#{template_dir}/#{engine})) - template_dir = engine_dir - end - else - # NOTE last matching template wins for template name if no engine is given - file_pattern = '*' - end + # NOTE opts become locals in the template + if template_name == 'document' + (template.render node, opts).strip + else + (template.render node, opts).rstrip + end + end - # example: templates/html5 (engine not set) or templates/haml/html5 (engine set) - if ::File.directory?(backend_dir = %(#{template_dir}/#{backend})) - template_dir = backend_dir - end + # Public: Checks whether there is a Tilt template registered with the specified name. + # + # name - the String template name + # + # Returns a [Boolean] that indicates whether a Tilt template is registered for the + # specified template name. + def handles? name + @templates.key? name + end - pattern = %(#{template_dir}/#{file_pattern}) + # Public: Retrieves the templates that this converter manages. + # + # Returns a [Hash] of Tilt template objects keyed by template name. + def templates + @templates.dup + end - if (scan_cache = @caches[:scans]) - template_cache = @caches[:templates] - unless (templates = scan_cache[pattern]) - templates = (scan_cache[pattern] = (scan_dir template_dir, pattern, template_cache)) - end - templates.each do |name, template| - @templates[name] = template_cache[template.file] = template - end - else - @templates.update scan_dir(template_dir, pattern, @caches[:templates]) - end - nil - end + # Public: Registers a Tilt template with this converter. + # + # name - the String template name + # template - the Tilt template object to register + # + # Returns the Tilt template object + def register name, template + @templates[name] = if (template_cache = @caches[:templates]) + template_cache[template.file] = template + else + template end + #create_handler name, template + end -=begin - # Internal: Creates convert methods (e.g., inline_anchor) that delegate to the discovered templates. - # - # Returns nothing - def create_handlers - @templates.each do |name, template| - create_handler name, template - end - nil - end + private - # Internal: Creates a convert method for the specified name that delegates to the specified template. - # - # Returns nothing - def create_handler name, template - metaclass = class << self; self; end - if name == 'document' - metaclass.send :define_method, name do |node| - (template.render node).strip + # Internal: Scans the template directories specified in the constructor for Tilt-supported + # templates, loads the templates and stores the in a Hash that is accessible via the + # {TemplateConverter#templates} method. + # + # Returns nothing + def scan + path_resolver = PathResolver.new + backend = @backend + engine = @engine + @template_dirs.each do |template_dir| + # FIXME need to think about safe mode restrictions here + next unless ::File.directory?(template_dir = (path_resolver.system_path template_dir)) + + if engine + file_pattern = %(*.#{engine}) + # example: templates/haml + if ::File.directory?(engine_dir = %(#{template_dir}/#{engine})) + template_dir = engine_dir end else - metaclass.send :define_method, name do |node| - (template.render node).rstrip - end + # NOTE last matching template wins for template name if no engine is given + file_pattern = '*' end - end -=end - # Public: Convert an {AbstractNode} to the backend format using the named template. - # - # Looks for a template that matches the value of the - # {AbstractNode#node_name} property if a template name is not specified. - # - # node - the AbstractNode to convert - # template_name - the String name of the template to use, or the value of - # the node_name property on the node if a template name is - # not specified. (optional, default: nil) - # opts - an optional Hash that is passed as local variables to the - # template. (optional, default: {}) - # - # Returns the [String] result from rendering the template - def convert node, template_name = nil, opts = {} - template_name ||= node.node_name - unless (template = @templates[template_name]) - raise %(Could not find a custom template to handle transform: #{template_name}) + # example: templates/html5 (engine not set) or templates/haml/html5 (engine set) + if ::File.directory?(backend_dir = %(#{template_dir}/#{backend})) + template_dir = backend_dir end - # Slim doesn't include helpers in the template's execution scope (like HAML), so do it ourselves - node.extend ::Slim::Helpers if (defined? ::Slim::Helpers) && (::Slim::Template === template) + pattern = %(#{template_dir}/#{file_pattern}) - # NOTE opts become locals in the template - if template_name == 'document' - (template.render node, opts).strip + if (scan_cache = @caches[:scans]) + template_cache = @caches[:templates] + unless (templates = scan_cache[pattern]) + templates = scan_cache[pattern] = scan_dir template_dir, pattern, template_cache + end + templates.each do |name, template| + @templates[name] = template_cache[template.file] = template + end else - (template.render node, opts).rstrip + @templates.update scan_dir(template_dir, pattern, @caches[:templates]) end + nil end + end - # Public: Checks whether there is a Tilt template registered with the specified name. - # - # name - the String template name - # - # Returns a [Boolean] that indicates whether a Tilt template is registered for the - # specified template name. - def handles? name - @templates.key? name - end - - # Public: Retrieves the templates that this converter manages. - # - # Returns a [Hash] of Tilt template objects keyed by template name. - def templates - @templates.dup - end - - # Public: Registers a Tilt template with this converter. - # - # name - the String template name - # template - the Tilt template object to register - # - # Returns the Tilt template object - def register name, template - @templates[name] = if (template_cache = @caches[:templates]) - template_cache[template.file] = template - else - template + # Internal: Scan the specified directory for template files matching pattern and instantiate + # a Tilt template for each matched file. + # + # Returns the scan result as a [Hash] + def scan_dir template_dir, pattern, template_cache = nil + result, helpers = {}, nil + # Grab the files in the top level of the directory (do not recurse) + ::Dir.glob(pattern).select {|match| ::File.file? match }.each do |file| + if (basename = ::File.basename file) == 'helpers.rb' + helpers = file + next + elsif (path_segments = basename.split '.').size < 2 + next end - #create_handler name, template - end - - # Internal: Scan the specified directory for template files matching pattern and instantiate - # a Tilt template for each matched file. - # - # Returns the scan result as a [Hash] - def scan_dir template_dir, pattern, template_cache = nil - result, helpers = {}, nil - # Grab the files in the top level of the directory (do not recurse) - ::Dir.glob(pattern).select {|match| ::File.file? match }.each do |file| - if (basename = ::File.basename file) == 'helpers.rb' - helpers = file + if (name = path_segments[0]) == 'block_ruler' + name = 'thematic_break' + elsif name.start_with? 'block_' + name = name.slice 6, name.length + end + unless template_cache && (template = template_cache[file]) + template_class, extra_engine_options, extsym = ::Tilt, {}, path_segments[-1].to_sym + case extsym + when :slim + unless @active_engines[extsym] + # NOTE slim doesn't get automatically loaded by Tilt + Helpers.require_library 'slim' unless defined? ::Slim::Engine + require 'slim/include' unless defined? ::Slim::Include + ::Slim::Engine.define_options asciidoc: {} + # align safe mode of AsciiDoc embedded in Slim template with safe mode of current document + # NOTE safe mode won't get updated if using template cache and changing safe mode + (@engine_options[extsym][:asciidoc] ||= {})[:safe] ||= @safe if @safe + @active_engines[extsym] = true + end + when :haml + unless @active_engines[extsym] + Helpers.require_library 'haml' unless defined? ::Haml::Engine + # NOTE Haml 5 dropped support for pretty printing + @engine_options[extsym].delete :ugly if defined? ::Haml::TempleEngine + @active_engines[extsym] = true + end + when :erb + template_class, extra_engine_options = (@active_engines[extsym] ||= (load_eruby @eruby)) + when :rb next - elsif (path_segments = basename.split '.').size < 2 - next + else + next unless ::Tilt.registered? extsym.to_s end - if (name = path_segments[0]) == 'block_ruler' - name = 'thematic_break' - elsif name.start_with? 'block_' - name = name.slice 6, name.length - end - unless template_cache && (template = template_cache[file]) - template_class, extra_engine_options, extsym = ::Tilt, {}, path_segments[-1].to_sym - case extsym - when :slim - unless @active_engines[extsym] - # NOTE slim doesn't get automatically loaded by Tilt - Helpers.require_library 'slim' unless defined? ::Slim - ::Slim::Engine.define_options :asciidoc => {} - # align safe mode of AsciiDoc embedded in Slim template with safe mode of current document - # NOTE safe mode won't get updated if using template cache and changing safe mode - (@engine_options[extsym][:asciidoc] ||= {})[:safe] ||= @safe if @safe && ::Slim::VERSION >= '3.0' - # load include plugin when using Slim >= 2.1 - require 'slim/include' unless (defined? ::Slim::Include) || ::Slim::VERSION < '2.1' - @active_engines[extsym] = true - end - when :haml - unless @active_engines[extsym] - Helpers.require_library 'haml' unless defined? ::Haml - # NOTE Haml 5 dropped support for pretty printing - @engine_options[extsym].delete :ugly if defined? ::Haml::TempleEngine - @active_engines[extsym] = true - end - when :erb - template_class, extra_engine_options = (@active_engines[extsym] ||= (load_eruby @eruby)) - when :rb - next - else - next unless ::Tilt.registered? extsym.to_s - end - template = template_class.new file, 1, (@engine_options[extsym] ||= {}).merge(extra_engine_options) - end - result[name] = template + template = template_class.new file, 1, (@engine_options[extsym] ||= {}).merge(extra_engine_options) end - if helpers || ::File.file?(helpers = %(#{template_dir}/helpers.rb)) - require helpers - end - result + result[name] = template end + if helpers || ::File.file?(helpers = %(#{template_dir}/helpers.rb)) + require helpers + end + result + end - # Internal: Load the eRuby implementation - # - # name - the String name of the eRuby implementation - # - # 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 - [::Tilt::ERBTemplate, {}] - elsif name == 'erubis' - Helpers.require_library 'erubis' unless defined? ::Erubis::FastEruby - [::Tilt::ErubisTemplate, { :engine_class => ::Erubis::FastEruby }] - else - raise ::ArgumentError, %(Unknown ERB implementation: #{name}) - end + # Internal: Load the eRuby implementation + # + # name - the String name of the eRuby implementation + # + # 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.version + [::Tilt::ERBTemplate, {}] + elsif name == 'erubis' + Helpers.require_library 'erubis' unless defined? ::Erubis::FastEruby + [::Tilt::ErubisTemplate, { engine_class: ::Erubis::FastEruby }] + else + raise ::ArgumentError, %(Unknown ERB implementation: #{name}) end end +end end