"Fossies" - the Fresh Open Source Software Archive

Member "asciidoctor-2.0.10/lib/asciidoctor/converter/template.rb" (1 Jun 2019, 10607 Bytes) of package /linux/www/asciidoctor-2.0.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "template.rb": 2.0.7_vs_2.0.8.

    1 # frozen_string_literal: true
    2 module Asciidoctor
    3 # A {Converter} implementation that uses templates composed in template
    4 # languages supported by {https://github.com/rtomayko/tilt Tilt} to convert
    5 # {AbstractNode} objects from a parsed AsciiDoc document tree to the backend
    6 # format.
    7 #
    8 # The converter scans the specified directories for template files that are
    9 # supported by Tilt. If an engine name (e.g., "slim") is specified in the
   10 # options Hash passed to the constructor, the scan is restricted to template
   11 # files that have a matching extension (e.g., ".slim"). The scanner trims any
   12 # extensions from the basename of the file and uses the resulting name as the
   13 # key under which to store the template. When the {Converter#convert} method
   14 # is invoked, the transform argument is used to select the template from this
   15 # table and use it to convert the node.
   16 #
   17 # For example, the template file "path/to/templates/paragraph.html.slim" will
   18 # be registered as the "paragraph" transform. The template is then used to
   19 # convert a paragraph {Block} object from the parsed AsciiDoc tree to an HTML
   20 # backend format (e.g., "html5").
   21 #
   22 # As an optimization, scan results and templates are cached for the lifetime
   23 # of the Ruby process. If the {https://rubygems.org/gems/concurrent-ruby
   24 # concurrent-ruby} gem is installed, these caches are guaranteed to be thread
   25 # safe. If this gem is not present, there is no such guarantee and a warning
   26 # will be issued.
   27 class Converter::TemplateConverter < Converter::Base
   28   DEFAULT_ENGINE_OPTIONS = {
   29     erb: { trim: 0 },
   30     # TODO line 466 of haml/compiler.rb sorts the attributes; file an issue to make this configurable
   31     # NOTE AsciiDoc syntax expects HTML/XML output to use double quotes around attribute values
   32     haml: { format: :xhtml, attr_wrapper: '"', escape_attrs: false, ugly: true },
   33     slim: { disable_escape: true, sort_attrs: false, pretty: false },
   34   }
   35 
   36   begin
   37     require 'concurrent/map' unless defined? ::Concurrent::Map
   38     @caches = { scans: ::Concurrent::Map.new, templates: ::Concurrent::Map.new }
   39   rescue ::LoadError
   40     @caches = { scans: {}, templates: {} }
   41   end
   42 
   43   def self.caches
   44     @caches
   45   end
   46 
   47   def self.clear_caches
   48     @caches[:scans].clear if @caches[:scans]
   49     @caches[:templates].clear if @caches[:templates]
   50   end
   51 
   52   def initialize backend, template_dirs, opts = {}
   53     Helpers.require_library 'tilt' unless defined? ::Tilt.new
   54     @backend = backend
   55     @templates = {}
   56     @template_dirs = template_dirs
   57     @eruby = opts[:eruby]
   58     @safe = opts[:safe]
   59     @active_engines = {}
   60     @engine = opts[:template_engine]
   61     @engine_options = {}.tap {|accum| DEFAULT_ENGINE_OPTIONS.each {|engine, engine_opts| accum[engine] = engine_opts.merge } }
   62     if opts[:htmlsyntax] == 'html' # if not set, assume xml since this converter is also used for DocBook (which doesn't specify htmlsyntax)
   63       @engine_options[:haml][:format] = :html5
   64       @engine_options[:slim][:format] = :html
   65     end
   66     @engine_options[:slim][:include_dirs] = template_dirs.reverse.map {|dir| ::File.expand_path dir }
   67     if (overrides = opts[:template_engine_options])
   68       overrides.each do |engine, override_opts|
   69         (@engine_options[engine] ||= {}).update override_opts
   70       end
   71     end
   72     case opts[:template_cache]
   73     when true
   74       logger.warn 'optional gem \'concurrent-ruby\' is not available. This gem is recommended when using the default template cache.' unless defined? ::Concurrent::Map
   75       @caches = self.class.caches
   76     when ::Hash
   77       @caches = opts[:template_cache]
   78     else
   79       @caches = {} # the empty Hash effectively disables caching
   80     end
   81     scan
   82   end
   83 
   84   # Public: Convert an {AbstractNode} to the backend format using the named template.
   85   #
   86   # Looks for a template that matches the value of the template name or, if the template name is not specified, the
   87   # value of the {AbstractNode#node_name} property.
   88   #
   89   # node          - the AbstractNode to convert
   90   # template_name - the String name of the template to use, or the value of
   91   #                 the node_name property on the node if a template name is
   92   #                 not specified. (optional, default: nil)
   93   # opts          - an optional Hash that is passed as local variables to the
   94   #                 template. (optional, default: nil)
   95   #
   96   # Returns the [String] result from rendering the template
   97   def convert node, template_name = nil, opts = nil
   98     unless (template = @templates[template_name ||= node.node_name])
   99       raise %(Could not find a custom template to handle transform: #{template_name})
  100     end
  101 
  102     # Slim doesn't include helpers in the template's execution scope (like HAML), so do it ourselves
  103     node.extend ::Slim::Helpers if (defined? ::Slim::Helpers) && (::Slim::Template === template)
  104 
  105     # NOTE opts become locals in the template
  106     if template_name == 'document'
  107       (template.render node, opts).strip
  108     else
  109       (template.render node, opts).rstrip
  110     end
  111   end
  112 
  113   # Public: Checks whether there is a Tilt template registered with the specified name.
  114   #
  115   # name - the String template name
  116   #
  117   # Returns a [Boolean] that indicates whether a Tilt template is registered for the
  118   # specified template name.
  119   def handles? name
  120     @templates.key? name
  121   end
  122 
  123   # Public: Retrieves the templates that this converter manages.
  124   #
  125   # Returns a [Hash] of Tilt template objects keyed by template name.
  126   def templates
  127     @templates.merge
  128   end
  129 
  130   # Public: Registers a Tilt template with this converter.
  131   #
  132   # name     - the String template name
  133   # template - the Tilt template object to register
  134   #
  135   # Returns the Tilt template object
  136   def register name, template
  137     @templates[name] = if (template_cache = @caches[:templates])
  138       template_cache[template.file] = template
  139     else
  140       template
  141     end
  142     #create_handler name, template
  143   end
  144 
  145   private
  146 
  147   # Internal: Scans the template directories specified in the constructor for Tilt-supported
  148   # templates, loads the templates and stores the in a Hash that is accessible via the
  149   # {TemplateConverter#templates} method.
  150   #
  151   # Returns nothing
  152   def scan
  153     path_resolver = PathResolver.new
  154     backend = @backend
  155     engine = @engine
  156     @template_dirs.each do |template_dir|
  157       # FIXME need to think about safe mode restrictions here
  158       next unless ::File.directory?(template_dir = (path_resolver.system_path template_dir))
  159 
  160       if engine
  161         file_pattern = %(*.#{engine})
  162         # example: templates/haml
  163         if ::File.directory?(engine_dir = %(#{template_dir}/#{engine}))
  164           template_dir = engine_dir
  165         end
  166       else
  167         # NOTE last matching template wins for template name if no engine is given
  168         file_pattern = '*'
  169       end
  170 
  171       # example: templates/html5 (engine not set) or templates/haml/html5 (engine set)
  172       if ::File.directory?(backend_dir = %(#{template_dir}/#{backend}))
  173         template_dir = backend_dir
  174       end
  175 
  176       pattern = %(#{template_dir}/#{file_pattern})
  177 
  178       if (scan_cache = @caches[:scans])
  179         template_cache = @caches[:templates]
  180         unless (templates = scan_cache[pattern])
  181           templates = scan_cache[pattern] = scan_dir template_dir, pattern, template_cache
  182         end
  183         templates.each do |name, template|
  184           @templates[name] = template_cache[template.file] = template
  185         end
  186       else
  187         @templates.update scan_dir(template_dir, pattern, @caches[:templates])
  188       end
  189       nil
  190     end
  191   end
  192 
  193   # Internal: Scan the specified directory for template files matching pattern and instantiate
  194   # a Tilt template for each matched file.
  195   #
  196   # Returns the scan result as a [Hash]
  197   def scan_dir template_dir, pattern, template_cache = nil
  198     result, helpers = {}, nil
  199     # Grab the files in the top level of the directory (do not recurse)
  200     ::Dir.glob(pattern).select {|match| ::File.file? match }.each do |file|
  201       if (basename = ::File.basename file) == 'helpers.rb'
  202         helpers = file
  203         next
  204       elsif (path_segments = basename.split '.').size < 2
  205         next
  206       end
  207       if (name = path_segments[0]) == 'block_ruler'
  208         name = 'thematic_break'
  209       elsif name.start_with? 'block_'
  210         name = name.slice 6, name.length
  211       end
  212       unless template_cache && (template = template_cache[file])
  213         template_class, extra_engine_options, extsym = ::Tilt, {}, path_segments[-1].to_sym
  214         case extsym
  215         when :slim
  216           unless @active_engines[extsym]
  217             # NOTE slim doesn't get automatically loaded by Tilt
  218             Helpers.require_library 'slim' unless defined? ::Slim::Engine
  219             require 'slim/include' unless defined? ::Slim::Include
  220             ::Slim::Engine.define_options asciidoc: {}
  221             # align safe mode of AsciiDoc embedded in Slim template with safe mode of current document
  222             # NOTE safe mode won't get updated if using template cache and changing safe mode
  223             (@engine_options[extsym][:asciidoc] ||= {})[:safe] ||= @safe if @safe
  224             @active_engines[extsym] = true
  225           end
  226         when :haml
  227           unless @active_engines[extsym]
  228             Helpers.require_library 'haml' unless defined? ::Haml::Engine
  229             # NOTE Haml 5 dropped support for pretty printing
  230             @engine_options[extsym].delete :ugly if defined? ::Haml::TempleEngine
  231             @active_engines[extsym] = true
  232           end
  233         when :erb
  234           template_class, extra_engine_options = (@active_engines[extsym] ||= (load_eruby @eruby))
  235         when :rb
  236           next
  237         else
  238           next unless ::Tilt.registered? extsym.to_s
  239         end
  240         template = template_class.new file, 1, (@engine_options[extsym] ||= {}).merge(extra_engine_options)
  241       end
  242       result[name] = template
  243     end
  244     if helpers || ::File.file?(helpers = %(#{template_dir}/helpers.rb))
  245       require helpers
  246     end
  247     result
  248   end
  249 
  250   # Internal: Load the eRuby implementation
  251   #
  252   # name - the String name of the eRuby implementation
  253   #
  254   # Returns an [Array] containing the Tilt template Class for the eRuby implementation
  255   # and a Hash of additional options to pass to the initializer
  256   def load_eruby name
  257     if !name || name == 'erb'
  258       require 'erb' unless defined? ::ERB.version
  259       [::Tilt::ERBTemplate, {}]
  260     elsif name == 'erubis'
  261       Helpers.require_library 'erubis' unless defined? ::Erubis::FastEruby
  262       [::Tilt::ErubisTemplate, { engine_class: ::Erubis::FastEruby }]
  263     else
  264       raise ::ArgumentError, %(Unknown ERB implementation: #{name})
  265     end
  266   end
  267 end
  268 end