@@ -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 | |||