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