Chris@0
|
1 module CodeRay
|
Chris@0
|
2
|
Chris@0
|
3 # = PluginHost
|
Chris@0
|
4 #
|
Chris@0
|
5 # A simple subclass plugin system.
|
Chris@0
|
6 #
|
Chris@0
|
7 # Example:
|
Chris@0
|
8 # class Generators < PluginHost
|
Chris@0
|
9 # plugin_path 'app/generators'
|
Chris@0
|
10 # end
|
Chris@0
|
11 #
|
Chris@0
|
12 # class Generator
|
Chris@0
|
13 # extend Plugin
|
Chris@0
|
14 # PLUGIN_HOST = Generators
|
Chris@0
|
15 # end
|
Chris@0
|
16 #
|
Chris@0
|
17 # class FancyGenerator < Generator
|
Chris@0
|
18 # register_for :fancy
|
Chris@0
|
19 # end
|
Chris@0
|
20 #
|
Chris@0
|
21 # Generators[:fancy] #-> FancyGenerator
|
Chris@0
|
22 # # or
|
Chris@0
|
23 # CodeRay.require_plugin 'Generators/fancy'
|
Chris@0
|
24 module PluginHost
|
Chris@0
|
25
|
Chris@0
|
26 # Raised if Encoders::[] fails because:
|
Chris@0
|
27 # * a file could not be found
|
Chris@0
|
28 # * the requested Encoder is not registered
|
Chris@0
|
29 PluginNotFound = Class.new Exception
|
Chris@0
|
30 HostNotFound = Class.new Exception
|
Chris@0
|
31
|
Chris@0
|
32 PLUGIN_HOSTS = []
|
Chris@0
|
33 PLUGIN_HOSTS_BY_ID = {} # dummy hash
|
Chris@0
|
34
|
Chris@0
|
35 # Loads all plugins using list and load.
|
Chris@0
|
36 def load_all
|
Chris@0
|
37 for plugin in list
|
Chris@0
|
38 load plugin
|
Chris@0
|
39 end
|
Chris@0
|
40 end
|
Chris@0
|
41
|
Chris@0
|
42 # Returns the Plugin for +id+.
|
Chris@0
|
43 #
|
Chris@0
|
44 # Example:
|
Chris@0
|
45 # yaml_plugin = MyPluginHost[:yaml]
|
Chris@0
|
46 def [] id, *args, &blk
|
Chris@0
|
47 plugin = validate_id(id)
|
Chris@0
|
48 begin
|
Chris@0
|
49 plugin = plugin_hash.[] plugin, *args, &blk
|
Chris@0
|
50 end while plugin.is_a? Symbol
|
Chris@0
|
51 plugin
|
Chris@0
|
52 end
|
Chris@0
|
53
|
Chris@0
|
54 # Alias for +[]+.
|
Chris@0
|
55 alias load []
|
Chris@0
|
56
|
Chris@0
|
57 def require_helper plugin_id, helper_name
|
Chris@0
|
58 path = path_to File.join(plugin_id, helper_name)
|
Chris@0
|
59 require path
|
Chris@0
|
60 end
|
Chris@0
|
61
|
Chris@0
|
62 class << self
|
Chris@0
|
63
|
Chris@0
|
64 # Adds the module/class to the PLUGIN_HOSTS list.
|
Chris@0
|
65 def extended mod
|
Chris@0
|
66 PLUGIN_HOSTS << mod
|
Chris@0
|
67 end
|
Chris@0
|
68
|
Chris@0
|
69 # Warns you that you should not #include this module.
|
Chris@0
|
70 def included mod
|
Chris@0
|
71 warn "#{name} should not be included. Use extend."
|
Chris@0
|
72 end
|
Chris@0
|
73
|
Chris@0
|
74 # Find the PluginHost for host_id.
|
Chris@0
|
75 def host_by_id host_id
|
Chris@0
|
76 unless PLUGIN_HOSTS_BY_ID.default_proc
|
Chris@0
|
77 ph = Hash.new do |h, a_host_id|
|
Chris@0
|
78 for host in PLUGIN_HOSTS
|
Chris@0
|
79 h[host.host_id] = host
|
Chris@0
|
80 end
|
Chris@0
|
81 h.fetch a_host_id, nil
|
Chris@0
|
82 end
|
Chris@0
|
83 PLUGIN_HOSTS_BY_ID.replace ph
|
Chris@0
|
84 end
|
Chris@0
|
85 PLUGIN_HOSTS_BY_ID[host_id]
|
Chris@0
|
86 end
|
Chris@0
|
87
|
Chris@0
|
88 end
|
Chris@0
|
89
|
Chris@0
|
90 # The path where the plugins can be found.
|
Chris@0
|
91 def plugin_path *args
|
Chris@0
|
92 unless args.empty?
|
Chris@0
|
93 @plugin_path = File.expand_path File.join(*args)
|
Chris@0
|
94 load_map
|
Chris@0
|
95 end
|
Chris@0
|
96 @plugin_path
|
Chris@0
|
97 end
|
Chris@0
|
98
|
Chris@0
|
99 # The host's ID.
|
Chris@0
|
100 #
|
Chris@0
|
101 # If PLUGIN_HOST_ID is not set, it is simply the class name.
|
Chris@0
|
102 def host_id
|
Chris@0
|
103 if self.const_defined? :PLUGIN_HOST_ID
|
Chris@0
|
104 self::PLUGIN_HOST_ID
|
Chris@0
|
105 else
|
Chris@0
|
106 name
|
Chris@0
|
107 end
|
Chris@0
|
108 end
|
Chris@0
|
109
|
Chris@0
|
110 # Map a plugin_id to another.
|
Chris@0
|
111 #
|
Chris@0
|
112 # Usage: Put this in a file plugin_path/_map.rb.
|
Chris@0
|
113 #
|
Chris@0
|
114 # class MyColorHost < PluginHost
|
Chris@0
|
115 # map :navy => :dark_blue,
|
Chris@0
|
116 # :maroon => :brown,
|
Chris@0
|
117 # :luna => :moon
|
Chris@0
|
118 # end
|
Chris@0
|
119 def map hash
|
Chris@0
|
120 for from, to in hash
|
Chris@0
|
121 from = validate_id from
|
Chris@0
|
122 to = validate_id to
|
Chris@0
|
123 plugin_hash[from] = to unless plugin_hash.has_key? from
|
Chris@0
|
124 end
|
Chris@0
|
125 end
|
Chris@0
|
126
|
Chris@0
|
127 # Define the default plugin to use when no plugin is found
|
Chris@0
|
128 # for a given id.
|
Chris@0
|
129 #
|
Chris@0
|
130 # See also map.
|
Chris@0
|
131 #
|
Chris@0
|
132 # class MyColorHost < PluginHost
|
Chris@0
|
133 # map :navy => :dark_blue
|
Chris@0
|
134 # default :gray
|
Chris@0
|
135 # end
|
Chris@0
|
136 def default id = nil
|
Chris@0
|
137 if id
|
Chris@0
|
138 id = validate_id id
|
Chris@0
|
139 plugin_hash[nil] = id
|
Chris@0
|
140 else
|
Chris@0
|
141 plugin_hash[nil]
|
Chris@0
|
142 end
|
Chris@0
|
143 end
|
Chris@0
|
144
|
Chris@0
|
145 # Every plugin must register itself for one or more
|
Chris@0
|
146 # +ids+ by calling register_for, which calls this method.
|
Chris@0
|
147 #
|
Chris@0
|
148 # See Plugin#register_for.
|
Chris@0
|
149 def register plugin, *ids
|
Chris@0
|
150 for id in ids
|
Chris@0
|
151 unless id.is_a? Symbol
|
Chris@0
|
152 raise ArgumentError,
|
Chris@0
|
153 "id must be a Symbol, but it was a #{id.class}"
|
Chris@0
|
154 end
|
Chris@0
|
155 plugin_hash[validate_id(id)] = plugin
|
Chris@0
|
156 end
|
Chris@0
|
157 end
|
Chris@0
|
158
|
Chris@0
|
159 # A Hash of plugion_id => Plugin pairs.
|
Chris@0
|
160 def plugin_hash
|
Chris@0
|
161 @plugin_hash ||= create_plugin_hash
|
Chris@0
|
162 end
|
Chris@0
|
163
|
Chris@0
|
164 # Returns an array of all .rb files in the plugin path.
|
Chris@0
|
165 #
|
Chris@0
|
166 # The extension .rb is not included.
|
Chris@0
|
167 def list
|
Chris@0
|
168 Dir[path_to('*')].select do |file|
|
Chris@0
|
169 File.basename(file)[/^(?!_)\w+\.rb$/]
|
Chris@0
|
170 end.map do |file|
|
Chris@0
|
171 File.basename file, '.rb'
|
Chris@0
|
172 end
|
Chris@0
|
173 end
|
Chris@0
|
174
|
Chris@0
|
175 # Makes a map of all loaded plugins.
|
Chris@0
|
176 def inspect
|
Chris@0
|
177 map = plugin_hash.dup
|
Chris@0
|
178 map.each do |id, plugin|
|
Chris@0
|
179 map[id] = plugin.to_s[/(?>\w+)$/]
|
Chris@0
|
180 end
|
Chris@0
|
181 "#{name}[#{host_id}]#{map.inspect}"
|
Chris@0
|
182 end
|
Chris@0
|
183
|
Chris@0
|
184 protected
|
Chris@0
|
185 # Created a new plugin list and stores it to @plugin_hash.
|
Chris@0
|
186 def create_plugin_hash
|
Chris@0
|
187 @plugin_hash =
|
Chris@0
|
188 Hash.new do |h, plugin_id|
|
Chris@0
|
189 id = validate_id(plugin_id)
|
Chris@0
|
190 path = path_to id
|
Chris@0
|
191 begin
|
Chris@0
|
192 require path
|
Chris@0
|
193 rescue LoadError => boom
|
Chris@0
|
194 if h.has_key? nil # default plugin
|
Chris@0
|
195 h[id] = h[nil]
|
Chris@0
|
196 else
|
Chris@0
|
197 raise PluginNotFound, 'Could not load plugin %p: %s' % [id, boom]
|
Chris@0
|
198 end
|
Chris@0
|
199 else
|
Chris@0
|
200 # Plugin should have registered by now
|
Chris@0
|
201 unless h.has_key? id
|
Chris@0
|
202 raise PluginNotFound,
|
Chris@0
|
203 "No #{self.name} plugin for #{id.inspect} found in #{path}."
|
Chris@0
|
204 end
|
Chris@0
|
205 end
|
Chris@0
|
206 h[id]
|
Chris@0
|
207 end
|
Chris@0
|
208 end
|
Chris@0
|
209
|
Chris@0
|
210 # Loads the map file (see map).
|
Chris@0
|
211 #
|
Chris@0
|
212 # This is done automatically when plugin_path is called.
|
Chris@0
|
213 def load_map
|
Chris@0
|
214 mapfile = path_to '_map'
|
Chris@0
|
215 if File.exist? mapfile
|
Chris@0
|
216 require mapfile
|
Chris@0
|
217 elsif $VERBOSE
|
Chris@0
|
218 warn 'no _map.rb found for %s' % name
|
Chris@0
|
219 end
|
Chris@0
|
220 end
|
Chris@0
|
221
|
Chris@0
|
222 # Returns the Plugin for +id+.
|
Chris@0
|
223 # Use it like Hash#fetch.
|
Chris@0
|
224 #
|
Chris@0
|
225 # Example:
|
Chris@0
|
226 # yaml_plugin = MyPluginHost[:yaml, :default]
|
Chris@0
|
227 def fetch id, *args, &blk
|
Chris@0
|
228 plugin_hash.fetch validate_id(id), *args, &blk
|
Chris@0
|
229 end
|
Chris@0
|
230
|
Chris@0
|
231 # Returns the expected path to the plugin file for the given id.
|
Chris@0
|
232 def path_to plugin_id
|
Chris@0
|
233 File.join plugin_path, "#{plugin_id}.rb"
|
Chris@0
|
234 end
|
Chris@0
|
235
|
Chris@0
|
236 # Converts +id+ to a Symbol if it is a String,
|
Chris@0
|
237 # or returns +id+ if it already is a Symbol.
|
Chris@0
|
238 #
|
Chris@0
|
239 # Raises +ArgumentError+ for all other objects, or if the
|
Chris@0
|
240 # given String includes non-alphanumeric characters (\W).
|
Chris@0
|
241 def validate_id id
|
Chris@0
|
242 if id.is_a? Symbol or id.nil?
|
Chris@0
|
243 id
|
Chris@0
|
244 elsif id.is_a? String
|
Chris@0
|
245 if id[/\w+/] == id
|
Chris@0
|
246 id.downcase.to_sym
|
Chris@0
|
247 else
|
Chris@0
|
248 raise ArgumentError, "Invalid id: '#{id}' given."
|
Chris@0
|
249 end
|
Chris@0
|
250 else
|
Chris@0
|
251 raise ArgumentError,
|
Chris@0
|
252 "String or Symbol expected, but #{id.class} given."
|
Chris@0
|
253 end
|
Chris@0
|
254 end
|
Chris@0
|
255
|
Chris@0
|
256 end
|
Chris@0
|
257
|
Chris@0
|
258
|
Chris@0
|
259 # = Plugin
|
Chris@0
|
260 #
|
Chris@0
|
261 # Plugins have to include this module.
|
Chris@0
|
262 #
|
Chris@0
|
263 # IMPORTANT: use extend for this module.
|
Chris@0
|
264 #
|
Chris@0
|
265 # Example: see PluginHost.
|
Chris@0
|
266 module Plugin
|
Chris@0
|
267
|
Chris@0
|
268 def included mod
|
Chris@0
|
269 warn "#{name} should not be included. Use extend."
|
Chris@0
|
270 end
|
Chris@0
|
271
|
Chris@0
|
272 # Register this class for the given langs.
|
Chris@0
|
273 # Example:
|
Chris@0
|
274 # class MyPlugin < PluginHost::BaseClass
|
Chris@0
|
275 # register_for :my_id
|
Chris@0
|
276 # ...
|
Chris@0
|
277 # end
|
Chris@0
|
278 #
|
Chris@0
|
279 # See PluginHost.register.
|
Chris@0
|
280 def register_for *ids
|
Chris@0
|
281 plugin_host.register self, *ids
|
Chris@0
|
282 end
|
Chris@0
|
283
|
Chris@0
|
284 # Returns the title of the plugin, or sets it to the
|
Chris@0
|
285 # optional argument +title+.
|
Chris@0
|
286 def title title = nil
|
Chris@0
|
287 if title
|
Chris@0
|
288 @title = title.to_s
|
Chris@0
|
289 else
|
Chris@0
|
290 @title ||= name[/([^:]+)$/, 1]
|
Chris@0
|
291 end
|
Chris@0
|
292 end
|
Chris@0
|
293
|
Chris@0
|
294 # The host for this Plugin class.
|
Chris@0
|
295 def plugin_host host = nil
|
Chris@0
|
296 if host and not host.is_a? PluginHost
|
Chris@0
|
297 raise ArgumentError,
|
Chris@0
|
298 "PluginHost expected, but #{host.class} given."
|
Chris@0
|
299 end
|
Chris@0
|
300 self.const_set :PLUGIN_HOST, host if host
|
Chris@0
|
301 self::PLUGIN_HOST
|
Chris@0
|
302 end
|
Chris@0
|
303
|
Chris@0
|
304 # Require some helper files.
|
Chris@0
|
305 #
|
Chris@0
|
306 # Example:
|
Chris@0
|
307 #
|
Chris@0
|
308 # class MyPlugin < PluginHost::BaseClass
|
Chris@0
|
309 # register_for :my_id
|
Chris@0
|
310 # helper :my_helper
|
Chris@0
|
311 #
|
Chris@0
|
312 # The above example loads the file myplugin/my_helper.rb relative to the
|
Chris@0
|
313 # file in which MyPlugin was defined.
|
Chris@0
|
314 #
|
Chris@0
|
315 # You can also load a helper from a different plugin:
|
Chris@0
|
316 #
|
Chris@0
|
317 # helper 'other_plugin/helper_name'
|
Chris@0
|
318 def helper *helpers
|
Chris@0
|
319 for helper in helpers
|
Chris@0
|
320 if helper.is_a?(String) && helper[/\//]
|
Chris@0
|
321 self::PLUGIN_HOST.require_helper $`, $'
|
Chris@0
|
322 else
|
Chris@0
|
323 self::PLUGIN_HOST.require_helper plugin_id, helper.to_s
|
Chris@0
|
324 end
|
Chris@0
|
325 end
|
Chris@0
|
326 end
|
Chris@0
|
327
|
Chris@0
|
328 # Returns the pulgin id used by the engine.
|
Chris@0
|
329 def plugin_id
|
Chris@0
|
330 name[/\w+$/].downcase
|
Chris@0
|
331 end
|
Chris@0
|
332
|
Chris@0
|
333 end
|
Chris@0
|
334
|
Chris@0
|
335 # Convenience method for plugin loading.
|
Chris@0
|
336 # The syntax used is:
|
Chris@0
|
337 #
|
Chris@0
|
338 # CodeRay.require_plugin '<Host ID>/<Plugin ID>'
|
Chris@0
|
339 #
|
Chris@0
|
340 # Returns the loaded plugin.
|
Chris@0
|
341 def self.require_plugin path
|
Chris@0
|
342 host_id, plugin_id = path.split '/', 2
|
Chris@0
|
343 host = PluginHost.host_by_id(host_id)
|
Chris@0
|
344 raise PluginHost::HostNotFound,
|
Chris@0
|
345 "No host for #{host_id.inspect} found." unless host
|
Chris@0
|
346 host.load plugin_id
|
Chris@0
|
347 end
|
Chris@0
|
348
|
Chris@0
|
349 end |