|
- # -*- coding: utf-8 -*-
- """
- Contains definition for a plugin protocol and other utiltities.
- """
- from hyde.exceptions import HydeException
- from hyde.util import first_match, discover_executable
- from hyde.model import Expando
-
- import abc
- from functools import partial
- import fnmatch
- import os
- import re
- import subprocess
- import sys
-
- from commando.util import getLoggerWithNullHandler, load_python_object
- from fswrap import File
-
- logger = getLoggerWithNullHandler('hyde.engine')
-
- # Plugins have been reorganized. Map old plugin paths to new.
- PLUGINS_OLD_AND_NEW = {
- "hyde.ext.plugins.less.LessCSSPlugin":
- "hyde.ext.plugins.css.LessCSSPlugin",
- "hyde.ext.plugins.stylus.StylusPlugin":
- "hyde.ext.plugins.css.StylusPlugin",
- "hyde.ext.plugins.jpegoptim.JPEGOptimPlugin":
- "hyde.ext.plugins.images.JPEGOptimPlugin",
- "hyde.ext.plugins.optipng.OptiPNGPlugin":
- "hyde.ext.plugins.images.OptiPNGPlugin",
- "hyde.ext.plugins.jpegtran.JPEGTranPlugin":
- "hyde.ext.plugins.images.JPEGTranPlugin",
- "hyde.ext.plugins.uglify.UglifyPlugin":
- "hyde.ext.plugins.js.UglifyPlugin",
- "hyde.ext.plugins.requirejs.RequireJSPlugin":
- "hyde.ext.plugins.js.RequireJSPlugin",
- "hyde.ext.plugins.coffee.CoffeePlugin":
- "hyde.ext.plugins.js.CoffeePlugin",
- "hyde.ext.plugins.sorter.SorterPlugin":
- "hyde.ext.plugins.meta.SorterPlugin",
- "hyde.ext.plugins.grouper.GrouperPlugin":
- "hyde.ext.plugins.meta.GrouperPlugin",
- "hyde.ext.plugins.tagger.TaggerPlugin":
- "hyde.ext.plugins.meta.TaggerPlugin",
- "hyde.ext.plugins.auto_extend.AutoExtendPlugin":
- "hyde.ext.plugins.meta.AutoExtendPlugin",
- "hyde.ext.plugins.folders.FlattenerPlugin":
- "hyde.ext.plugins.structure.FlattenerPlugin",
- "hyde.ext.plugins.combine.CombinePlugin":
- "hyde.ext.plugins.structure.CombinePlugin",
- "hyde.ext.plugins.paginator.PaginatorPlugin":
- "hyde.ext.plugins.structure.PaginatorPlugin",
- "hyde.ext.plugins.blockdown.BlockdownPlugin":
- "hyde.ext.plugins.text.BlockdownPlugin",
- "hyde.ext.plugins.markings.MarkingsPlugin":
- "hyde.ext.plugins.text.MarkingsPlugin",
- "hyde.ext.plugins.markings.ReferencePlugin":
- "hyde.ext.plugins.text.ReferencePlugin",
- "hyde.ext.plugins.syntext.SyntextPlugin":
- "hyde.ext.plugins.text.SyntextPlugin",
- "hyde.ext.plugins.textlinks.TextlinksPlugin":
- "hyde.ext.plugins.text.TextlinksPlugin",
- "hyde.ext.plugins.git.GitDatesPlugin":
- "hyde.ext.plugins.vcs.GitDatesPlugin"
- }
-
-
- class PluginProxy(object):
-
- """
- A proxy class to raise events in registered plugins
- """
-
- def __init__(self, site):
- super(PluginProxy, self).__init__()
- self.site = site
-
- def __getattr__(self, method_name):
- if hasattr(Plugin, method_name):
- def __call_plugins__(*args):
- res = None
- if self.site.plugins:
- for plugin in self.site.plugins:
- if hasattr(plugin, method_name):
- checker = getattr(
- plugin, 'should_call__' + method_name)
- if checker(*args):
- function = getattr(plugin, method_name)
- try:
- res = function(*args)
- except:
- HydeException.reraise(
- 'Error occured when calling %s' %
- plugin.plugin_name, sys.exc_info())
- targs = list(args)
- if len(targs):
- last = targs.pop()
- res = res if res else last
- targs.append(res)
- args = tuple(targs)
- return res
-
- return __call_plugins__
- raise HydeException(
- "Unknown plugin method [%s] called." % method_name)
-
-
- class Plugin(object):
-
- """
- The plugin protocol
- """
- __metaclass__ = abc.ABCMeta
-
- def __init__(self, site):
- super(Plugin, self).__init__()
- self.site = site
- self.logger = getLoggerWithNullHandler(
- 'hyde.engine.%s' % self.__class__.__name__)
- self.template = None
-
- def template_loaded(self, template):
- """
- Called when the template for the site has been identified.
-
- Handles the template loaded event to keep
- a reference to the template object.
- """
- self.template = template
-
- def __getattribute__(self, name):
- """
- Syntactic sugar for template methods
- """
- result = None
- if name.startswith('t_') and self.template:
- attr = name[2:]
- if hasattr(self.template, attr):
- result = self.template[attr]
- elif attr.endswith('_close_tag'):
- tag = attr.replace('_close_tag', '')
- result = partial(self.template.get_close_tag, tag)
- elif attr.endswith('_open_tag'):
- tag = attr.replace('_open_tag', '')
- result = partial(self.template.get_open_tag, tag)
- elif name.startswith('should_call__'):
- (_, _, method) = name.rpartition('__')
- if (method in ('begin_text_resource', 'text_resource_complete',
- 'begin_binary_resource',
- 'binary_resource_complete')):
- result = self._file_filter
- elif (method in ('begin_node', 'node_complete')):
- result = self._dir_filter
- else:
- def always_true(*args, **kwargs):
- return True
- result = always_true
-
- return result if result else super(Plugin, self).__getattribute__(name)
-
- @property
- def settings(self):
- """
- The settings for this plugin the site config.
- """
-
- opts = Expando({})
- try:
- opts = getattr(self.site.config, self.plugin_name)
- except AttributeError:
- pass
- return opts
-
- @property
- def plugin_name(self):
- """
- The name of the plugin. Makes an intelligent guess.
-
- This is used to lookup the settings for the plugin.
- """
-
- return self.__class__.__name__.replace('Plugin', '').lower()
-
- def begin_generation(self):
- """
- Called when generation is about to take place.
- """
- pass
-
- def begin_site(self):
- """
- Called when the site is loaded completely. This implies that all the
- nodes and resources have been identified and are accessible in the
- site variable.
- """
- pass
-
- def begin_node(self, node):
- """
- Called when a node is about to be processed for generation.
- This method is called only when the entire node is generated.
- """
- pass
-
- def _file_filter(self, resource, *args, **kwargs):
- """
- Returns True if the resource path matches the filter property in
- plugin settings.
- """
-
- if not self._dir_filter(resource.node, *args, **kwargs):
- return False
-
- try:
- filters = self.settings.include_file_pattern
- if not isinstance(filters, list):
- filters = [filters]
- except AttributeError:
- filters = None
- result = any(fnmatch.fnmatch(resource.path, f)
- for f in filters) if filters else True
- return result
-
- def _dir_filter(self, node, *args, **kwargs):
- """
- Returns True if the node path is a descendant of the
- include_paths property in plugin settings.
- """
- try:
- node_filters = self.settings.include_paths
- if not isinstance(node_filters, list):
- node_filters = [node_filters]
- node_filters = [self.site.content.node_from_relative_path(f)
- for f in node_filters]
- except AttributeError:
- node_filters = None
- result = any(node.source == f.source or
- node.source.is_descendant_of(f.source)
- for f in node_filters if f) \
- if node_filters else True
- return result
-
- def begin_text_resource(self, resource, text):
- """
- Called when a text resource is about to be processed for generation.
- The `text` parameter contains the resource text at this point
- in its lifecycle. It is the text that has been loaded and any
- plugins that are higher in the order may have tampered with it.
- But the text has not been processed by the template yet. Note that
- the source file associated with the text resource may not be modifed
- by any plugins.
-
- If this function returns a value, it is used as the text for further
- processing.
- """
- return text
-
- def begin_binary_resource(self, resource):
- """
- Called when a binary resource is about to be processed for generation.
-
- Plugins are free to modify the contents of the file.
- """
- pass
-
- def text_resource_complete(self, resource, text):
- """
- Called when a resource has been processed by the template.
- The `text` parameter contains the resource text at this point
- in its lifecycle. It is the text that has been processed by the
- template and any plugins that are higher in the order may have
- tampered with it. Note that the source file associated with the
- text resource may not be modifed by any plugins.
-
- If this function returns a value, it is used as the text for further
- processing.
- """
- return text
-
- def binary_resource_complete(self, resource):
- """
- Called when a binary resource has already been processed.
-
- Plugins are free to modify the contents of the file.
- """
- pass
-
- def node_complete(self, node):
- """
- Called when all the resources in the node have been processed.
- This method is called only when the entire node is generated.
- """
- pass
-
- def site_complete(self):
- """
- Called when the entire site has been processed. This method is called
- only when the entire site is generated.
- """
- pass
-
- def generation_complete(self):
- """
- Called when generation is completed.
- """
- pass
-
- @staticmethod
- def load_all(site):
- """
- Loads plugins based on the configuration. Assigns the plugins to
- 'site.plugins'
- """
- def load_plugin(name):
- plugin_name = PLUGINS_OLD_AND_NEW.get(name, name)
- return load_python_object(plugin_name)(site)
-
- site.plugins = [load_plugin(name)
- for name in site.config.plugins]
-
- @staticmethod
- def get_proxy(site):
- """
- Returns a new instance of the Plugin proxy.
- """
- return PluginProxy(site)
-
-
- class CLTransformer(Plugin):
-
- """
- Handy class for plugins that simply call a command line app to
- transform resources.
- """
- @property
- def defaults(self):
- """
- Default command line options. Can be overridden
- by specifying them in config.
- """
-
- return {}
-
- @property
- def executable_name(self):
- """
- The executable name for the plugin. This can be overridden in the
- config. If a configuration option is not provided, this is used
- to guess the complete path of the executable.
- """
- return self.plugin_name
-
- @property
- def executable_not_found_message(self):
- """
- Message to be displayed if the command line application
- is not found.
- """
-
- return ("%(name)s executable path not configured properly. "
- "This plugin expects `%(name)s.app` to point "
- "to the full path of the `%(exec)s` executable." %
- {
- "name": self.plugin_name, "exec": self.executable_name
- })
-
- @property
- def app(self):
- """
- Gets the application path from the site configuration.
-
- If the path is not configured, attempts to guess the path
- from the sytem path environment variable.
- """
-
- try:
- app_path = getattr(self.settings, 'app')
- except AttributeError:
- app_path = self.executable_name
-
- # Honour the PATH environment variable.
- if app_path is not None and not os.path.isabs(app_path):
- app_path = discover_executable(app_path, self.site.sitepath)
-
- if app_path is None:
- raise HydeException(self.executable_not_found_message)
- app = File(app_path)
-
- if not app.exists:
- raise HydeException(self.executable_not_found_message)
-
- return app
-
- def option_prefix(self, option):
- """
- Return the prefix for the given option.
-
- Defaults to --.
- """
- return "--"
-
- def process_args(self, supported):
- """
- Given a list of supported arguments, consutructs an argument
- list that could be passed on to the call_app function.
- """
- args = {}
- args.update(self.defaults)
- try:
- args.update(self.settings.args.to_dict())
- except AttributeError:
- pass
-
- params = []
- for option in supported:
- if isinstance(option, tuple):
- (descriptive, short) = option
- else:
- descriptive = short = option
-
- options = [descriptive.rstrip("="), short.rstrip("=")]
- match = first_match(lambda arg: arg in options, args)
- if match:
- val = args[match]
- param = "%s%s" % (self.option_prefix(descriptive),
- descriptive)
- if descriptive.endswith("="):
- param += val
- val = None
- params.append(param)
- if val:
- params.append(val)
- return params
-
- def call_app(self, args):
- """
- Calls the application with the given command line parameters.
- """
- try:
- self.logger.debug(
- "Calling executable [%s] with arguments %s" %
- (args[0], unicode(args[1:])))
- return subprocess.check_output(args)
- except subprocess.CalledProcessError, error:
- self.logger.error(error.output)
- raise
-
-
- class TextyPlugin(Plugin):
-
- """
- Base class for text preprocessing plugins.
-
- Plugins that desire to provide syntactic sugar for
- commonly used hyde functions for various templates
- can inherit from this class.
- """
-
- __metaclass__ = abc.ABCMeta
-
- def __init__(self, site):
- super(TextyPlugin, self).__init__(site)
- self.open_pattern = self.default_open_pattern
- self.close_pattern = self.default_close_pattern
- self.template = None
- config = getattr(site.config, self.plugin_name, None)
-
- if config and hasattr(config, 'open_pattern'):
- self.open_pattern = config.open_pattern
-
- if self.close_pattern and config and hasattr(config, 'close_pattern'):
- self.close_pattern = config.close_pattern
-
- @property
- def plugin_name(self):
- """
- The name of the plugin. Makes an intelligent guess.
- """
- return self.__class__.__name__.replace('Plugin', '').lower()
-
- @abc.abstractproperty
- def tag_name(self):
- """
- The tag that this plugin tries add syntactic sugar for.
- """
- return self.plugin_name
-
- @abc.abstractproperty
- def default_open_pattern(self):
- """
- The default pattern for opening the tag.
- """
- return None
-
- @abc.abstractproperty
- def default_close_pattern(self):
- """
- The default pattern for closing the tag.
- """
- return None
-
- def get_params(self, match, start=True):
- """
- Default implementation for getting template args.
- """
- return match.groups(1)[0] if match.lastindex else ''
-
- @abc.abstractmethod
- def text_to_tag(self, match, start=True):
- """
- Replaces the matched text with tag statement
- given by the template.
- """
- params = self.get_params(match, start)
- return (self.template.get_open_tag(self.tag_name, params)
- if start
- else self.template.get_close_tag(self.tag_name, params))
-
- def begin_text_resource(self, resource, text):
- """
- Replace a text base pattern with a template statement.
- """
- text_open = re.compile(self.open_pattern, re.UNICODE | re.MULTILINE)
- text = text_open.sub(self.text_to_tag, text)
- if self.close_pattern:
- text_close = re.compile(
- self.close_pattern, re.UNICODE | re.MULTILINE)
- text = text_close.sub(
- partial(self.text_to_tag, start=False), text)
- return text
|