Browse Source

Added uglify js plugin

main
Lakshmi Vyasarajan 13 years ago
parent
commit
28a7fd124d
15 changed files with 8724 additions and 124 deletions
  1. +1
    -1
      hyde/ext/plugins/blockdown.py
  2. +1
    -0
      hyde/ext/plugins/depends.py
  3. +17
    -25
      hyde/ext/plugins/less.py
  4. +1
    -1
      hyde/ext/plugins/markings.py
  5. +1
    -1
      hyde/ext/plugins/syntext.py
  6. +0
    -91
      hyde/ext/plugins/texty.py
  7. +75
    -0
      hyde/ext/plugins/uglify.py
  8. +5
    -2
      hyde/ext/templates/jinja.py
  9. +1
    -0
      hyde/model.py
  10. +190
    -3
      hyde/plugin.py
  11. +1
    -0
      hyde/tests/ext/test_depends.py
  12. +95
    -0
      hyde/tests/ext/test_uglify.py
  13. +3
    -0
      hyde/tests/ext/uglify/expected-jquery-nc.js
  14. +17
    -0
      hyde/tests/ext/uglify/expected-jquery.js
  15. +8316
    -0
      hyde/tests/ext/uglify/jquery.js

+ 1
- 1
hyde/ext/plugins/blockdown.py View File

@@ -3,7 +3,7 @@
Blockdown plugin
"""

from hyde.ext.plugins.texty import TextyPlugin
from hyde.plugin import TextyPlugin

class BlockdownPlugin(TextyPlugin):
"""


+ 1
- 0
hyde/ext/plugins/depends.py View File

@@ -46,4 +46,5 @@ class DependsPlugin(Plugin):
resource=resource,
site=self.site,
context=self.site.context))
resource.depends = list(set(resource.depends))
return text

+ 17
- 25
hyde/ext/plugins/less.py View File

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


+ 1
- 1
hyde/ext/plugins/markings.py View File

@@ -3,7 +3,7 @@
Markings plugin
"""

from hyde.ext.plugins.texty import TextyPlugin
from hyde.plugin import TextyPlugin

class MarkingsPlugin(TextyPlugin):
"""


+ 1
- 1
hyde/ext/plugins/syntext.py View File

@@ -3,7 +3,7 @@
Syntext plugin
"""

from hyde.ext.plugins.texty import TextyPlugin
from hyde.plugin import TextyPlugin

class SyntextPlugin(TextyPlugin):
"""


+ 0
- 91
hyde/ext/plugins/texty.py View File

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

+ 75
- 0
hyde/ext/plugins/uglify.py View File

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

+ 5
- 2
hyde/ext/templates/jinja.py View File

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


+ 1
- 0
hyde/model.py View File

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


+ 190
- 3
hyde/plugin.py View File

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

+ 1
- 0
hyde/tests/ext/test_depends.py View File

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


+ 95
- 0
hyde/tests/ext/test_uglify.py View File

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



+ 3
- 0
hyde/tests/ext/uglify/expected-jquery-nc.js
File diff suppressed because it is too large
View File


+ 17
- 0
hyde/tests/ext/uglify/expected-jquery.js
File diff suppressed because it is too large
View File


+ 8316
- 0
hyde/tests/ext/uglify/jquery.js
File diff suppressed because it is too large
View File


Loading…
Cancel
Save