class Asciidoctor::Converter::TemplateConverter
A {Converter} implementation that uses templates composed in template languages supported by {github.com/rtomayko/tilt Tilt} to convert {AbstractNode} objects from a parsed AsciiDoc document tree to the backend format.
The converter scans the provided 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 limited 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 would then be 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 {rubygems.org/gems/thread_safe thread_safe} gem is installed, these caches are guaranteed to be thread safe. If this gem is not present, they are not and a warning is issued.
Constants
- DEFAULT_ENGINE_OPTIONS
Public Class Methods
# File lib/asciidoctor/converter/template.rb, line 44 def self.caches @caches end
# File lib/asciidoctor/converter/template.rb, line 48 def self.clear_caches @caches[:scans].clear if @caches[:scans] @caches[:templates].clear if @caches[:templates] end
# File lib/asciidoctor/converter/template.rb, line 53 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] @engine = opts[:template_engine] @engine_options = DEFAULT_ENGINE_OPTIONS.inject({}) do |accum, (engine, default_opts)| accum[engine] = default_opts.dup accum end if opts[:htmlsyntax] == 'html' @engine_options[:haml][:format] = :html5 @engine_options[:slim][:format] = :html end 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 @caches = self.class.caches when ::Hash @caches = opts[:template_cache] else @caches = {} # the empty Hash effectively disables caching end scan #create_handlers end
Public Instance Methods
# 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 # 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 end else metaclass.send :define_method, name do |node| (template.render node).chomp 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
# File lib/asciidoctor/converter/template.rb, line 179 def convert node, template_name = nil, opts = {} template_name ||= node.node_name unless (template = @templates[template_name]) raise %Q(Could not find a custom template to handle transform: #{template_name}) 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) # NOTE opts become locals in the template if template_name == 'document' (template.render node, opts).strip else (template.render node, opts).chomp 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.
# File lib/asciidoctor/converter/template.rb, line 202 def handles? name @templates.key? 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
# File lib/asciidoctor/converter/template.rb, line 283 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, %Q(Unknown ERB implementation: #{name}) end 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
# File lib/asciidoctor/converter/template.rb, line 219 def register name, template @templates[name] = if (template_cache = @caches[:templates]) template_cache[template.file] = template else template end #create_handler name, template end
# Public: Called when this converter is added to a composite converter. def composed parent # TODO set the backend info determined during the scan 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
# File lib/asciidoctor/converter/template.rb, line 98 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, nil)) # NOTE last matching template wins for template name if no engine is given file_pattern = '*' if engine file_pattern = %Q(*.#{engine}) # example: templates/haml if ::File.directory?(engine_dir = (::File.join template_dir, engine)) template_dir = engine_dir end end # example: templates/html5 or templates/haml/html5 if ::File.directory?(backend_dir = (::File.join template_dir, backend)) template_dir = backend_dir end pattern = ::File.join template_dir, file_pattern 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 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]
# File lib/asciidoctor/converter/template.rb, line 232 def scan_dir template_dir, pattern, template_cache = nil result = {} eruby_loaded = 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' || (path_segments = basename.split '.').size < 2 next end # TODO we could derive the basebackend from the minor extension of the template file #name, *rest, ext_name = *path_segments # this form only works in Ruby >= 1.9 name = path_segments[0] if name == 'block_ruler' name = 'thematic_break' elsif name.start_with? 'block_' name = name[6..-1] end template_class = ::Tilt extra_engine_options = {} case (ext_name = path_segments[-1]) when 'slim' # slim doesn't get loaded by Tilt, so we have to load it explicitly Helpers.require_library 'slim' unless defined? ::Slim # align safe mode of AsciiDoc embedded in Slim template with safe mode of current document (@engine_options[:slim][: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' when 'erb' template_class, extra_engine_options = (eruby_loaded ||= load_eruby(@eruby)) when 'rb' next else next unless ::Tilt.registered? ext_name end unless template_cache && (template = template_cache[file]) template = template_class.new file, 1, (@engine_options[ext_name.to_sym] || {}).merge(extra_engine_options) end result[name] = template end if ::File.file?(helpers = (::File.join template_dir, 'helpers.rb')) require helpers end result end
Public: Retrieves the templates that this converter manages.
Returns a [Hash] of Tilt template objects keyed by template name.
# File lib/asciidoctor/converter/template.rb, line 209 def templates @templates.dup.freeze end