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 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 {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.

Constants

DEFAULT_ENGINE_OPTIONS

Attributes

caches[R]

Public Class Methods

clear_caches() click to toggle source
# File lib/asciidoctor/converter/template.rb, line 46
def clear_caches
  @caches[:scans].clear
  @caches[:templates].clear
end
new(backend, template_dirs, opts = {}) click to toggle source
# File lib/asciidoctor/converter/template.rb, line 52
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.merge } }
  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
  end
  case opts[:template_cache]
  when true
    logger.warn 'optional gem \'concurrent-ruby\' is not available. This gem is recommended when using the default template cache.' unless defined? ::Concurrent::Map
    @caches = self.class.caches
  when ::Hash
    @caches = opts[:template_cache]
  else
    @caches = {} # the empty Hash effectively disables caching
  end
  scan
end

Public Instance Methods

convert(node, template_name = nil, opts = nil) click to toggle source

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

# File lib/asciidoctor/converter/template.rb, line 97
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

  # 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).rstrip
  end
end
handles?(name) click to toggle source

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 119
def handles? name
  @templates.key? name
end
register(name, template) click to toggle source

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 136
def register name, template
  if (template_cache = @caches[:templates])
    template_cache[template.file] = template
  end
  @templates[name] = template
end
templates() click to toggle source

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 126
def templates
  @templates.merge
end

Private Instance Methods

load_eruby(name) click to toggle source

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 256
def load_eruby name
  if !name || name == 'erb'
    require 'erb' unless defined? ::ERB.version
    [::Tilt::ERBTemplate, {}]
  elsif name == 'erubi'
    Helpers.require_library 'erubi' unless defined? ::Erubis::Engine
    [::Tilt::ErubiTemplate, {}]
  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
scan() click to toggle source

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 150
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
    # Ruby 2.3 requires the extra brackets around the path_resolver.system_path method call
    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
      # NOTE last matching template wins for template name if no engine is given
      file_pattern = '*'
    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

    pattern = %(#{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
  end
  nil
end
scan_dir(template_dir, pattern, template_cache = nil) click to toggle source

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 196
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).keep_if {|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
    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
          @engine_options[extsym][:attr_quote] = @engine_options[extsym].delete :attr_wrapper unless defined? ::Haml::Options
          @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
  end
  if helpers || ::File.file?(helpers = %(#{template_dir}/helpers.rb))
    require helpers
  end
  result
end