@@ -3,7 +3,7 @@ | |||||
Blockdown plugin | Blockdown plugin | ||||
""" | """ | ||||
from hyde.ext.plugins.texty import TextyPlugin | from hyde.plugin import TextyPlugin | ||||
class BlockdownPlugin(TextyPlugin): | class BlockdownPlugin(TextyPlugin): | ||||
""" | """ | ||||
@@ -46,4 +46,5 @@ class DependsPlugin(Plugin): | |||||
resource=resource, | resource=resource, | ||||
site=self.site, | site=self.site, | ||||
context=self.site.context)) | context=self.site.context)) | ||||
resource.depends = list(set(resource.depends)) | |||||
return text | return text |
@@ -3,15 +3,14 @@ | |||||
Less css plugin | Less css plugin | ||||
""" | """ | ||||
from hyde.plugin import Plugin | from hyde.plugin import CLTransformer | ||||
from hyde.fs import File, Folder | from hyde.fs import File | ||||
import re | import re | ||||
import subprocess | import subprocess | ||||
import traceback | |||||
class LessCSSPlugin(Plugin): | class LessCSSPlugin(CLTransformer): | ||||
""" | """ | ||||
The plugin class for less css | The plugin class for less css | ||||
""" | """ | ||||
@@ -46,6 +45,14 @@ class LessCSSPlugin(Plugin): | |||||
text = import_finder.sub(import_to_include, text) | text = import_finder.sub(import_to_include, text) | ||||
return text | return text | ||||
@property | |||||
def plugin_name(self): | |||||
""" | |||||
The name of the plugin. | |||||
""" | |||||
return "less" | |||||
def text_resource_complete(self, resource, text): | def text_resource_complete(self, resource, text): | ||||
""" | """ | ||||
Save the file to a temporary place and run less compiler. | 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': | if not resource.source_file.kind == 'less': | ||||
return | return | ||||
if not (hasattr(self.site.config, 'less') and | less = self.app | ||||
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) | |||||
source = File.make_temp(text) | source = File.make_temp(text) | ||||
target = File.make_temp('') | target = File.make_temp('') | ||||
try: | try: | ||||
subprocess.check_call([str(less), str(source), str(target)]) | self.call_app([str(less), str(source), str(target)]) | ||||
except subprocess.CalledProcessError, error: | except subprocess.CalledProcessError: | ||||
self.logger.error(traceback.format_exc()) | raise self.template.exception_class( | ||||
self.logger.error(error.output) | "Cannot process %s. Error occurred when " | ||||
raise self.template.exception_class( | "processing [%s]" % (self.app.name, resource.source_file)) | ||||
"Cannot process less css. Error occurred when " | |||||
"processing [%s]" % resource.source_file) | |||||
out = target.read_all() | out = target.read_all() | ||||
new_name = resource.source_file.name_without_extension + ".css" | new_name = resource.source_file.name_without_extension + ".css" | ||||
target_folder = File(resource.relative_path).parent | target_folder = File(resource.relative_path).parent | ||||
@@ -3,7 +3,7 @@ | |||||
Markings plugin | Markings plugin | ||||
""" | """ | ||||
from hyde.ext.plugins.texty import TextyPlugin | from hyde.plugin import TextyPlugin | ||||
class MarkingsPlugin(TextyPlugin): | class MarkingsPlugin(TextyPlugin): | ||||
""" | """ | ||||
@@ -3,7 +3,7 @@ | |||||
Syntext plugin | Syntext plugin | ||||
""" | """ | ||||
from hyde.ext.plugins.texty import TextyPlugin | from hyde.plugin import TextyPlugin | ||||
class SyntextPlugin(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.site import Resource | ||||
from hyde.util import getLoggerWithNullHandler, getLoggerWithConsoleHandler | 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 import environmentfilter, Markup, Undefined, nodes | ||||
from jinja2.ext import Extension | from jinja2.ext import Extension | ||||
from jinja2.exceptions import TemplateError | from jinja2.exceptions import TemplateError | ||||
@@ -358,7 +359,8 @@ class Refer(Extension): | |||||
namespace['parent_resource'] = resource | namespace['parent_resource'] = resource | ||||
if not hasattr(resource, 'depends'): | if not hasattr(resource, 'depends'): | ||||
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) | namespace['resource'] = site.content.resource_from_relative_path(template) | ||||
return '' | return '' | ||||
@@ -433,6 +435,7 @@ class Jinja2Template(Template): | |||||
self.env = Environment(loader=self.loader, | self.env = Environment(loader=self.loader, | ||||
undefined=SilentUndefined, | undefined=SilentUndefined, | ||||
trim_blocks=True, | trim_blocks=True, | ||||
bytecode_cache=FileSystemBytecodeCache(), | |||||
extensions=[IncludeText, | extensions=[IncludeText, | ||||
Markdown, | Markdown, | ||||
Syntax, | Syntax, | ||||
@@ -98,6 +98,7 @@ class Config(Expando): | |||||
def __init__(self, sitepath, config_file=None, config_dict=None): | def __init__(self, sitepath, config_file=None, config_dict=None): | ||||
default_config = dict( | default_config = dict( | ||||
mode='production', | |||||
content_root='content', | content_root='content', | ||||
deploy_root='deploy', | deploy_root='deploy', | ||||
media_root='media', | media_root='media', | ||||
@@ -3,10 +3,18 @@ | |||||
Contains definition for a plugin protocol and other utiltities. | Contains definition for a plugin protocol and other utiltities. | ||||
""" | """ | ||||
import abc | import abc | ||||
from hyde import loader | from hyde import loader | ||||
from hyde.exceptions import HydeException | |||||
from hyde.fs import File | |||||
from hyde.util import getLoggerWithNullHandler | from hyde.util import getLoggerWithNullHandler | ||||
from hyde.model import Expando | |||||
from functools import partial | from functools import partial | ||||
import re | |||||
import subprocess | |||||
import traceback | |||||
logger = getLoggerWithNullHandler('hyde.engine') | logger = getLoggerWithNullHandler('hyde.engine') | ||||
@@ -56,6 +64,7 @@ class Plugin(object): | |||||
super(Plugin, self).__init__() | super(Plugin, self).__init__() | ||||
self.site = site | self.site = site | ||||
self.logger = getLoggerWithNullHandler(self.__class__.__name__) | self.logger = getLoggerWithNullHandler(self.__class__.__name__) | ||||
self.template = None | |||||
def template_loaded(self, template): | def template_loaded(self, template): | ||||
@@ -170,9 +179,6 @@ class Plugin(object): | |||||
""" | """ | ||||
pass | pass | ||||
def raise_event(self, event_name): | |||||
return getattr(Plugin.proxy, event_name)() | |||||
@staticmethod | @staticmethod | ||||
def load_all(site): | def load_all(site): | ||||
""" | """ | ||||
@@ -184,4 +190,185 @@ class Plugin(object): | |||||
@staticmethod | @staticmethod | ||||
def get_proxy(site): | def get_proxy(site): | ||||
""" | |||||
Returns a new instance of the Plugin proxy. | |||||
""" | |||||
return PluginProxy(site) | 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.template.env.filters['dateformat'] = dateformat | ||||
gen.generate_resource_at_path(inc.name) | gen.generate_resource_at_path(inc.name) | ||||
res = s.content.resource_from_relative_path(inc.name) | res = s.content.resource_from_relative_path(inc.name) | ||||
print res.__dict__ | |||||
assert len(res.depends) == 1 | assert len(res.depends) == 1 | ||||
assert 'index.html' in res.depends | assert 'index.html' in res.depends | ||||
deps = list(gen.get_dependencies(res)) | 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 | |||||