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