Merge the huge plugin structural refactoring and retrofit changes from the past.main
@@ -1,55 +0,0 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
Coffee plugin | |||
""" | |||
import traceback | |||
from hyde.plugin import CLTransformer | |||
from hyde.fs import File | |||
class CoffeePlugin(CLTransformer): | |||
""" | |||
The plugin class for Coffeescript | |||
""" | |||
def __init__(self, site): | |||
super(CoffeePlugin, self).__init__(site) | |||
@property | |||
def executable_name(self): | |||
return "coffee" | |||
@property | |||
def plugin_name(self): | |||
""" | |||
The name of the plugin. | |||
""" | |||
return "Coffee" | |||
def begin_site(self): | |||
""" | |||
Find all the coffee files and set their relative deploy path. | |||
""" | |||
for resource in self.site.content.walk_resources(): | |||
if resource.source_file.kind == 'coffee': | |||
new_name = resource.source_file.name_without_extension + ".js" | |||
target_folder = File(resource.relative_deploy_path).parent | |||
resource.relative_deploy_path = target_folder.child(new_name) | |||
def text_resource_complete(self, resource, text): | |||
""" | |||
Save the file to a temporary place and run the Coffee | |||
compiler. Read the generated file and return the text as | |||
output. | |||
""" | |||
if not resource.source_file.kind == 'coffee': | |||
return | |||
coffee = self.app | |||
source = File.make_temp(text) | |||
target = File.make_temp('') | |||
args = [unicode(coffee)] | |||
args.extend(["-c", "-p", unicode(source)]) | |||
return self.call_app(args) |
@@ -221,7 +221,7 @@ class StylusPlugin(CLTransformer): | |||
args.append(unicode(source)) | |||
try: | |||
self.call_app(args) | |||
except subprocess.CalledProcessError, e: | |||
except subprocess.CalledProcessError: | |||
raise self.template.exception_class( | |||
"Cannot process %s. Error occurred when " | |||
"processing [%s]" % (stylus.name, resource.source_file)) | |||
@@ -1,213 +0,0 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
Contains classes and utilities related to grouping | |||
resources and nodes in hyde. | |||
""" | |||
from hyde.model import Expando | |||
from hyde.plugin import Plugin | |||
from hyde.site import Node, Resource | |||
from hyde.util import add_method, add_property, pairwalk | |||
from collections import namedtuple | |||
Grouper = namedtuple('Grouper', 'group resources') | |||
class Group(Expando): | |||
""" | |||
A wrapper class for groups. Adds methods for | |||
grouping resources. | |||
""" | |||
def __init__(self, grouping, parent=None): | |||
self.name = 'groups' | |||
self.parent = parent | |||
self.root = self | |||
self.root = parent.root if parent else self | |||
self.groups = [] | |||
self.sorter = getattr(grouping, 'sorter', None) | |||
if hasattr(parent, 'sorter'): | |||
self.sorter = parent.sorter | |||
super(Group, self).__init__(grouping) | |||
add_method(Node, | |||
'walk_%s_groups' % self.name, | |||
Group.walk_groups_in_node, | |||
group=self) | |||
add_method(Node, | |||
'walk_resources_grouped_by_%s' % self.name, | |||
Group.walk_resources, | |||
group=self) | |||
add_property(Resource, | |||
'%s_group' % self.name, | |||
Group.get_resource_group, | |||
group=self) | |||
add_method(Resource, | |||
'walk_%s_groups' % self.name, | |||
Group.walk_resource_groups, | |||
group=self) | |||
def set_expando(self, key, value): | |||
""" | |||
If the key is groups, creates group objects instead of | |||
regular expando objects. | |||
""" | |||
if key == "groups": | |||
self.groups = [Group(group, parent=self) for group in value] | |||
else: | |||
return super(Group, self).set_expando(key, value) | |||
@staticmethod | |||
def get_resource_group(resource, group): | |||
""" | |||
This method gets attached to the resource object. | |||
Returns group and its ancestors that the resource | |||
belongs to, in that order. | |||
""" | |||
try: | |||
group_name = getattr(resource.meta, group.root.name) | |||
except AttributeError: | |||
group_name = None | |||
return next((g for g in group.walk_groups() | |||
if g.name == group_name), None) \ | |||
if group_name \ | |||
else None | |||
@staticmethod | |||
def walk_resource_groups(resource, group): | |||
""" | |||
This method gets attached to the resource object. | |||
Returns group and its ancestors that the resource | |||
belongs to, in that order. | |||
""" | |||
try: | |||
group_name = getattr(resource.meta, group.root.name) | |||
except AttributeError: | |||
group_name = None | |||
if group_name: | |||
for g in group.walk_groups(): | |||
if g.name == group_name: | |||
return reversed(list(g.walk_hierarchy())) | |||
return [] | |||
@staticmethod | |||
def walk_resources(node, group): | |||
""" | |||
The method that gets attached to the node | |||
object for walking the resources in the node | |||
that belong to this group. | |||
""" | |||
for group in group.walk_groups(): | |||
for resource in group.walk_resources_in_node(node): | |||
yield resource | |||
@staticmethod | |||
def walk_groups_in_node(node, group): | |||
""" | |||
The method that gets attached to the node | |||
object for walking the groups in the node. | |||
""" | |||
walker = group.walk_groups() | |||
for g in walker: | |||
lister = g.walk_resources_in_node(node) | |||
yield Grouper(group=g, resources=lister) | |||
def walk_hierarchy(self): | |||
""" | |||
Walks the group hierarchy starting from | |||
this group. | |||
""" | |||
g = self | |||
yield g | |||
while g.parent: | |||
yield g.parent | |||
g = g.parent | |||
def walk_groups(self): | |||
""" | |||
Walks the groups in the current group | |||
""" | |||
yield self | |||
for group in self.groups: | |||
for child in group.walk_groups(): | |||
yield child | |||
def walk_resources_in_node(self, node): | |||
""" | |||
Walks the resources in the given node | |||
sorted based on sorter configuration in this | |||
group. | |||
""" | |||
walker = 'walk_resources' | |||
if hasattr(self, 'sorter') and self.sorter: | |||
walker = 'walk_resources_sorted_by_' + self.sorter | |||
walker = getattr(node, walker, getattr(node, 'walk_resources')) | |||
for resource in walker(): | |||
try: | |||
group_value = getattr(resource.meta, self.root.name) | |||
except AttributeError: | |||
continue | |||
if group_value == self.name: | |||
yield resource | |||
class GrouperPlugin(Plugin): | |||
""" | |||
Grouper plugin for hyde. Adds the ability to do | |||
group resources and nodes in an arbitrary | |||
hierarchy. | |||
Configuration example | |||
--------------------- | |||
#yaml | |||
sorter: | |||
kind: | |||
atts: source.kind | |||
grouper: | |||
hyde: | |||
# Categorizes the nodes and resources | |||
# based on the groups specified here. | |||
# The node and resource should be tagged | |||
# with the categories in their metadata | |||
sorter: kind # A reference to the sorter | |||
description: Articles about hyde | |||
groups: | |||
- | |||
name: announcements | |||
description: Hyde release announcements | |||
- | |||
name: making of | |||
description: Articles about hyde design decisions | |||
- | |||
name: tips and tricks | |||
description: > | |||
Helpful snippets and tweaks to | |||
make hyde more awesome. | |||
""" | |||
def __init__(self, site): | |||
super(GrouperPlugin, self).__init__(site) | |||
def begin_site(self): | |||
""" | |||
Initialize plugin. Add the specified groups to the | |||
site context variable. | |||
""" | |||
config = self.site.config | |||
if not hasattr(config, 'grouper'): | |||
return | |||
if not hasattr(self.site, 'grouper'): | |||
self.site.grouper = {} | |||
for name, grouping in self.site.config.grouper.__dict__.items(): | |||
grouping.name = name | |||
prev_att = 'prev_in_%s' % name | |||
next_att = 'next_in_%s' % name | |||
setattr(Resource, prev_att, None) | |||
setattr(Resource, next_att, None) | |||
self.site.grouper[name] = Group(grouping) | |||
walker = Group.walk_resources( | |||
self.site.content, self.site.grouper[name]) | |||
for prev, next in pairwalk(walker): | |||
setattr(next, prev_att, prev) | |||
setattr(prev, next_att, next) |
@@ -416,6 +416,65 @@ class JPEGOptimPlugin(CLTransformer): | |||
self.call_app(args) | |||
class JPEGTranPlugin(CLTransformer): | |||
""" | |||
Almost like jpegoptim except it uses jpegtran. jpegtran allows to make | |||
progressive JPEG. Unfortunately, it only does lossless compression. If | |||
you want both, you need to combine this plugin with jpegoptim one. | |||
""" | |||
def __init__(self, site): | |||
super(JPEGTranPlugin, self).__init__(site) | |||
@property | |||
def plugin_name(self): | |||
""" | |||
The name of the plugin. | |||
""" | |||
return "jpegtran" | |||
def option_prefix(self, option): | |||
return "-" | |||
def binary_resource_complete(self, resource): | |||
""" | |||
If the site is in development mode, just return. | |||
Otherwise, run jpegtran to compress the jpg file. | |||
""" | |||
try: | |||
mode = self.site.config.mode | |||
except AttributeError: | |||
mode = "production" | |||
if not resource.source_file.kind == 'jpg': | |||
return | |||
if mode.startswith('dev'): | |||
self.logger.debug("Skipping jpegtran in development mode.") | |||
return | |||
supported = [ | |||
"optimize", | |||
"progressive", | |||
"restart", | |||
"arithmetic", | |||
"perfect", | |||
"copy", | |||
] | |||
source = File(self.site.config.deploy_root_path.child( | |||
resource.relative_deploy_path)) | |||
target = File.make_temp('') | |||
jpegtran = self.app | |||
args = [unicode(jpegtran)] | |||
args.extend(self.process_args(supported)) | |||
args.extend(["-outfile", unicode(target), unicode(source)]) | |||
self.call_app(args) | |||
target.copy_to(source) | |||
target.delete() | |||
# | |||
# PNG Optimization | |||
# | |||
@@ -1,58 +0,0 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
jpegoptim plugin | |||
""" | |||
from hyde.plugin import CLTransformer | |||
from fswrap import File | |||
class JPEGOptimPlugin(CLTransformer): | |||
""" | |||
The plugin class for JPEGOptim | |||
""" | |||
def __init__(self, site): | |||
super(JPEGOptimPlugin, self).__init__(site) | |||
@property | |||
def plugin_name(self): | |||
""" | |||
The name of the plugin. | |||
""" | |||
return "jpegoptim" | |||
def binary_resource_complete(self, resource): | |||
""" | |||
If the site is in development mode, just return. | |||
Otherwise, run jpegoptim to compress the jpg file. | |||
""" | |||
try: | |||
mode = self.site.config.mode | |||
except AttributeError: | |||
mode = "production" | |||
if not resource.source_file.kind == 'jpg': | |||
return | |||
if mode.startswith('dev'): | |||
self.logger.debug("Skipping jpegoptim in development mode.") | |||
return | |||
supported = [ | |||
"force", | |||
"max=", | |||
"strip-all", | |||
"strip-com", | |||
"strip-exif", | |||
"strip-iptc", | |||
"strip-icc", | |||
] | |||
target = File(self.site.config.deploy_root_path.child( | |||
resource.relative_deploy_path)) | |||
jpegoptim = self.app | |||
args = [unicode(jpegoptim)] | |||
args.extend(self.process_args(supported)) | |||
args.extend(["-q", unicode(target)]) | |||
self.call_app(args) |
@@ -1,66 +0,0 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
jpegtran plugin | |||
Almost like jpegoptim except it uses jpegtran. jpegtran allows to make | |||
progressive JPEG. Unfortunately, it only does lossless compression. If | |||
you want both, you need to combine this plugin with jpegoptim one. | |||
""" | |||
from hyde.plugin import CLTransformer | |||
from hyde.fs import File | |||
class JPEGTranPlugin(CLTransformer): | |||
""" | |||
The plugin class for JPEGTran | |||
""" | |||
def __init__(self, site): | |||
super(JPEGTranPlugin, self).__init__(site) | |||
@property | |||
def plugin_name(self): | |||
""" | |||
The name of the plugin. | |||
""" | |||
return "jpegtran" | |||
def option_prefix(self, option): | |||
return "-" | |||
def binary_resource_complete(self, resource): | |||
""" | |||
If the site is in development mode, just return. | |||
Otherwise, run jpegtran to compress the jpg file. | |||
""" | |||
try: | |||
mode = self.site.config.mode | |||
except AttributeError: | |||
mode = "production" | |||
if not resource.source_file.kind == 'jpg': | |||
return | |||
if mode.startswith('dev'): | |||
self.logger.debug("Skipping jpegtran in development mode.") | |||
return | |||
supported = [ | |||
"optimize", | |||
"progressive", | |||
"restart", | |||
"arithmetic", | |||
"perfect", | |||
"copy", | |||
] | |||
source = File(self.site.config.deploy_root_path.child( | |||
resource.relative_deploy_path)) | |||
target = File.make_temp('') | |||
jpegtran = self.app | |||
args = [unicode(jpegtran)] | |||
args.extend(self.process_args(supported)) | |||
args.extend(["-outfile", unicode(target), unicode(source)]) | |||
self.call_app(args) | |||
target.copy_to(source) | |||
target.delete() |
@@ -2,6 +2,7 @@ | |||
""" | |||
JavaScript plugins | |||
""" | |||
import subprocess | |||
from hyde.plugin import CLTransformer | |||
@@ -80,3 +81,98 @@ class UglifyPlugin(CLTransformer): | |||
out = target.read_all() | |||
return out | |||
class RequireJSPlugin(CLTransformer): | |||
""" | |||
requirejs plugin | |||
Calls r.js optimizer in order to proces javascript files, | |||
bundle them into one single file and compress it. | |||
The call to r.js is being made with options -o and out. Example: | |||
r.js -o rjs.conf out=app.js | |||
whereas rjs.conf is the require.js configuration file pointing | |||
to the main javascript file as well as passing options to r.js. | |||
The bundled and compressed result is written to 'app.js' file | |||
within the deployment structure. | |||
Please see the homepage of requirejs for usage details. | |||
""" | |||
def __init__(self, site): | |||
super(RequireJSPlugin, self).__init__(site) | |||
@property | |||
def executable_name(self): | |||
return "r.js" | |||
def begin_site(self): | |||
for resource in self.site.content.walk_resources(): | |||
if resource.source_file.name == "rjs.conf": | |||
new_name = "app.js" | |||
target_folder = File(resource.relative_deploy_path).parent | |||
resource.relative_deploy_path = target_folder.child(new_name) | |||
def text_resource_complete(self, resource, text): | |||
if not resource.source_file.name == 'rjs.conf': | |||
return | |||
rjs = self.app | |||
target = File.make_temp('') | |||
args = [unicode(rjs)] | |||
args.extend(['-o', unicode(resource), ("out=" + target.fully_expanded_path)]) | |||
try: | |||
self.call_app(args) | |||
except subprocess.CalledProcessError: | |||
raise self.template.exception_class( | |||
"Cannot process %s. Error occurred when " | |||
"processing [%s]" % (self.app.name, resource.source_file)) | |||
return target.read_all() | |||
class CoffeePlugin(CLTransformer): | |||
""" | |||
The plugin class for Coffeescript | |||
""" | |||
def __init__(self, site): | |||
super(CoffeePlugin, self).__init__(site) | |||
@property | |||
def executable_name(self): | |||
return "coffee" | |||
@property | |||
def plugin_name(self): | |||
""" | |||
The name of the plugin. | |||
""" | |||
return "Coffee" | |||
def begin_site(self): | |||
""" | |||
Find all the coffee files and set their relative deploy path. | |||
""" | |||
for resource in self.site.content.walk_resources(): | |||
if resource.source_file.kind == 'coffee': | |||
new_name = resource.source_file.name_without_extension + ".js" | |||
target_folder = File(resource.relative_deploy_path).parent | |||
resource.relative_deploy_path = target_folder.child(new_name) | |||
def text_resource_complete(self, resource, text): | |||
""" | |||
Save the file to a temporary place and run the Coffee | |||
compiler. Read the generated file and return the text as | |||
output. | |||
""" | |||
if not resource.source_file.kind == 'coffee': | |||
return | |||
coffee = self.app | |||
source = File.make_temp(text) | |||
args = [unicode(coffee)] | |||
args.extend(["-c", "-p", unicode(source)]) | |||
return self.call_app(args) |
@@ -3,16 +3,19 @@ | |||
Contains classes and utilities related to meta data in hyde. | |||
""" | |||
import re | |||
from collections import namedtuple | |||
from operator import attrgetter | |||
from itertools import ifilter | |||
from functools import partial | |||
from itertools import ifilter | |||
from operator import attrgetter | |||
import re | |||
from hyde.model import Expando | |||
from hyde.plugin import Plugin | |||
from hyde.fs import File, Folder | |||
from hyde.site import Node, Resource | |||
from hyde.util import add_method, add_property, pairwalk | |||
from fswrap import File, Folder | |||
import yaml | |||
@@ -441,7 +444,7 @@ def attributes_checker(item, attributes=None): | |||
Checks if the given list of attributes exist. | |||
""" | |||
try: | |||
x = attrgetter(*attributes)(item) | |||
attrgetter(*attributes)(item) | |||
return True | |||
except AttributeError: | |||
return False | |||
@@ -1,71 +0,0 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
OPTIPNG plugin | |||
""" | |||
from hyde.plugin import CLTransformer | |||
from fswrap 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 = [unicode(optipng)] | |||
args.extend(self.process_args(supported)) | |||
args.extend([unicode(target)]) | |||
self.call_app(args) |
@@ -1,155 +0,0 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
Paginator plugin. Groups a sorted set of resources into pages and supplies | |||
each page to a copy of the original resource. | |||
""" | |||
import os | |||
from hyde.plugin import Plugin | |||
from hyde.site import Resource | |||
from hyde.util import pairwalk | |||
from hyde.ext.plugins.meta import Metadata | |||
from fswrap import File | |||
class Page: | |||
def __init__(self, posts, number): | |||
self.posts = posts | |||
self.number = number | |||
class Paginator: | |||
""" | |||
Iterates resources which have pages associated with them. | |||
""" | |||
file_pattern = 'page$PAGE/$FILE$EXT' | |||
def __init__(self, settings): | |||
self.sorter = getattr(settings, 'sorter', None) | |||
self.size = getattr(settings, 'size', 10) | |||
self.file_pattern = getattr(settings, 'file_pattern', self.file_pattern) | |||
def _relative_url(self, source_path, number, basename, ext): | |||
""" | |||
Create a new URL for a new page. The first page keeps the same name; | |||
the subsequent pages are named according to file_pattern. | |||
""" | |||
path = File(source_path) | |||
if number != 1: | |||
filename = self.file_pattern.replace('$PAGE', str(number)) \ | |||
.replace('$FILE', basename) \ | |||
.replace('$EXT', ext) | |||
path = path.parent.child(os.path.normpath(filename)) | |||
return path | |||
def _new_resource(self, base_resource, node, page_number): | |||
""" | |||
Create a new resource as a copy of a base_resource, with a page of | |||
resources associated with it. | |||
""" | |||
res = Resource(base_resource.source_file, node) | |||
res.node.meta = Metadata(node.meta) | |||
res.meta = Metadata(base_resource.meta, res.node.meta) | |||
path = self._relative_url(base_resource.relative_path, | |||
page_number, | |||
base_resource.source_file.name_without_extension, | |||
base_resource.source_file.extension) | |||
res.set_relative_deploy_path(path) | |||
return res | |||
@staticmethod | |||
def _attach_page_to_resource(page, resource): | |||
""" | |||
Hook up a page and a resource. | |||
""" | |||
resource.page = page | |||
page.resource = resource | |||
@staticmethod | |||
def _add_dependencies_to_resource(dependencies, resource): | |||
""" | |||
Add a bunch of resources as dependencies to another resource. | |||
""" | |||
if not hasattr(resource, 'depends'): | |||
resource.depends = [] | |||
resource.depends.extend([dep.relative_path for dep in dependencies | |||
if dep.relative_path not in resource.depends]) | |||
def _walk_pages_in_node(self, node): | |||
""" | |||
Segregate each resource into a page. | |||
""" | |||
walker = 'walk_resources' | |||
if self.sorter: | |||
walker = 'walk_resources_sorted_by_%s' % self.sorter | |||
walker = getattr(node, walker, getattr(node, 'walk_resources')) | |||
posts = list(walker()) | |||
number = 1 | |||
while posts: | |||
yield Page(posts[:self.size], number) | |||
posts = posts[self.size:] | |||
number += 1 | |||
def walk_paged_resources(self, node, resource): | |||
""" | |||
Group the resources and return the new page resources. | |||
""" | |||
added_resources = [] | |||
pages = list(self._walk_pages_in_node(node)) | |||
if pages: | |||
deps = reduce(list.__add__, [page.posts for page in pages], []) | |||
Paginator._attach_page_to_resource(pages[0], resource) | |||
Paginator._add_dependencies_to_resource(deps, resource) | |||
for page in pages[1:]: | |||
# make new resource | |||
new_resource = self._new_resource(resource, node, page.number) | |||
Paginator._attach_page_to_resource(page, new_resource) | |||
new_resource.depends = resource.depends | |||
added_resources.append(new_resource) | |||
for prev, next in pairwalk(pages): | |||
next.previous = prev | |||
prev.next = next | |||
return added_resources | |||
class PaginatorPlugin(Plugin): | |||
""" | |||
Paginator plugin. | |||
Configuration: in a resource's metadata: | |||
paginator: | |||
sorter: time | |||
size: 5 | |||
file_pattern: page$PAGE/$FILE$EXT # optional | |||
then in the resource's content: | |||
{% for res in resource.page.posts %} | |||
{% refer to res.url as post %} | |||
{{ post }} | |||
{% endfor %} | |||
{{ resource.page.previous }} | |||
{{ resource.page.next }} | |||
""" | |||
def __init__(self, site): | |||
super(PaginatorPlugin, self).__init__(site) | |||
def begin_site(self): | |||
for node in self.site.content.walk(): | |||
added_resources = [] | |||
paged_resources = (res for res in node.resources | |||
if hasattr(res.meta, 'paginator')) | |||
for resource in paged_resources: | |||
paginator = Paginator(resource.meta.paginator) | |||
added_resources += paginator.walk_paged_resources(node, resource) | |||
node.resources += added_resources |
@@ -1,57 +0,0 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
requirejs plugin | |||
Calls r.js optimizer in order to proces javascript files, | |||
bundle them into one single file and compress it. | |||
The call to r.js is being made with options -o and out. Example: | |||
r.js -o rjs.conf out=app.js | |||
whereas rjs.conf is the require.js configuration file pointing | |||
to the main javascript file as well as passing options to r.js. | |||
The bundled and compressed result is written to 'app.js' file | |||
within the deployment structure. | |||
Please see the homepage of requirejs for usage details. | |||
""" | |||
from hyde.plugin import CLTransformer | |||
from fswrap import File | |||
import subprocess | |||
class RequireJSPlugin(CLTransformer): | |||
def __init__(self, site): | |||
super(RequireJSPlugin, self).__init__(site) | |||
@property | |||
def executable_name(self): | |||
return "r.js" | |||
def begin_site(self): | |||
for resource in self.site.content.walk_resources(): | |||
if resource.source_file.name == "rjs.conf": | |||
new_name = "app.js" | |||
target_folder = File(resource.relative_deploy_path).parent | |||
resource.relative_deploy_path = target_folder.child(new_name) | |||
def text_resource_complete(self, resource, text): | |||
if not resource.source_file.name == 'rjs.conf': | |||
return | |||
rjs = self.app | |||
target = File.make_temp('') | |||
args = [unicode(rjs)] | |||
args.extend(['-o', unicode(resource), ("out=" + target.fully_expanded_path)]) | |||
try: | |||
self.call_app(args) | |||
except subprocess.CalledProcessError: | |||
raise self.template.exception_class( | |||
"Cannot process %s. Error occurred when " | |||
"processing [%s]" % (self.app.name, resource.source_file)) | |||
return target.read_all() |
@@ -1,129 +0,0 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
Contains classes and utilities related to sorting | |||
resources and nodes in hyde. | |||
""" | |||
from hyde.plugin import Plugin | |||
from hyde.site import Node, Resource | |||
from hyde.util import add_method, pairwalk | |||
from itertools import ifilter | |||
from functools import partial | |||
from operator import attrgetter | |||
def filter_method(item, settings=None): | |||
""" | |||
Returns true if all the filters in the | |||
given settings evaluate to True. | |||
""" | |||
all_match = True | |||
default_filters = {} | |||
filters = {} | |||
if hasattr(settings, 'filters'): | |||
filters.update(default_filters) | |||
filters.update(settings.filters.__dict__) | |||
for field, value in filters.items(): | |||
try: | |||
res = attrgetter(field)(item) | |||
except: | |||
res = None | |||
if res != value: | |||
all_match = False | |||
break | |||
return all_match | |||
def attributes_checker(item, attributes=None): | |||
""" | |||
Checks if the given list of attributes exist. | |||
""" | |||
try: | |||
attrgetter(*attributes)(item) | |||
return True | |||
except AttributeError: | |||
return False | |||
def sort_method(node, settings=None): | |||
""" | |||
Sorts the resources in the given node based on the | |||
given settings. | |||
""" | |||
attr = 'name' | |||
if settings and hasattr(settings, 'attr') and settings.attr: | |||
attr = settings.attr | |||
reverse = False | |||
if settings and hasattr(settings, 'reverse'): | |||
reverse = settings.reverse | |||
if not isinstance(attr, list): | |||
attr = [attr] | |||
filter_ = partial(filter_method, settings=settings) | |||
excluder_ = partial(attributes_checker, attributes=attr) | |||
resources = ifilter(lambda x: excluder_(x) and filter_(x), | |||
node.walk_resources()) | |||
return sorted(resources, | |||
key=attrgetter(*attr), | |||
reverse=reverse) | |||
class SorterPlugin(Plugin): | |||
""" | |||
Sorter plugin for hyde. Adds the ability to do | |||
sophisticated sorting by expanding the site objects | |||
to support prebuilt sorting methods. These methods | |||
can be used in the templates directly. | |||
Configuration example | |||
--------------------- | |||
#yaml | |||
sorter: | |||
kind: | |||
# Sorts by this attribute name | |||
# Uses `attrgetter` on the resource object | |||
attr: source_file.kind | |||
# The filters to be used before sorting | |||
# This can be used to remove all the items | |||
# that do not apply. For example, | |||
# filtering non html content | |||
filters: | |||
source_file.kind: html | |||
is_processable: True | |||
meta.is_listable: True | |||
""" | |||
def __init__(self, site): | |||
super(SorterPlugin, self).__init__(site) | |||
def begin_site(self): | |||
""" | |||
Initialize plugin. Add a sort and match method | |||
for every configuration mentioned in site settings | |||
""" | |||
config = self.site.config | |||
if not hasattr(config, 'sorter'): | |||
return | |||
for name, settings in config.sorter.__dict__.items(): | |||
sort_method_name = 'walk_resources_sorted_by_%s' % name | |||
self.logger.debug("Adding sort methods for [%s]" % name) | |||
add_method(Node, sort_method_name, sort_method, settings=settings) | |||
match_method_name = 'is_%s' % name | |||
add_method(Resource, match_method_name, filter_method, settings) | |||
prev_att = 'prev_by_%s' % name | |||
next_att = 'next_by_%s' % name | |||
setattr(Resource, prev_att, None) | |||
setattr(Resource, next_att, None) | |||
walker = getattr(self.site.content, | |||
sort_method_name, | |||
self.site.content.walk_resources) | |||
for prev, next in pairwalk(walker()): | |||
setattr(prev, next_att, next) | |||
setattr(next, prev_att, prev) | |||
@@ -3,13 +3,13 @@ | |||
Plugins related to structure | |||
""" | |||
from hyde.ext.plugins.meta import Metadata | |||
from hyde.plugin import Plugin | |||
from fswrap import File, Folder | |||
from hyde.site import Resource | |||
from hyde.util import pairwalk | |||
from fswrap import File, Folder | |||
import os | |||
from fnmatch import fnmatch | |||
import operator | |||
@@ -225,6 +225,8 @@ class Paginator: | |||
resources associated with it. | |||
""" | |||
res = Resource(base_resource.source_file, node) | |||
res.node.meta = Metadata(node.meta) | |||
res.meta = Metadata(base_resource.meta, res.node.meta) | |||
path = self._relative_url(base_resource.relative_path, | |||
page_number, | |||
base_resource.source_file.name_without_extension, | |||
@@ -272,20 +274,21 @@ class Paginator: | |||
""" | |||
added_resources = [] | |||
pages = list(self._walk_pages_in_node(node)) | |||
deps = reduce(list.__add__, [page.posts for page in pages], []) | |||
Paginator._attach_page_to_resource(pages[0], resource) | |||
Paginator._add_dependencies_to_resource(deps, resource) | |||
for page in pages[1:]: | |||
# make new resource | |||
new_resource = self._new_resource(resource, node, page.number) | |||
Paginator._attach_page_to_resource(page, new_resource) | |||
new_resource.depends = resource.depends | |||
added_resources.append(new_resource) | |||
for prev, next in pairwalk(pages): | |||
next.previous = prev | |||
prev.next = next | |||
if pages: | |||
deps = reduce(list.__add__, [page.posts for page in pages], []) | |||
Paginator._attach_page_to_resource(pages[0], resource) | |||
Paginator._add_dependencies_to_resource(deps, resource) | |||
for page in pages[1:]: | |||
# make new resource | |||
new_resource = self._new_resource(resource, node, page.number) | |||
Paginator._attach_page_to_resource(page, new_resource) | |||
new_resource.depends = resource.depends | |||
added_resources.append(new_resource) | |||
for prev, next in pairwalk(pages): | |||
next.previous = prev | |||
prev.next = next | |||
return added_resources | |||
@@ -1,210 +0,0 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
Contains classes and utilities related to tagging | |||
resources in hyde. | |||
""" | |||
from hyde.model import Expando | |||
from hyde.plugin import Plugin | |||
from hyde.site import Node | |||
from hyde.util import add_method | |||
from operator import attrgetter | |||
from fswrap import File, Folder | |||
class Tag(Expando): | |||
""" | |||
A simple object that represents a tag. | |||
""" | |||
def __init__(self, name): | |||
""" | |||
Initialize the tag with a name. | |||
""" | |||
self.name = name | |||
self.resources = [] | |||
def __repr__(self): | |||
return self.name | |||
def __str__(self): | |||
return self.name | |||
def get_tagger_sort_method(site): | |||
config = site.config | |||
content = site.content | |||
walker = 'walk_resources' | |||
sorter = None | |||
try: | |||
sorter = attrgetter('tagger.sorter')(config) | |||
walker = walker + '_sorted_by_%s' % sorter | |||
except AttributeError: | |||
pass | |||
try: | |||
walker = getattr(content, walker) | |||
except AttributeError: | |||
raise self.template.exception_class( | |||
"Cannot find the sorter: %s" % sorter) | |||
return walker | |||
def walk_resources_tagged_with(node, tag): | |||
tags = set(unicode(tag).split('+')) | |||
walker = get_tagger_sort_method(node.site) | |||
for resource in walker(): | |||
try: | |||
taglist = set(attrgetter("meta.tags")(resource)) | |||
except AttributeError: | |||
continue | |||
if tags <= taglist: | |||
yield resource | |||
class TaggerPlugin(Plugin): | |||
""" | |||
Tagger plugin for hyde. Adds the ability to do tag resources and search | |||
based on the tags. | |||
Configuration example | |||
--------------------- | |||
#yaml | |||
sorter: | |||
kind: | |||
atts: source.kind | |||
tagger: | |||
sorter: kind # How to sort the resources in a tag | |||
archives: | |||
blog: | |||
template: tagged_posts.j2 | |||
source: blog | |||
target: blog/tags | |||
archive_extension: html | |||
""" | |||
def __init__(self, site): | |||
super(TaggerPlugin, self).__init__(site) | |||
def begin_site(self): | |||
""" | |||
Initialize plugin. Add tag to the site context variable | |||
and methods for walking tagged resources. | |||
""" | |||
self.logger.debug("Adding tags from metadata") | |||
config = self.site.config | |||
content = self.site.content | |||
tags = {} | |||
add_method(Node, | |||
'walk_resources_tagged_with', walk_resources_tagged_with) | |||
walker = get_tagger_sort_method(self.site) | |||
for resource in walker(): | |||
self._process_tags_in_resource(resource, tags) | |||
self._process_tag_metadata(tags) | |||
self.site.tagger = Expando(dict(tags=tags)) | |||
self._generate_archives() | |||
def _process_tag_metadata(self, tags): | |||
""" | |||
Parses and adds metadata to the tagger object, if the tagger | |||
configuration contains metadata. | |||
""" | |||
try: | |||
tag_meta = self.site.config.tagger.tags.to_dict() | |||
except AttributeError: | |||
tag_meta = {} | |||
for tagname, meta in tag_meta.iteritems(): | |||
# Don't allow name and resources in meta | |||
if 'resources' in meta: | |||
del(meta['resources']) | |||
if 'name' in meta: | |||
del(meta['name']) | |||
if tagname in tags: | |||
tags[tagname].update(meta) | |||
def _process_tags_in_resource(self, resource, tags): | |||
""" | |||
Reads the tags associated with this resource and | |||
adds them to the tag list if needed. | |||
""" | |||
try: | |||
taglist = attrgetter("meta.tags")(resource) | |||
except AttributeError: | |||
return | |||
for tagname in taglist: | |||
if not tagname in tags: | |||
tag = Tag(tagname) | |||
tags[tagname] = tag | |||
tag.resources.append(resource) | |||
add_method(Node, | |||
'walk_resources_tagged_with_%s' % tagname, | |||
walk_resources_tagged_with, | |||
tag=tag) | |||
else: | |||
tags[tagname].resources.append(resource) | |||
if not hasattr(resource, 'tags'): | |||
setattr(resource, 'tags', []) | |||
resource.tags.append(tags[tagname]) | |||
def _generate_archives(self): | |||
""" | |||
Generates archives if the configuration demands. | |||
""" | |||
archive_config = None | |||
try: | |||
archive_config = attrgetter("tagger.archives")(self.site.config) | |||
except AttributeError: | |||
return | |||
self.logger.debug("Generating archives for tags") | |||
for name, config in archive_config.to_dict().iteritems(): | |||
self._create_tag_archive(config) | |||
def _create_tag_archive(self, config): | |||
""" | |||
Generates archives for each tag based on the given configuration. | |||
""" | |||
if not 'template' in config: | |||
raise self.template.exception_class( | |||
"No Template specified in tagger configuration.") | |||
content = self.site.content.source_folder | |||
source = Folder(config.get('source', '')) | |||
target = content.child_folder(config.get('target', 'tags')) | |||
if not target.exists: | |||
target.make() | |||
# Write meta data for the configuration | |||
meta = config.get('meta', {}) | |||
meta_text = u'' | |||
if meta: | |||
import yaml | |||
meta_text = yaml.dump(meta, default_flow_style=False) | |||
extension = config.get('extension', 'html') | |||
template = config['template'] | |||
archive_text = u""" | |||
--- | |||
extends: false | |||
%(meta)s | |||
--- | |||
{%% set tag = site.tagger.tags['%(tag)s'] %%} | |||
{%% set source = site.content.node_from_relative_path('%(node)s') %%} | |||
{%% set walker = source['walk_resources_tagged_with_%(tag)s'] %%} | |||
{%% extends "%(template)s" %%} | |||
""" | |||
for tagname, tag in self.site.tagger.tags.to_dict().iteritems(): | |||
tag_data = { | |||
"tag": tagname, | |||
"node": source.name, | |||
"template": template, | |||
"meta": meta_text | |||
} | |||
text = archive_text % tag_data | |||
archive_file = File(target.child("%s.%s" % (tagname, extension))) | |||
archive_file.delete() | |||
archive_file.write(text.strip()) | |||
self.site.content.add_resource(archive_file) |
@@ -19,6 +19,31 @@ from fswrap import File | |||
logger = getLoggerWithNullHandler('hyde.engine') | |||
# Plugins have been reorganized. Map old plugin paths to new. | |||
PLUGINS_OLD_AND_NEW = { | |||
"hyde.ext.plugins.less.LessCSSPlugin" : "hyde.ext.plugins.css.LessCSSPlugin", | |||
"hyde.ext.plugins.stylus.StylusPlugin" : "hyde.ext.plugins.css.StylusPlugin", | |||
"hyde.ext.plugins.jpegoptim.JPEGOptimPlugin" : "hyde.ext.plugins.images.JPEGOptimPlugin", | |||
"hyde.ext.plugins.optipng.OptiPNGPlugin" : "hyde.ext.plugins.images.OptiPNGPlugin", | |||
"hyde.ext.plugins.jpegtran.JPEGTranPlugin" : "hyde.ext.plugins.images.JPEGTranPlugin", | |||
"hyde.ext.plugins.uglify.UglifyPlugin": "hyde.ext.plugins.js.UglifyPlugin", | |||
"hyde.ext.plugins.requirejs.RequireJSPlugin": "hyde.ext.plugins.js.RequireJSPlugin", | |||
"hyde.ext.plugins.coffee.CoffeePlugin": "hyde.ext.plugins.js.CoffeePlugin", | |||
"hyde.ext.plugins.sorter.SorterPlugin": "hyde.ext.plugins.meta.SorterPlugin", | |||
"hyde.ext.plugins.grouper.GrouperPlugin": "hyde.ext.plugins.meta.GrouperPlugin", | |||
"hyde.ext.plugins.tagger.TaggerPlugin": "hyde.ext.plugins.meta.TaggerPlugin", | |||
"hyde.ext.plugins.auto_extend.AutoExtendPlugin": "hyde.ext.plugins.meta.AutoExtendPlugin", | |||
"hyde.ext.plugins.folders.FlattenerPlugin": "hyde.ext.plugins.structure.FlattenerPlugin", | |||
"hyde.ext.plugins.combine.CombinePlugin": "hyde.ext.plugins.structure.CombinePlugin", | |||
"hyde.ext.plugins.paginator.PaginatorPlugin": "hyde.ext.plugins.structure.PaginatorPlugin", | |||
"hyde.ext.plugins.blockdown.BlockdownPlugin": "hyde.ext.plugins.text.BlockdownPlugin", | |||
"hyde.ext.plugins.markings.MarkingsPlugin": "hyde.ext.plugins.text.MarkingsPlugin", | |||
"hyde.ext.plugins.markings.ReferencePlugin": "hyde.ext.plugins.text.ReferencePlugin", | |||
"hyde.ext.plugins.syntext.SyntextPlugin": "hyde.ext.plugins.text.SyntextPlugin", | |||
"hyde.ext.plugins.textlinks.TextlinksPlugin": "hyde.ext.plugins.text.TextlinksPlugin", | |||
"hyde.ext.plugins.git.GitDatesPlugin": "hyde.ext.plugins.vcs.GitDatesPlugin" | |||
} | |||
class PluginProxy(object): | |||
""" | |||
A proxy class to raise events in registered plugins | |||
@@ -261,7 +286,11 @@ class Plugin(object): | |||
Loads plugins based on the configuration. Assigns the plugins to | |||
'site.plugins' | |||
""" | |||
site.plugins = [load_python_object(name)(site) | |||
def load_plugin(name): | |||
plugin_name = PLUGINS_OLD_AND_NEW.get(name, name) | |||
return load_python_object(plugin_name)(site) | |||
site.plugins = [load_plugin(name) | |||
for name in site.config.plugins] | |||
@staticmethod | |||
@@ -4,14 +4,7 @@ Use nose | |||
`$ pip install nose` | |||
`$ nosetests` | |||
""" | |||
from hyde.ext.plugins.meta import MetaPlugin | |||
from hyde.ext.plugins.meta import SorterPlugin | |||
<<<<<<< HEAD | |||
from hyde.ext.plugins.grouper import GrouperPlugin | |||
======= | |||
from hyde.ext.plugins.meta import GrouperPlugin | |||
from hyde.fs import File, Folder | |||
>>>>>>> Move the grouper plugin into meta module. | |||
from hyde.ext.plugins.meta import GrouperPlugin, MetaPlugin, SorterPlugin | |||
from hyde.generator import Generator | |||
from hyde.site import Site | |||
from hyde.model import Config, Expando | |||
@@ -24,7 +24,7 @@ class TestRequireJS(object): | |||
def test_can_execute_rjs(self): | |||
s = Site(TEST_SITE) | |||
s.config.plugins = ['hyde.ext.plugins.requirejs.RequireJSPlugin'] | |||
s.config.plugins = ['hyde.ext.plugins.js.RequireJSPlugin'] | |||
source = TEST_SITE.child('content/media/js/rjs.conf') | |||
target = File(Folder(s.config.deploy_root_path).child('media/js/app.js')) | |||
gen = Generator(s) | |||
@@ -4,13 +4,7 @@ Use nose | |||
`$ pip install nose` | |||
`$ nosetests` | |||
""" | |||
from hyde.ext.plugins.meta import MetaPlugin | |||
<<<<<<< HEAD | |||
from hyde.ext.plugins.sorter import SorterPlugin | |||
======= | |||
from hyde.ext.plugins.meta import SorterPlugin | |||
from hyde.fs import File, Folder | |||
>>>>>>> Move the sorter plugin into the meta module. | |||
from hyde.ext.plugins.meta import MetaPlugin, SorterPlugin | |||
from hyde.generator import Generator | |||
from hyde.site import Site | |||
from hyde.model import Config, Expando | |||