@@ -0,0 +1,70 @@ | |||||
# -*- coding: utf-8 -*- | |||||
""" | |||||
OPTIPNG plugin | |||||
""" | |||||
from hyde.plugin import CLTransformer | |||||
from hyde.fs import File | |||||
class OptiPNGPlugin(CLTransformer): | |||||
""" | |||||
The plugin class for OptiPNG | |||||
""" | |||||
def __init__(self, site): | |||||
super(OptiPNGPlugin, self).__init__(site) | |||||
@property | |||||
def plugin_name(self): | |||||
""" | |||||
The name of the plugin. | |||||
""" | |||||
return "optipng" | |||||
def option_prefix(self, option): | |||||
return "-" | |||||
def binary_resource_complete(self, resource): | |||||
""" | |||||
If the site is in development mode, just return. | |||||
Otherwise, run optipng to compress the png file. | |||||
""" | |||||
try: | |||||
mode = self.site.config.mode | |||||
except AttributeError: | |||||
mode = "production" | |||||
if not resource.source_file.kind == 'png': | |||||
return | |||||
if mode.startswith('dev'): | |||||
self.logger.debug("Skipping optipng in development mode.") | |||||
return | |||||
supported = [ | |||||
"o", | |||||
"fix", | |||||
"force", | |||||
"preserve", | |||||
"quiet", | |||||
"log", | |||||
"f", | |||||
"i", | |||||
"zc", | |||||
"zm", | |||||
"zs", | |||||
"zw", | |||||
"full", | |||||
"nb", | |||||
"nc", | |||||
"np", | |||||
"nz" | |||||
] | |||||
target = File(self.site.config.deploy_root_path.child( | |||||
resource.relative_deploy_path)) | |||||
optipng = self.app | |||||
args = [str(optipng)] | |||||
args.extend(self.process_args(supported)) | |||||
args.extend([str(target)]) | |||||
self.call_app(args) |
@@ -13,6 +13,7 @@ import os | |||||
import shutil | import shutil | ||||
from distutils import dir_util | from distutils import dir_util | ||||
import functools | import functools | ||||
import fnmatch | |||||
from hyde.util import getLoggerWithNullHandler | from hyde.util import getLoggerWithNullHandler | ||||
@@ -94,12 +95,12 @@ class FS(object): | |||||
Generates the parents until stop or the absolute | Generates the parents until stop or the absolute | ||||
root directory is reached. | root directory is reached. | ||||
""" | """ | ||||
f = self | |||||
while f.parent != stop: | |||||
if f.parent == f: | |||||
folder = self | |||||
while folder.parent != stop: | |||||
if folder.parent == folder: | |||||
return | return | ||||
yield f.parent | |||||
f = f.parent | |||||
yield folder.parent | |||||
folder = folder.parent | |||||
def is_descendant_of(self, ancestor): | def is_descendant_of(self, ancestor): | ||||
""" | """ | ||||
@@ -185,9 +186,22 @@ class File(FS): | |||||
""" | """ | ||||
return self.extension.lstrip(".") | return self.extension.lstrip(".") | ||||
@property | |||||
def size(self): | |||||
""" | |||||
Size of this file. | |||||
""" | |||||
if not self.exists: | |||||
return -1 | |||||
return os.path.getsize(self.path) | |||||
@property | @property | ||||
def mimetype(self): | def mimetype(self): | ||||
(mime, encoding) = mimetypes.guess_type(self.path) | |||||
""" | |||||
Gets the mimetype of this file. | |||||
""" | |||||
(mime, _) = mimetypes.guess_type(self.path) | |||||
return mime | return mime | ||||
@property | @property | ||||
@@ -246,9 +260,9 @@ class File(FS): | |||||
import tempfile | import tempfile | ||||
(handle, path) = tempfile.mkstemp(text=True) | (handle, path) = tempfile.mkstemp(text=True) | ||||
os.close(handle) | os.close(handle) | ||||
f = File(path) | |||||
f.write(text) | |||||
return f | |||||
afile = File(path) | |||||
afile.write(text) | |||||
return afile | |||||
def read_all(self, encoding='utf-8'): | def read_all(self, encoding='utf-8'): | ||||
""" | """ | ||||
@@ -296,26 +310,26 @@ class FSVisitor(object): | |||||
self.folder = folder | self.folder = folder | ||||
self.pattern = pattern | self.pattern = pattern | ||||
def folder_visitor(self, f): | |||||
def folder_visitor(self, function): | |||||
""" | """ | ||||
Decorator for `visit_folder` protocol | Decorator for `visit_folder` protocol | ||||
""" | """ | ||||
self.visit_folder = f | |||||
return f | |||||
self.visit_folder = function | |||||
return function | |||||
def file_visitor(self, f): | |||||
def file_visitor(self, function): | |||||
""" | """ | ||||
Decorator for `visit_file` protocol | Decorator for `visit_file` protocol | ||||
""" | """ | ||||
self.visit_file = f | |||||
return f | |||||
self.visit_file = function | |||||
return function | |||||
def finalizer(self, f): | |||||
def finalizer(self, function): | |||||
""" | """ | ||||
Decorator for `visit_complete` protocol | Decorator for `visit_complete` protocol | ||||
""" | """ | ||||
self.visit_complete = f | |||||
return f | |||||
self.visit_complete = function | |||||
return function | |||||
def __enter__(self): | def __enter__(self): | ||||
return self | return self | ||||
@@ -341,7 +355,7 @@ class FolderWalker(FSVisitor): | |||||
if not walk_files and not walk_folders: | if not walk_files and not walk_folders: | ||||
return | return | ||||
for root, dirs, a_files in os.walk(self.folder.path, followlinks=True): | |||||
for root, _, a_files in os.walk(self.folder.path, followlinks=True): | |||||
folder = Folder(root) | folder = Folder(root) | ||||
if walk_folders: | if walk_folders: | ||||
yield folder | yield folder | ||||
@@ -262,6 +262,9 @@ class CLTransformer(Plugin): | |||||
return app | return app | ||||
def option_prefix(self, option): | |||||
return "--" | |||||
def process_args(self, supported): | def process_args(self, supported): | ||||
""" | """ | ||||
Given a list of supported arguments, consutructs an argument | Given a list of supported arguments, consutructs an argument | ||||
@@ -282,7 +285,8 @@ class CLTransformer(Plugin): | |||||
descriptive = short = arg | descriptive = short = arg | ||||
if descriptive in args or short in args: | if descriptive in args or short in args: | ||||
result.append("--%s" % descriptive) | |||||
result.append("%s%s" % (self.option_prefix(descriptive), | |||||
descriptive)) | |||||
val = args[descriptive if descriptive in args else short] | val = args[descriptive if descriptive in args else short] | ||||
if val: | if val: | ||||
result.append(val) | result.append(val) | ||||
@@ -0,0 +1 @@ | |||||
../../../../resources/hyde-lt-b.png |
@@ -32,7 +32,7 @@ class TestLess(object): | |||||
paths = ['/usr/local/share/npm/bin/lessc', '~/local/bin/lessc'] | paths = ['/usr/local/share/npm/bin/lessc', '~/local/bin/lessc'] | ||||
less = [path for path in paths if File(path).exists] | less = [path for path in paths if File(path).exists] | ||||
if not less: | if not less: | ||||
assert False, "Cannot find the uglify executable" | |||||
assert False, "Cannot find the lessc executable" | |||||
less = less[0] | less = less[0] | ||||
s.config.less = Expando(dict(app=less)) | s.config.less = Expando(dict(app=less)) | ||||
source = TEST_SITE.child('content/media/css/site.less') | source = TEST_SITE.child('content/media/css/site.less') | ||||
@@ -0,0 +1,45 @@ | |||||
# -*- 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 | |||||
OPTIPNG_SOURCE = File(__file__).parent.child_folder('optipng') | |||||
TEST_SITE = File(__file__).parent.parent.child_folder('_test') | |||||
class TestOptipng(object): | |||||
def setUp(self): | |||||
TEST_SITE.make() | |||||
TEST_SITE.parent.child_folder( | |||||
'sites/test_jinja').copy_contents_to(TEST_SITE) | |||||
IMAGES = TEST_SITE.child_folder('content/media/images') | |||||
IMAGES.make() | |||||
OPTIPNG_SOURCE.copy_contents_to(IMAGES) | |||||
def tearDown(self): | |||||
TEST_SITE.delete() | |||||
def test_can_execute_optipng(self): | |||||
s = Site(TEST_SITE) | |||||
s.config.mode = "production" | |||||
s.config.plugins = ['hyde.ext.plugins.optipng.OptiPNGPlugin'] | |||||
paths = ['/usr/local/bin/optipng'] | |||||
optipng = [path for path in paths if File(path).exists] | |||||
if not optipng: | |||||
assert False, "Cannot find the optipng executable" | |||||
optipng = optipng[0] | |||||
s.config.optipng = Expando(dict(app=optipng, args=dict(quiet=""))) | |||||
source =File(TEST_SITE.child('content/media/images/hyde-lt-b.png')) | |||||
target = File(Folder(s.config.deploy_root_path).child('media/images/hyde-lt-b.png')) | |||||
gen = Generator(s) | |||||
gen.generate_resource_at_path(source) | |||||
assert target.exists | |||||
assert target.size < source.size |
@@ -204,6 +204,9 @@ def test_is_image(): | |||||
assert not HELPERS.is_image | assert not HELPERS.is_image | ||||
assert LOGO.is_image | assert LOGO.is_image | ||||
def test_file_size(): | |||||
assert LOGO.size == 1942 | |||||
@nottest | @nottest | ||||
def setup_data(): | def setup_data(): | ||||
DATA_ROOT.make() | DATA_ROOT.make() | ||||