Browse Source

Added uglify js plugin

main
Lakshmi Vyasarajan 14 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 Blockdown plugin
""" """


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


class BlockdownPlugin(TextyPlugin): class BlockdownPlugin(TextyPlugin):
""" """


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

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

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

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


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

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


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

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


+ 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.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,


+ 1
- 0
hyde/model.py View File

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


+ 190
- 3
hyde/plugin.py View File

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

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

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


+ 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


||||||
x
 
000:0
Loading…
Cancel
Save