summaryrefslogtreecommitdiff
path: root/lib/coderay/helpers/plugin.rb
blob: ada5ae766a899e0914fc7a37a3e55484ade6e58a (plain)
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
module CodeRay
  
  # = PluginHost
  #
  # A simple subclass/subfolder plugin system.
  #
  # Example:
  #  class Generators < PluginHost
  #    plugin_path 'app/generators'
  #  end
  #  
  #  class Generator
  #    extend Plugin
  #    PLUGIN_HOST = Generators
  #  end
  #  
  #  class FancyGenerator < Generator
  #    register_for :fancy
  #  end
  #  
  #  Generators[:fancy]  #-> FancyGenerator
  #  # or
  #  CodeRay.require_plugin 'Generators/fancy'
  #  # or
  #  Generators::Fancy
  module PluginHost
    
    # Raised if Encoders::[] fails because:
    # * a file could not be found
    # * the requested Plugin is not registered
    PluginNotFound = Class.new LoadError
    HostNotFound = Class.new LoadError
    
    PLUGIN_HOSTS = []
    PLUGIN_HOSTS_BY_ID = {}  # dummy hash
    
    # Loads all plugins using list and load.
    def load_all
      for plugin in list
        load plugin
      end
    end
    
    # Returns the Plugin for +id+.
    #
    # Example:
    #  yaml_plugin = MyPluginHost[:yaml]
    def [] id, *args, &blk
      plugin = validate_id(id)
      begin
        plugin = plugin_hash.[] plugin, *args, &blk
      end while plugin.is_a? Symbol
      plugin
    end
    
    alias load []
    
    # Tries to +load+ the missing plugin by translating +const+ to the
    # underscore form (eg. LinesOfCode becomes lines_of_code).
    def const_missing const
      id = const.to_s.
        gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
        gsub(/([a-z\d])([A-Z])/,'\1_\2').
        downcase
      load id
    end
    
    # Loads a helper file for the given +plugin_id+ and +helper_name+.
    # Helpers are located in a folder with the name of the plugin,
    # so if you have a plugin called "plugin" inside a folder named "plugins"
    # and a helper named "helper", this method will try to load
    # "plugins/plugin/helper".
    def require_helper plugin_id, helper_name
      path = path_to File.join(plugin_id, helper_name)
      require path
    end
    
    class << self
      
      # Adds the module/class to the PLUGIN_HOSTS list.
      def extended mod
        PLUGIN_HOSTS << mod
      end
      
      # Warns you that you should not #include this module.
      def included mod
        warn "#{name} should not be included. Use extend."
      end
      
      # Find the PluginHost for host_id.
      def host_by_id host_id
        unless PLUGIN_HOSTS_BY_ID.default_proc
          ph = Hash.new do |h, a_host_id|
            for host in PLUGIN_HOSTS
              h[host.host_id] = host
            end
            h.fetch a_host_id, nil
          end
          PLUGIN_HOSTS_BY_ID.replace ph
        end
        PLUGIN_HOSTS_BY_ID[host_id]
      end
      
    end
    
    # The path where the plugins can be found.
    def plugin_path *args
      unless args.empty?
        @plugin_path = File.expand_path File.join(*args)
        load_map
      end
      @plugin_path
    end
    
    # The host's ID.
    #
    # If PLUGIN_HOST_ID is not set, it is simply the class name.
    def host_id
      if self.const_defined? :PLUGIN_HOST_ID
        self::PLUGIN_HOST_ID
      else
        name
      end
    end
    
    # Map a plugin_id to another.
    #
    # Usage: Put this in a file plugin_path/_map.rb.
    #
    #  class MyColorHost < PluginHost
    #    map :navy => :dark_blue,
    #      :maroon => :brown,
    #      :luna => :moon
    #  end
    def map hash
      for from, to in hash
        from = validate_id from
        to = validate_id to
        plugin_hash[from] = to unless plugin_hash.has_key? from
      end
    end
    
    # Define the default plugin to use when no plugin is found
    # for a given id.
    #
    # See also map.
    #
    #  class MyColorHost < PluginHost
    #    map :navy => :dark_blue
    #    default :gray
    #  end
    def default id = nil
      if id
        id = validate_id id
        plugin_hash[nil] = id
      else
        plugin_hash[nil]
      end
    end
    
    # Every plugin must register itself for one or more
    # +ids+ by calling register_for, which calls this method.
    #
    # See Plugin#register_for.
    def register plugin, *ids
      for id in ids
        unless id.is_a? Symbol
          raise ArgumentError,
            "id must be a Symbol, but it was a #{id.class}"
        end
        plugin_hash[validate_id(id)] = plugin
      end
    end
    
    # A Hash of plugion_id => Plugin pairs.
    def plugin_hash
      @plugin_hash ||= create_plugin_hash
    end
    
    # Returns an array of all .rb files in the plugin path.
    #
    # The extension .rb is not included.
    def list
      Dir[path_to('*')].select do |file|
        File.basename(file)[/^(?!_)\w+\.rb$/]
      end.map do |file|
        File.basename file, '.rb'
      end
    end
    
    # Makes a map of all loaded plugins.
    def inspect
      map = plugin_hash.dup
      map.each do |id, plugin|
        map[id] = plugin.to_s[/(?>\w+)$/]
      end
      "#{name}[#{host_id}]#{map.inspect}"
    end
    
  protected
    # Created a new plugin list and stores it to @plugin_hash.
    def create_plugin_hash
      @plugin_hash =
        Hash.new do |h, plugin_id|
          id = validate_id(plugin_id)
          path = path_to id
          begin
            require path
          rescue LoadError => boom
            if h.has_key? nil  # default plugin
              h[id] = h[nil]
            else
              raise PluginNotFound, 'Could not load plugin %p: %s' % [id, boom]
            end
          else
            # Plugin should have registered by now
            unless h.has_key? id
              raise PluginNotFound,
                "No #{self.name} plugin for #{id.inspect} found in #{path}."
            end
          end
          h[id]
        end
    end
    
    # Loads the map file (see map).
    #
    # This is done automatically when plugin_path is called.
    def load_map
      mapfile = path_to '_map'
      if File.exist? mapfile
        require mapfile
      elsif $VERBOSE
        warn 'no _map.rb found for %s' % name
      end
    end
    
    # Returns the Plugin for +id+.
    # Use it like Hash#fetch.
    #
    # Example:
    #  yaml_plugin = MyPluginHost[:yaml, :default]
    def fetch id, *args, &blk
      plugin_hash.fetch validate_id(id), *args, &blk
    end
    
    # Returns the expected path to the plugin file for the given id.
    def path_to plugin_id
      File.join plugin_path, "#{plugin_id}.rb"
    end
    
    # Converts +id+ to a Symbol if it is a String,
    # or returns +id+ if it already is a Symbol.
    #
    # Raises +ArgumentError+ for all other objects, or if the
    # given String includes non-alphanumeric characters (\W).
    def validate_id id
      if id.is_a? Symbol or id.nil?
        id
      elsif id.is_a? String
        if id[/\w+/] == id
          id.downcase.to_sym
        else
          warn "Invalid id: '#{id}' given."
          ''
        end
      else
        raise ArgumentError,
          "String or Symbol expected, but #{id.class} given."
      end
    end
    
  end
  
  
  # = Plugin
  #
  #  Plugins have to include this module.
  #
  #  IMPORTANT: use extend for this module.
  #
  #  See CodeRay::PluginHost for examples.
  module Plugin
    
    def included mod  # :nodoc:
      warn "#{name} should not be included. Use extend."
    end
    
    # Register this class for the given langs.
    # Example:
    #   class MyPlugin < PluginHost::BaseClass
    #     register_for :my_id
    #     ...
    #   end
    #
    # See PluginHost.register.
    def register_for *ids
      @plugin_id = ids.first
      plugin_host.register self, *ids
    end
    
    # Returns the title of the plugin, or sets it to the
    # optional argument +title+.
    def title title = nil
      if title
        @title = title.to_s
      else
        @title ||= name[/([^:]+)$/, 1]
      end
    end
    
    # The PluginHost for this Plugin class.
    def plugin_host host = nil
      if host and not host.is_a? PluginHost
        raise ArgumentError,
          "PluginHost expected, but #{host.class} given."
      end
      self.const_set :PLUGIN_HOST, host if host
      self::PLUGIN_HOST
    end
    
    # Require some helper files.
    #
    # Example:
    #
    #  class MyPlugin < PluginHost::BaseClass
    #     register_for :my_id
    #     helper :my_helper
    #
    # The above example loads the file myplugin/my_helper.rb relative to the
    # file in which MyPlugin was defined.
    # 
    # You can also load a helper from a different plugin:
    # 
    #  helper 'other_plugin/helper_name'
    def helper *helpers
      for helper in helpers
        if helper.is_a?(String) && helper[/\//]
          self::PLUGIN_HOST.require_helper $`, $'
        else
          self::PLUGIN_HOST.require_helper plugin_id.to_s, helper.to_s
        end
      end
    end
    
    # Returns the plugin id used by the engine.
    def plugin_id
      @plugin_id || name[/\w+$/].downcase
    end
    
  end
  
  # Convenience method for plugin loading.
  # The syntax used is:
  #
  #  CodeRay.require_plugin '<Host ID>/<Plugin ID>'
  #
  # Returns the loaded plugin.
  def self.require_plugin path
    host_id, plugin_id = path.split '/', 2
    host = PluginHost.host_by_id(host_id)
    raise PluginHost::HostNotFound,
      "No host for #{host_id.inspect} found." unless host
    host.load plugin_id
  end
  
end