File: mapping.rb

package info (click to toggle)
ruby-tilt 2.6.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 648 kB
  • sloc: ruby: 4,998; makefile: 7
file content (412 lines) | stat: -rw-r--r-- 13,634 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# frozen_string_literal: true
require_relative 'pipeline'

module Tilt
  # Private internal base class for both Mapping and FinalizedMapping, for the shared methods.
  class BaseMapping
    # Instantiates a new template class based on the file.
    #
    # @raise [RuntimeError] if there is no template class registered for the
    #   file name.
    #
    # @example
    #   mapping.new('index.mt') # => instance of MyEngine::Template
    #
    # @see Tilt::Template.new
    def new(file, line=nil, options={}, &block)
      if template_class = self[file]
        template_class.new(file, line, options, &block)
      else
        fail "No template engine registered for #{File.basename(file)}"
      end
    end

    # Looks up a template class based on file name and/or extension.
    #
    # @example
    #   mapping['views/hello.erb'] # => Tilt::ERBTemplate
    #   mapping['hello.erb']       # => Tilt::ERBTemplate
    #   mapping['erb']             # => Tilt::ERBTemplate
    #
    # @return [template class]
    def [](file)
      _, ext = split(file)
      ext && lookup(ext)
    end

    alias template_for []

    # Looks up a list of template classes based on file name. If the file name
    # has multiple extensions, it will return all template classes matching the
    # extensions from the end.
    #
    # @example
    #   mapping.templates_for('views/index.haml.erb')
    #   # => [Tilt::ERBTemplate, Tilt::HamlTemplate]
    #
    # @return [Array<template class>]
    def templates_for(file)
      templates = []

      while true
        prefix, ext = split(file)
        break unless ext
        templates << lookup(ext)
        file = prefix
      end

      templates
    end

    private

    def split(file)
      pattern = file.to_s.downcase
      full_pattern = pattern.dup

      until registered?(pattern)
        return if pattern.empty?
        pattern = File.basename(pattern)
        pattern.sub!(/\A[^.]*\.?/, '')
      end

      prefix_size = full_pattern.size - pattern.size
      [full_pattern[0,prefix_size-1], pattern]
    end
  end
  private_constant :BaseMapping

  # Tilt::Mapping associates file extensions with template implementations.
  #
  #     mapping = Tilt::Mapping.new
  #     mapping.register(Tilt::RDocTemplate, 'rdoc')
  #     mapping['index.rdoc'] # => Tilt::RDocTemplate
  #     mapping.new('index.rdoc').render
  #
  # You can use {#register} to register a template class by file
  # extension, {#registered?} to see if a file extension is mapped,
  # {#[]} to lookup template classes, and {#new} to instantiate template
  # objects.
  #
  # Mapping also supports *lazy* template implementations. Note that regularly
  # registered template implementations *always* have preference over lazily
  # registered template implementations. You should use {#register} if you
  # depend on a specific template implementation and {#register_lazy} if there
  # are multiple alternatives.
  #
  #     mapping = Tilt::Mapping.new
  #     mapping.register_lazy('RDiscount::Template', 'rdiscount/template', 'md')
  #     mapping['index.md']
  #     # => RDiscount::Template
  #
  # {#register_lazy} takes a class name, a filename, and a list of file
  # extensions. When you try to lookup a template name that matches the
  # file extension, Tilt will automatically try to require the filename and
  # constantize the class name.
  #
  # Unlike {#register}, there can be multiple template implementations
  # registered lazily to the same file extension. Tilt will attempt to load the
  # template implementations in order (registered *last* would be tried first),
  # returning the first which doesn't raise LoadError.
  #
  # If all of the registered template implementations fails, Tilt will raise
  # the exception of the first, since that was the most preferred one.
  #
  #     mapping = Tilt::Mapping.new
  #     mapping.register_lazy('Kramdown::Template', 'kramdown/template', 'md')
  #     mapping.register_lazy('RDiscount::Template', 'rdiscount/template', 'md')
  #     mapping['index.md']
  #     # => RDiscount::Template
  #
  # In the previous example we say that RDiscount has a *higher priority* than
  # Kramdown. Tilt will first try to `require "rdiscount/template"`, falling
  # back to `require "kramdown/template"`. If none of these are successful,
  # the first error will be raised.
  class Mapping < BaseMapping
    LOCK = Mutex.new

    # @private
    attr_reader :lazy_map, :template_map

    def initialize
      @template_map = Hash.new
      @lazy_map = Hash.new { |h, k| h[k] = [] }
    end

    # @private
    def initialize_copy(other)
      LOCK.synchronize do
        @template_map = other.template_map.dup
        @lazy_map = other.lazy_map.dup
      end
    end

    # Return a finalized mapping. A finalized mapping will only include
    # support for template libraries already loaded, and will not
    # allow registering new template libraries or lazy loading template
    # libraries not yet loaded. Finalized mappings improve performance
    # by not requiring synchronization and ensure that the mapping will
    # not attempt to load additional files (useful when restricting
    # file system access after template libraries in use are loaded).
    def finalized
      LOCK.synchronize{@lazy_map.dup}.each do |pattern, classes|
        register_defined_classes(LOCK.synchronize{classes.map(&:first)}, pattern)
      end

      # Check if a template class is already present
      FinalizedMapping.new(LOCK.synchronize{@template_map.dup}.freeze)
    end

    # Registers a lazy template implementation by file extension. You
    # can have multiple lazy template implementations defined on the
    # same file extension, in which case the template implementation
    # defined *last* will be attempted loaded *first*.
    #
    # @param class_name [String] Class name of a template class.
    # @param file [String] Filename where the template class is defined.
    # @param extensions [Array<String>] List of extensions.
    # @return [void]
    #
    # @example
    #   mapping.register_lazy 'MyEngine::Template', 'my_engine/template',  'mt'
    #
    #   defined?(MyEngine::Template) # => false
    #   mapping['index.mt'] # => MyEngine::Template
    #   defined?(MyEngine::Template) # => true
    def register_lazy(class_name, file, *extensions)
      # Internal API
      if class_name.is_a?(Symbol)
        Tilt.autoload class_name, file
        class_name = "Tilt::#{class_name}"
      end

      v = [class_name, file].freeze
      extensions.each do |ext|
        LOCK.synchronize{@lazy_map[ext].unshift(v)}
      end
    end

    # Registers a template implementation by file extension. There can only be
    # one template implementation per file extension, and this method will
    # override any existing mapping.
    #
    # @param template_class
    # @param extensions [Array<String>] List of extensions.
    # @return [void]
    # 
    # @example
    #   mapping.register MyEngine::Template, 'mt'
    #   mapping['index.mt'] # => MyEngine::Template
    def register(template_class, *extensions)
      if template_class.respond_to?(:to_str)
        # Support register(ext, template_class) too
        extensions, template_class = [template_class], extensions[0]
      end

      extensions.each do |ext|
        ext = ext.to_s
        LOCK.synchronize do
          @template_map[ext] = template_class
        end
      end
    end

    # Register a new template class using the given extension that
    # represents a pipeline of multiple existing template, where the
    # output from the previous template is used as input to the next
    # template.
    #
    # This will register a template class that processes the input
    # with the *erb* template processor, and takes the output of
    # that and feeds it to the *scss* template processor, returning
    # the output of the *scss* template processor as the result of
    # the pipeline.
    #
    # @param ext [String] Primary extension to register
    # @option :templates [Array<String>] Extensions of templates
    #         to execute in order (defaults to the ext.split('.').reverse)
    # @option :extra_exts [Array<String>] Additional extensions to register
    # @option String [Hash] Options hash for individual template in the
    #         pipeline (key is extension).
    # @return [void]
    #
    # @example
    #   mapping.register_pipeline('scss.erb')
    #   mapping.register_pipeline('scss.erb', 'erb'=>{:outvar=>'@foo'})
    #   mapping.register_pipeline('scsserb', :extra_exts => 'scss.erb',
    #                             :templates=>['erb', 'scss'])
    def register_pipeline(ext, options=EMPTY_HASH)
      templates = options[:templates] || ext.split('.').reverse
      templates = templates.map{|t| [self[t], options[t] || EMPTY_HASH]}

      klass = Class.new(Pipeline)
      klass.send(:const_set, :TEMPLATES, templates)

      register(klass, ext, *Array(options[:extra_exts]))
      klass
    end

    # Unregisters an extension. This removes the both normal registrations
    # and lazy registrations.
    #
    # @param extensions [Array<String>] List of extensions.
    # @return nil
    #
    # @example
    #   mapping.register MyEngine::Template, 'mt'
    #   mapping['index.mt'] # => MyEngine::Template
    #   mapping.unregister('mt')
    #   mapping['index.mt'] # => nil
    def unregister(*extensions)
      extensions.each do |ext|
        ext = ext.to_s
        LOCK.synchronize do
          @template_map.delete(ext)
          @lazy_map.delete(ext)
        end
      end

      nil
    end

    # Checks if a file extension is registered (either eagerly or
    # lazily) in this mapping.
    #
    # @param ext [String] File extension.
    #
    # @example
    #   mapping.registered?('erb')  # => true
    #   mapping.registered?('nope') # => false
    def registered?(ext)
      ext_downcase = ext.downcase
      LOCK.synchronize{@template_map.has_key?(ext_downcase)} or lazy?(ext)
    end

    # Finds the extensions the template class has been registered under.
    # @param [template class] template_class
    def extensions_for(template_class)
      res = []
      LOCK.synchronize{@template_map.to_a}.each do |ext, klass|
        res << ext if template_class == klass
      end
      LOCK.synchronize{@lazy_map.to_a}.each do |ext, choices|
        res << ext if LOCK.synchronize{choices.dup}.any? { |klass, file| template_class.to_s == klass }
      end
      res.uniq!
      res
    end

    private

    def lazy?(ext)
      ext = ext.downcase
      LOCK.synchronize{@lazy_map.has_key?(ext) && !@lazy_map[ext].empty?}
    end

    def lookup(ext)
      LOCK.synchronize{@template_map[ext]} || lazy_load(ext)
    end

    def register_defined_classes(class_names, pattern)
      class_names.each do |class_name|
        template_class = constant_defined?(class_name)
        if template_class
          register(template_class, pattern)
          yield template_class if block_given?
        end
      end
    end

    def lazy_load(pattern)
      choices = LOCK.synchronize{@lazy_map[pattern].dup}

      # Check if a template class is already present
      register_defined_classes(choices.map(&:first), pattern) do |template_class|
        return template_class
      end

      first_failure = nil

      # Load in order
      choices.each do |class_name, file|
        begin
          require file
          # It's safe to eval() here because constant_defined? will
          # raise NameError on invalid constant names
          template_class = eval(class_name)
        rescue LoadError => ex
          first_failure ||= ex
        else
          register(template_class, pattern)
          return template_class
        end
      end

      raise first_failure
    end

    # The proper behavior (in MRI) for autoload? is to
    # return `false` when the constant/file has been
    # explicitly required.
    #
    # However, in JRuby it returns `true` even after it's
    # been required. In that case it turns out that `defined?`
    # returns `"constant"` if it exists and `nil` when it doesn't.
    # This is actually a second bug: `defined?` should resolve
    # autoload (aka. actually try to require the file).
    #
    # We use the second bug in order to resolve the first bug.

    def constant_defined?(name)
      name.split('::').inject(Object) do |scope, n|
        return false if scope.autoload?(n) || !scope.const_defined?(n)
        scope.const_get(n)
      end
    end
  end

  # Private internal class for finalized mappings, which are frozen and
  # cannot be modified.
  class FinalizedMapping < BaseMapping
    # Set the template map to use.  The template map should already
    # be frozen, but this is an internal class, so it does not
    # explicitly check for that.
    def initialize(template_map)
      @template_map = template_map
      freeze
    end

    # Returns receiver, since instances are always frozen.
    def dup
      self
    end

    # Returns receiver, since instances are always frozen.
    def clone(freeze: false)
      self
    end

    # Return whether the given file extension has been registered.
    def registered?(ext)
      @template_map.has_key?(ext.downcase)
    end

    # Returns an aarry of all extensions the template class will
    # be used for.
    def extensions_for(template_class)
      res = []
      @template_map.each do |ext, klass|
        res << ext if template_class == klass
      end
      res.uniq!
      res
    end

    private

    def lookup(ext)
      @template_map[ext]
    end
  end
  private_constant :FinalizedMapping
end