summaryrefslogtreecommitdiff
path: root/lib/coderay/helpers/plugin.rb
blob: 9a724ffff39f54988953edad3e687cfa5938eb4e (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
module CodeRay
  
  # = PluginHost
  #
  # A simple subclass/subfolder plugin system.
  #
  # Example:
  #  class Generators
  #    extend 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? String
      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
    
    class << self
      
      # Adds the module/class to the PLUGIN_HOSTS list.
      def extended mod
        PLUGIN_HOSTS << mod
      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)
      end
      @plugin_path ||= ''
    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, or return the default plugin.
    #
    # See also map.
    #
    #  class MyColorHost < PluginHost
    #    map :navy => :dark_blue
    #    default :gray
    #  end
    #  
    #  MyColorHost.default  # loads and returns the Gray plugin
    def default id = nil
      if id
        id = validate_id id
        raise "The default plugin can't be named \"default\"." if id == :default
        plugin_hash[:default] = id
      else
        load :default
      end
    end
    
    # Every plugin must register itself for +id+ by calling register_for,
    # which calls this method.
    #
    # See Plugin#register_for.
    def register plugin, id
      plugin_hash[validate_id(id)] = plugin
    end
    
    # A Hash of plugion_id => Plugin pairs.
    def plugin_hash
      @plugin_hash ||= (@plugin_hash = make_plugin_hash).tap { load_plugin_map }
    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').to_sym
      end
    end
    
    # Returns an array of all Plugins.
    # 
    # Note: This loads all plugins using load_all.
    def all_plugins
      load_all
      plugin_hash.values.grep(Class)
    end
    
    # Loads the map file (see map).
    #
    # This is done automatically when plugin_path is called.
    def load_plugin_map
      mapfile = path_to '_map'
      if File.exist? mapfile
        require mapfile
        true
      else
        false
      end
    end
    
  protected
    
    # Return a plugin hash that automatically loads plugins.
    def make_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?(:default)
            h[:default]
          else
            raise PluginNotFound, '%p could not load plugin %p: %s' % [self, id, boom]
          end
        else
          # Plugin should have registered by now
          if h.has_key? id
            h[id]
          else
            raise PluginNotFound, "No #{self.name} plugin for #{id.inspect} found in #{path}."
          end
        end
      end
    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 valid plugin ID String, or returns +nil+.
    #
    # Raises +ArgumentError+ for all other objects, or if the
    # given String includes non-alphanumeric characters (\W).
    def validate_id id
      case id
      when Symbol
        id.to_s
      when String
        if id[/\w+/] == id
          id.downcase
        else
          raise ArgumentError, "Invalid id given: #{id}"
        end
      else
        raise ArgumentError, "Symbol or String 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
    
    attr_reader :plugin_id
    
    # Register this class for the given +id+.
    # 
    # Example:
    #   class MyPlugin < PluginHost::BaseClass
    #     register_for :my_id
    #     ...
    #   end
    #
    # See PluginHost.register.
    def register_for id
      @plugin_id = id
      plugin_host.register self, id
    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.is_a? PluginHost
        const_set :PLUGIN_HOST, host
      end
      self::PLUGIN_HOST
    end
    
    def aliases
      plugin_host.plugin_hash.inject [] do |aliases, (key, _)|
        aliases << key if plugin_host[key] == self
        aliases
      end
    end
    
  end
  
end