| @@ -3,7 +3,7 @@ | |||
| Blockdown plugin | |||
| """ | |||
| from hyde.ext.plugins.texty import TextyPlugin | |||
| from hyde.plugin import TextyPlugin | |||
| class BlockdownPlugin(TextyPlugin): | |||
| """ | |||
| @@ -46,4 +46,5 @@ class DependsPlugin(Plugin): | |||
| resource=resource, | |||
| site=self.site, | |||
| context=self.site.context)) | |||
| resource.depends = list(set(resource.depends)) | |||
| return text | |||
| @@ -3,15 +3,14 @@ | |||
| Less css plugin | |||
| """ | |||
| from hyde.plugin import Plugin | |||
| from hyde.fs import File, Folder | |||
| from hyde.plugin import CLTransformer | |||
| from hyde.fs import File | |||
| import re | |||
| import subprocess | |||
| import traceback | |||
| class LessCSSPlugin(Plugin): | |||
| class LessCSSPlugin(CLTransformer): | |||
| """ | |||
| The plugin class for less css | |||
| """ | |||
| @@ -46,6 +45,14 @@ class LessCSSPlugin(Plugin): | |||
| text = import_finder.sub(import_to_include, text) | |||
| return text | |||
| @property | |||
| def plugin_name(self): | |||
| """ | |||
| The name of the plugin. | |||
| """ | |||
| return "less" | |||
| def text_resource_complete(self, resource, text): | |||
| """ | |||
| Save the file to a temporary place and run less compiler. | |||
| @@ -54,30 +61,15 @@ class LessCSSPlugin(Plugin): | |||
| """ | |||
| if not resource.source_file.kind == 'less': | |||
| return | |||
| if not (hasattr(self.site.config, 'less') and | |||
| hasattr(self.site.config.less, 'app')): | |||
| raise self.template.exception_class( | |||
| "Less css path not configured. " | |||
| "This plugin expects `less.app` to point " | |||
| "to the `lessc` executable.") | |||
| less = File(self.site.config.less.app) | |||
| if not File(less).exists: | |||
| raise self.template.exception_class( | |||
| "Cannot find the less executable. The given path [%s] " | |||
| "is incorrect" % less) | |||
| less = self.app | |||
| source = File.make_temp(text) | |||
| target = File.make_temp('') | |||
| try: | |||
| subprocess.check_call([str(less), str(source), str(target)]) | |||
| except subprocess.CalledProcessError, error: | |||
| self.logger.error(traceback.format_exc()) | |||
| self.logger.error(error.output) | |||
| raise self.template.exception_class( | |||
| "Cannot process less css. Error occurred when " | |||
| "processing [%s]" % resource.source_file) | |||
| self.call_app([str(less), str(source), str(target)]) | |||
| except subprocess.CalledProcessError: | |||
| raise self.template.exception_class( | |||
| "Cannot process %s. Error occurred when " | |||
| "processing [%s]" % (self.app.name, resource.source_file)) | |||
| out = target.read_all() | |||
| new_name = resource.source_file.name_without_extension + ".css" | |||
| target_folder = File(resource.relative_path).parent | |||
| @@ -3,7 +3,7 @@ | |||
| Markings plugin | |||
| """ | |||
| from hyde.ext.plugins.texty import TextyPlugin | |||
| from hyde.plugin import TextyPlugin | |||
| class MarkingsPlugin(TextyPlugin): | |||
| """ | |||
| @@ -3,7 +3,7 @@ | |||
| Syntext plugin | |||
| """ | |||
| from hyde.ext.plugins.texty import TextyPlugin | |||
| from hyde.plugin import TextyPlugin | |||
| class SyntextPlugin(TextyPlugin): | |||
| """ | |||
| @@ -1,91 +0,0 @@ | |||
| # -*- coding: utf-8 -*- | |||
| """ | |||
| Provides classes and utilities that allow text | |||
| to be replaced before the templates are | |||
| rendered. | |||
| """ | |||
| from hyde.plugin import Plugin | |||
| import abc | |||
| import re | |||
| from functools import partial | |||
| 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): | |||
| 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 | |||
| @@ -0,0 +1,75 @@ | |||
| # -*- coding: utf-8 -*- | |||
| """ | |||
| Uglify plugin | |||
| """ | |||
| from hyde.plugin import CLTransformer | |||
| from hyde.fs import File, Folder | |||
| import subprocess | |||
| import traceback | |||
| class UglifyPlugin(CLTransformer): | |||
| """ | |||
| The plugin class for Uglify JS | |||
| """ | |||
| def __init__(self, site): | |||
| super(UglifyPlugin, self).__init__(site) | |||
| @property | |||
| def plugin_name(self): | |||
| """ | |||
| The name of the plugin. | |||
| """ | |||
| return "uglify" | |||
| def text_resource_complete(self, resource, text): | |||
| """ | |||
| If the site is in development mode, just return. | |||
| Otherwise, save the file to a temporary place | |||
| and run the uglify app. Read the generated file | |||
| and return the text as output. | |||
| """ | |||
| try: | |||
| mode = self.site.config.mode | |||
| except AttributeError: | |||
| mode = "production" | |||
| if not resource.source_file.kind == 'js': | |||
| return | |||
| if self.site.config.mode.startswith('dev'): | |||
| self.logger.debug("Skipping uglify in development mode.") | |||
| return | |||
| supported = [ | |||
| ("beautify", "b"), | |||
| ("indent", "i"), | |||
| ("quote-keys", "q"), | |||
| ("mangle-toplevel", "mt"), | |||
| ("no-mangle", "nm"), | |||
| ("no-squeeze", "ns"), | |||
| "no-seqs", | |||
| "no-dead-code", | |||
| ("no-copyright", "nc"), | |||
| "overwrite", | |||
| "verbose", | |||
| "unsafe", | |||
| "max-line-len", | |||
| "reserved-names", | |||
| "ascii" | |||
| ] | |||
| uglify = self.app | |||
| source = File.make_temp(text) | |||
| target = File.make_temp('') | |||
| args = [str(uglify)] | |||
| args.extend(self.process_args(supported)) | |||
| args.extend(["-o", str(target), str(source)]) | |||
| self.call_app(args) | |||
| out = target.read_all() | |||
| return out | |||
| @@ -9,7 +9,8 @@ from hyde.template import HtmlWrap, Template | |||
| from hyde.site import Resource | |||
| from hyde.util import getLoggerWithNullHandler, getLoggerWithConsoleHandler | |||
| from jinja2 import contextfunction, Environment, FileSystemLoader | |||
| from jinja2 import contextfunction, Environment | |||
| from jinja2 import FileSystemLoader, FileSystemBytecodeCache | |||
| from jinja2 import environmentfilter, Markup, Undefined, nodes | |||
| from jinja2.ext import Extension | |||
| from jinja2.exceptions import TemplateError | |||
| @@ -358,7 +359,8 @@ class Refer(Extension): | |||
| namespace['parent_resource'] = resource | |||
| if not hasattr(resource, 'depends'): | |||
| resource.depends = [] | |||
| resource.depends.append(template) | |||
| if not template in resource.depends: | |||
| resource.depends.append(template) | |||
| namespace['resource'] = site.content.resource_from_relative_path(template) | |||
| return '' | |||
| @@ -433,6 +435,7 @@ class Jinja2Template(Template): | |||
| self.env = Environment(loader=self.loader, | |||
| undefined=SilentUndefined, | |||
| trim_blocks=True, | |||
| bytecode_cache=FileSystemBytecodeCache(), | |||
| extensions=[IncludeText, | |||
| Markdown, | |||
| Syntax, | |||
| @@ -98,6 +98,7 @@ class Config(Expando): | |||
| def __init__(self, sitepath, config_file=None, config_dict=None): | |||
| default_config = dict( | |||
| mode='production', | |||
| content_root='content', | |||
| deploy_root='deploy', | |||
| media_root='media', | |||
| @@ -3,10 +3,18 @@ | |||
| Contains definition for a plugin protocol and other utiltities. | |||
| """ | |||
| import abc | |||
| from hyde import loader | |||
| from hyde.exceptions import HydeException | |||
| from hyde.fs import File | |||
| from hyde.util import getLoggerWithNullHandler | |||
| from hyde.model import Expando | |||
| from functools import partial | |||
| import re | |||
| import subprocess | |||
| import traceback | |||
| logger = getLoggerWithNullHandler('hyde.engine') | |||
| @@ -56,6 +64,7 @@ class Plugin(object): | |||
| super(Plugin, self).__init__() | |||
| self.site = site | |||
| self.logger = getLoggerWithNullHandler(self.__class__.__name__) | |||
| self.template = None | |||
| def template_loaded(self, template): | |||
| @@ -170,9 +179,6 @@ class Plugin(object): | |||
| """ | |||
| pass | |||
| def raise_event(self, event_name): | |||
| return getattr(Plugin.proxy, event_name)() | |||
| @staticmethod | |||
| def load_all(site): | |||
| """ | |||
| @@ -184,4 +190,185 @@ class Plugin(object): | |||
| @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 plugin_name(self): | |||
| """ | |||
| The name of the plugin. Makes an intelligent guess. | |||
| """ | |||
| return self.__class__.__name__.replace('Plugin', '').lower() | |||
| def defaults(self): | |||
| """ | |||
| Default command line options. Can be overridden | |||
| by specifying them in config. | |||
| """ | |||
| return {} | |||
| @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 `%(name)s` executable." % {"name": self.plugin_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 app(self): | |||
| """ | |||
| Gets the application path from the site configuration. | |||
| """ | |||
| try: | |||
| app_path = getattr(self.settings, 'app') | |||
| except AttributeError: | |||
| raise self.template.exception_class( | |||
| self.executable_not_found_message) | |||
| app = File(app_path) | |||
| if not app.exists: | |||
| raise self.template.exception_class( | |||
| self.executable_not_found_message) | |||
| return app | |||
| def process_args(self, supported): | |||
| try: | |||
| args = getattr(self.settings, 'args').to_dict() | |||
| except AttributeError: | |||
| args = {} | |||
| result = [] | |||
| for arg in supported: | |||
| if isinstance(arg, tuple): | |||
| (descriptive, short) = arg | |||
| else: | |||
| descriptive = short = arg | |||
| if descriptive in args or short in args: | |||
| result.append("--%s" % descriptive) | |||
| val = args[descriptive if descriptive in args else short] | |||
| if val: | |||
| result.append(val) | |||
| return result | |||
| 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], str(args[1:]))) | |||
| subprocess.check_call(args) | |||
| except subprocess.CalledProcessError, error: | |||
| self.logger.error(traceback.format_exc()) | |||
| 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): | |||
| 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 | |||
| @@ -49,6 +49,7 @@ depends: index.html | |||
| gen.template.env.filters['dateformat'] = dateformat | |||
| gen.generate_resource_at_path(inc.name) | |||
| res = s.content.resource_from_relative_path(inc.name) | |||
| print res.__dict__ | |||
| assert len(res.depends) == 1 | |||
| assert 'index.html' in res.depends | |||
| deps = list(gen.get_dependencies(res)) | |||
| @@ -0,0 +1,95 @@ | |||
| # -*- coding: utf-8 -*- | |||
| """ | |||
| Use nose | |||
| `$ pip install nose` | |||
| `$ nosetests` | |||
| """ | |||
| from hyde.fs import File, Folder | |||
| from hyde.model import Expando | |||
| from hyde.generator import Generator | |||
| from hyde.site import Site | |||
| UGLIFY_SOURCE = File(__file__).parent.child_folder('uglify') | |||
| TEST_SITE = File(__file__).parent.parent.child_folder('_test') | |||
| class TestLess(object): | |||
| def setUp(self): | |||
| TEST_SITE.make() | |||
| TEST_SITE.parent.child_folder( | |||
| 'sites/test_jinja').copy_contents_to(TEST_SITE) | |||
| JS = TEST_SITE.child_folder('content/media/js') | |||
| JS.make() | |||
| UGLIFY_SOURCE.copy_contents_to(JS) | |||
| def tearDown(self): | |||
| TEST_SITE.delete() | |||
| def test_can_uglify(self): | |||
| s = Site(TEST_SITE) | |||
| s.config.plugins = ['hyde.ext.plugins.uglify.UglifyPlugin'] | |||
| s.config.mode = "production" | |||
| paths = ['/usr/local/share/npm/bin/uglifyjs', '~/local/bin/uglifyjs'] | |||
| uglify = [path for path in paths if File(path).exists] | |||
| if not uglify: | |||
| assert False, "Cannot find the uglify executable" | |||
| uglify = uglify[0] | |||
| s.config.uglify = Expando(dict(app=path)) | |||
| source = TEST_SITE.child('content/media/js/jquery.js') | |||
| target = File(Folder(s.config.deploy_root_path).child('media/js/jquery.js')) | |||
| gen = Generator(s) | |||
| gen.generate_resource_at_path(source) | |||
| assert target.exists | |||
| expected = File(UGLIFY_SOURCE.child('expected-jquery.js')) | |||
| # TODO: Very fragile. Better comparison needed. | |||
| assert target.read_all() == expected.read_all() | |||
| def test_uglify_with_extra_options(self): | |||
| s = Site(TEST_SITE) | |||
| s.config.plugins = ['hyde.ext.plugins.uglify.UglifyPlugin'] | |||
| s.config.mode = "production" | |||
| paths = ['/usr/local/share/npm/bin/uglifyjs', '~/local/bin/uglifyjs'] | |||
| uglify = [path for path in paths if File(path).exists] | |||
| if not uglify: | |||
| assert False, "Cannot find the uglify executable" | |||
| uglify = uglify[0] | |||
| s.config.uglify = Expando(dict(app=path, args={"nc":""})) | |||
| source = TEST_SITE.child('content/media/js/jquery.js') | |||
| target = File(Folder(s.config.deploy_root_path).child('media/js/jquery.js')) | |||
| gen = Generator(s) | |||
| gen.generate_resource_at_path(source) | |||
| assert target.exists | |||
| expected = File(UGLIFY_SOURCE.child('expected-jquery-nc.js')) | |||
| # TODO: Very fragile. Better comparison needed. | |||
| text = target.read_all() | |||
| assert text.startswith("(function(") | |||
| def test_no_uglify_in_dev_mode(self): | |||
| s = Site(TEST_SITE) | |||
| s.config.plugins = ['hyde.ext.plugins.uglify.UglifyPlugin'] | |||
| s.config.mode = "dev" | |||
| paths = ['/usr/local/share/npm/bin/uglifyjs', '~/local/bin/uglifyjs'] | |||
| uglify = [path for path in paths if File(path).exists] | |||
| if not uglify: | |||
| assert False, "Cannot find the uglify executable" | |||
| uglify = uglify[0] | |||
| s.config.uglify = Expando(dict(app=path)) | |||
| source = TEST_SITE.child('content/media/js/jquery.js') | |||
| target = File(Folder(s.config.deploy_root_path).child('media/js/jquery.js')) | |||
| gen = Generator(s) | |||
| gen.generate_resource_at_path(source) | |||
| assert target.exists | |||
| expected = File(UGLIFY_SOURCE.child('jquery.js')) | |||
| # TODO: Very fragile. Better comparison needed. | |||
| text = target.read_all() | |||
| expected = expected.read_all() | |||
| assert text == expected | |||