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)) | args.append(unicode(source)) | ||||
try: | try: | ||||
self.call_app(args) | self.call_app(args) | ||||
except subprocess.CalledProcessError, e: | |||||
except subprocess.CalledProcessError: | |||||
raise self.template.exception_class( | raise self.template.exception_class( | ||||
"Cannot process %s. Error occurred when " | "Cannot process %s. Error occurred when " | ||||
"processing [%s]" % (stylus.name, resource.source_file)) | "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) | 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 | # 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 | JavaScript plugins | ||||
""" | """ | ||||
import subprocess | |||||
from hyde.plugin import CLTransformer | from hyde.plugin import CLTransformer | ||||
@@ -80,3 +81,98 @@ class UglifyPlugin(CLTransformer): | |||||
out = target.read_all() | out = target.read_all() | ||||
return out | 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. | Contains classes and utilities related to meta data in hyde. | ||||
""" | """ | ||||
import re | |||||
from collections import namedtuple | from collections import namedtuple | ||||
from operator import attrgetter | |||||
from itertools import ifilter | |||||
from functools import partial | from functools import partial | ||||
from itertools import ifilter | |||||
from operator import attrgetter | |||||
import re | |||||
from hyde.model import Expando | from hyde.model import Expando | ||||
from hyde.plugin import Plugin | from hyde.plugin import Plugin | ||||
from hyde.fs import File, Folder | |||||
from hyde.site import Node, Resource | from hyde.site import Node, Resource | ||||
from hyde.util import add_method, add_property, pairwalk | from hyde.util import add_method, add_property, pairwalk | ||||
from fswrap import File, Folder | |||||
import yaml | import yaml | ||||
@@ -441,7 +444,7 @@ def attributes_checker(item, attributes=None): | |||||
Checks if the given list of attributes exist. | Checks if the given list of attributes exist. | ||||
""" | """ | ||||
try: | try: | ||||
x = attrgetter(*attributes)(item) | |||||
attrgetter(*attributes)(item) | |||||
return True | return True | ||||
except AttributeError: | except AttributeError: | ||||
return False | 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 | Plugins related to structure | ||||
""" | """ | ||||
from hyde.ext.plugins.meta import Metadata | |||||
from hyde.plugin import Plugin | from hyde.plugin import Plugin | ||||
from fswrap import File, Folder | |||||
from hyde.site import Resource | from hyde.site import Resource | ||||
from hyde.util import pairwalk | from hyde.util import pairwalk | ||||
from fswrap import File, Folder | |||||
import os | import os | ||||
from fnmatch import fnmatch | from fnmatch import fnmatch | ||||
import operator | import operator | ||||
@@ -225,6 +225,8 @@ class Paginator: | |||||
resources associated with it. | resources associated with it. | ||||
""" | """ | ||||
res = Resource(base_resource.source_file, node) | 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, | path = self._relative_url(base_resource.relative_path, | ||||
page_number, | page_number, | ||||
base_resource.source_file.name_without_extension, | base_resource.source_file.name_without_extension, | ||||
@@ -272,20 +274,21 @@ class Paginator: | |||||
""" | """ | ||||
added_resources = [] | added_resources = [] | ||||
pages = list(self._walk_pages_in_node(node)) | 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 | 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') | 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): | class PluginProxy(object): | ||||
""" | """ | ||||
A proxy class to raise events in registered plugins | 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 | Loads plugins based on the configuration. Assigns the plugins to | ||||
'site.plugins' | '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] | for name in site.config.plugins] | ||||
@staticmethod | @staticmethod | ||||
@@ -4,14 +4,7 @@ Use nose | |||||
`$ pip install nose` | `$ pip install nose` | ||||
`$ nosetests` | `$ 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.generator import Generator | ||||
from hyde.site import Site | from hyde.site import Site | ||||
from hyde.model import Config, Expando | from hyde.model import Config, Expando | ||||
@@ -24,7 +24,7 @@ class TestRequireJS(object): | |||||
def test_can_execute_rjs(self): | def test_can_execute_rjs(self): | ||||
s = Site(TEST_SITE) | 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') | source = TEST_SITE.child('content/media/js/rjs.conf') | ||||
target = File(Folder(s.config.deploy_root_path).child('media/js/app.js')) | target = File(Folder(s.config.deploy_root_path).child('media/js/app.js')) | ||||
gen = Generator(s) | gen = Generator(s) | ||||
@@ -4,13 +4,7 @@ Use nose | |||||
`$ pip install nose` | `$ pip install nose` | ||||
`$ nosetests` | `$ 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.generator import Generator | ||||
from hyde.site import Site | from hyde.site import Site | ||||
from hyde.model import Config, Expando | from hyde.model import Config, Expando | ||||