@@ -1 +0,0 @@ | |||
# -*- coding: utf-8 -*- |
@@ -6,12 +6,22 @@ from commando import * | |||
from hyde.exceptions import HydeException | |||
from hyde.fs import File, Folder | |||
from hyde.layout import Layout, HYDE_DATA | |||
from hyde.model import Config | |||
from hyde.site import Site | |||
from hyde.version import __version__ | |||
import logging | |||
import os | |||
import yaml | |||
HYDE_LAYOUTS = "HYDE_LAYOUTS" | |||
logger = logging.getLogger('hyde.engine') | |||
logger.setLevel(logging.DEBUG) | |||
import sys | |||
logger.addHandler(logging.StreamHandler(sys.stdout)) | |||
class Engine(Application): | |||
""" | |||
The Hyde Application | |||
@@ -29,36 +39,67 @@ class Engine(Application): | |||
""" | |||
pass | |||
@subcommand('init', help='Create a new hyde site') | |||
@subcommand('create', help='Create a new hyde site') | |||
@store('-l', '--layout', default='basic', help='Layout for the new site') | |||
@true('-f', '--force', default=False, dest='overwrite', | |||
help='Overwrite the current site if it exists') | |||
def init(self, args): | |||
def create(self, args): | |||
""" | |||
The initialize command. Creates a new site from the template at the given | |||
The create command. Creates a new site from the template at the given | |||
sitepath. | |||
""" | |||
sitepath = File(args.sitepath) | |||
sitepath = Folder(args.sitepath) | |||
if sitepath.exists and not args.overwrite: | |||
raise HydeException("The given site path[%s] is not empty" % sitepath) | |||
layout = Layout.find_layout(args.layout) | |||
logger.info("Creating site at [%s] with layout [%s]" % (sitepath, layout)) | |||
if not layout or not layout.exists: | |||
raise HydeException( | |||
"The given layout is invalid. Please check if you have the `layout` " | |||
"in the right place and the environment variable(%s) has been setup " | |||
"properly if you are using custom path for layouts" % HYDE_DATA) | |||
layout.copy_contents_to(args.sitepath) | |||
logger.info("Site creation complete") | |||
@subcommand('gen', help='Generate the site') | |||
@store('-c', '--config-path', default='site.yaml', help='The configuration used to generate the site') | |||
@store('-c', '--config-path', default='site.yaml', dest='config', | |||
help='The configuration used to generate the site') | |||
@store('-d', '--deploy-path', default='deploy', help='Where should the site be generated?') | |||
def gen(self, args): | |||
""" | |||
The generate command. Generates the site at the given deployment directory. | |||
""" | |||
sitepath = File(args.sitepath) | |||
sitepath = Folder(args.sitepath) | |||
logger.info("Generating site at [%s]" % sitepath) | |||
# Read the configuration | |||
# Find the appropriate template environment | |||
config_file = sitepath.child(args.config) | |||
logger.info("Reading site configuration from [%s]", config_file) | |||
conf = {} | |||
with open(config_file) as stream: | |||
conf = yaml.load(stream) | |||
site = Site(sitepath, Config(sitepath, conf)) | |||
# TODO: Find the appropriate template environment | |||
from hyde.ext.templates.jinja import Jinja2Template | |||
template = Jinja2Template(sitepath) | |||
logger.info("Using [%s] as the template", template) | |||
# Configure the environment | |||
logger.info("Configuring Template environment") | |||
template.configure(site.config) | |||
# Prepare site info | |||
# Generate site one file at a time | |||
logger.info("Analyzing site contents") | |||
site.build() | |||
context = dict(site=site) | |||
# Generate site one file at a time | |||
logger.info("Generating site to [%s]" % site.config.deploy_root_path) | |||
for page in site.content.walk_resources(): | |||
logger.info("Processing [%s]", page) | |||
target = File(page.source_file.get_mirror(site.config.deploy_root_path, site.content.source_folder)) | |||
target.parent.make() | |||
if page.source_file.is_text: | |||
logger.info("Rendering [%s]", page) | |||
context.update(page=page) | |||
text = template.render(page, context) | |||
target.write(text) | |||
else: | |||
logger.info("Copying binary file [%s]", page) | |||
page.source_file.copy_to(target) |
@@ -1,28 +1,57 @@ | |||
""" | |||
Jinja template utilties | |||
""" | |||
from hyde.fs import File, Folder | |||
from hyde.template import Template | |||
from jinja2 import Environment, FileSystemLoader | |||
from jinja2 import contextfunction, Environment, FileSystemLoader, Undefined | |||
class LoyalUndefined(Undefined): | |||
def __getattr__(self, name): | |||
return self | |||
__getitem__ = __getattr__ | |||
def __call__(self, *args, **kwargs): | |||
return self | |||
@contextfunction | |||
def media_url(context, path): | |||
site = context['site'] | |||
return Folder(site.config.media_url).child(path) | |||
@contextfunction | |||
def content_url(context, path): | |||
site = context['site'] | |||
return Folder(site.config.base_url).child(path) | |||
# pylint: disable-msg=W0104,E0602,W0613,R0201 | |||
class Jinja2Template(Template): | |||
""" | |||
The Jinja2 Template implementation | |||
""" | |||
def __init__(self, sitepath): | |||
super(Jinja2Template, self).__init__(sitepath) | |||
self.env = Environment(loader=FileSystemLoader(sitepath)) | |||
def configure(self, config): | |||
""" | |||
Uses the config object to initialize the jinja environment. | |||
""" | |||
pass | |||
if config: | |||
loader = FileSystemLoader([ | |||
str(config.content_root_path), | |||
str(config.media_root_path), | |||
str(config.layout_root_path), | |||
]) | |||
self.env = Environment(loader=loader, undefined=LoyalUndefined) | |||
self.env.globals['media_url'] = media_url | |||
self.env.globals['content_url'] = content_url | |||
def render(self, template_name, context): | |||
def render(self, resource, context): | |||
""" | |||
Renders the given template using the context | |||
Renders the given resource using the context | |||
""" | |||
template = self.env.get_template(template_name) | |||
template = self.env.get_template(resource.relative_path) | |||
return template.render(context) |
@@ -7,14 +7,13 @@ for common operations to provide a single interface. | |||
""" | |||
import codecs | |||
import contextlib | |||
import logging | |||
from logging import NullHandler | |||
import mimetypes | |||
import os | |||
import shutil | |||
from distutils import dir_util | |||
import functools | |||
import itertools | |||
# pylint: disable-msg=E0611 | |||
logger = logging.getLogger('fs') | |||
@@ -29,7 +28,7 @@ class FS(object): | |||
""" | |||
def __init__(self, path): | |||
super(FS, self).__init__() | |||
self.path = str(path).strip().rstrip(os.sep) | |||
self.path = os.path.expanduser(str(path).strip().rstrip(os.sep)) | |||
def __str__(self): | |||
return self.path | |||
@@ -85,18 +84,23 @@ class FS(object): | |||
f = f.parent | |||
def is_descendant_of(self, ancestor): | |||
stop = Folder(ancestor) | |||
for folder in self.ancestors(): | |||
if folder == stop: | |||
return True | |||
if stop.depth > folder.depth: | |||
return False | |||
return False | |||
""" | |||
Checks if this folder is inside the given ancestor. | |||
""" | |||
stop = Folder(ancestor) | |||
for folder in self.ancestors(): | |||
if folder == stop: | |||
return True | |||
if stop.depth > folder.depth: | |||
return False | |||
return False | |||
def get_relative_path(self, root): | |||
""" | |||
Gets the fragment of the current path starting at root. | |||
""" | |||
if self == root: | |||
return '' | |||
return functools.reduce(lambda f, p: Folder(p.name).child(f), self.ancestors(stop=root), self.name) | |||
def get_mirror(self, target_root, source_root=None): | |||
@@ -123,7 +127,7 @@ class FS(object): | |||
Returns a File or Folder object that would represent this entity | |||
if it were copied or moved to `destination`. | |||
""" | |||
if (isinstance(destination, File) or os.path.isfile(str(destination))): | |||
if isinstance(destination, File) or os.path.isfile(str(destination)): | |||
return destination | |||
else: | |||
return FS.file_or_folder(Folder(destination).child(self.name)) | |||
@@ -156,6 +160,19 @@ class File(FS): | |||
""" | |||
return self.extension.lstrip(".") | |||
@property | |||
def mimetype(self): | |||
(mime, encoding) = mimetypes.guess_type(self.path) | |||
return mime | |||
@property | |||
def is_text(self): | |||
return self.mimetype.split("/")[0] == "text" | |||
@property | |||
def is_image(self): | |||
return self.mimetype.split("/")[0] == "image" | |||
def read_all(self, encoding='utf-8'): | |||
""" | |||
Reads from the file and returns the content as a string. | |||
@@ -224,20 +241,56 @@ class FSVisitor(object): | |||
class FolderWalker(FSVisitor): | |||
""" | |||
Walks the entire hirearchy of this directory starting with itself. | |||
Calls self.visit_folder first and then calls self.visit_file for | |||
any files found. After all files and folders have been exhausted | |||
self.visit_complete is called. | |||
If a pattern is provided, only the files that match the pattern are | |||
processed. | |||
If visitor.visit_folder returns False, the files in the folder are not | |||
processed. | |||
""" | |||
def walk(self, walk_folders=False, walk_files=False): | |||
""" | |||
A simple generator that yields a File or Folder object based on | |||
the arguments. | |||
""" | |||
if walk_files or walk_folders: | |||
for root, dirs, a_files in os.walk(self.folder.path): | |||
folder = Folder(root) | |||
if walk_folders: | |||
yield folder | |||
if walk_files: | |||
for a_file in a_files: | |||
if not self.pattern or fnmatch.fnmatch(a_file, self.pattern): | |||
yield File(folder.child(a_file)) | |||
def walk_all(self): | |||
""" | |||
Yield both Files and Folders as the tree is walked. | |||
""" | |||
return self.walk(walk_folders=True, walk_files=True) | |||
def walk_files(self): | |||
""" | |||
Yield only Files. | |||
""" | |||
return self.walk(walk_folders=False, walk_files=True) | |||
def walk_folders(self): | |||
""" | |||
Yield only Folders. | |||
""" | |||
return self.walk(walk_folders=True, walk_files=False) | |||
def __exit__(self, exc_type, exc_val, exc_tb): | |||
""" | |||
Automatically walk the folder when the context manager is exited. | |||
Calls self.visit_folder first and then calls self.visit_file for | |||
any files found. After all files and folders have been exhausted | |||
self.visit_complete is called. | |||
If visitor.visit_folder returns False, the files in the folder are not | |||
processed. | |||
""" | |||
def __visit_folder__(folder): | |||
@@ -271,18 +324,54 @@ class FolderWalker(FSVisitor): | |||
class FolderLister(FSVisitor): | |||
""" | |||
Lists the contents of this directory starting with itself. | |||
Calls self.visit_folder first and then calls self.visit_file for | |||
any files found. After all files and folders have been exhausted | |||
self.visit_complete is called. | |||
Lists the contents of this directory. | |||
If a pattern is provided, only the files that match the pattern are | |||
processed. | |||
""" | |||
def list(self, list_folders=False, list_files=False): | |||
""" | |||
A simple generator that yields a File or Folder object based on | |||
the arguments. | |||
""" | |||
a_files = os.listdir(self.folder.path) | |||
for a_file in a_files: | |||
path = self.folder.child(a_file) | |||
if os.path.isdir(path): | |||
if list_folders: | |||
yield Folder(path) | |||
elif list_files: | |||
if not self.pattern or fnmatch.fnmatch(a_file, self.pattern): | |||
yield File(path) | |||
def list_all(self): | |||
""" | |||
Yield both Files and Folders as the folder is listed. | |||
""" | |||
return self.list(list_folders=True, list_files=True) | |||
def list_files(self): | |||
""" | |||
Yield only Files. | |||
""" | |||
return self.list(list_folders=False, list_files=True) | |||
def list_folders(self): | |||
""" | |||
Yield only Folders. | |||
""" | |||
return self.list(list_folders=True, list_files=False) | |||
def __exit__(self, exc_type, exc_val, exc_tb): | |||
""" | |||
Automatically list the folder contents when the context manager is exited. | |||
Calls self.visit_folder first and then calls self.visit_file for | |||
any files found. After all files and folders have been exhausted | |||
self.visit_complete is called. | |||
""" | |||
a_files = os.listdir(self.folder.path) | |||
@@ -307,13 +396,13 @@ class Folder(FS): | |||
""" | |||
Returns a folder object by combining the fragment to this folder's path | |||
""" | |||
return Folder(os.path.join(self.path, fragment)) | |||
return Folder(os.path.join(self.path, Folder(fragment).path)) | |||
def child(self, name): | |||
def child(self, fragment): | |||
""" | |||
Returns a path of a child item represented by `name`. | |||
Returns a path of a child item represented by `fragment`. | |||
""" | |||
return os.path.join(self.path, name) | |||
return os.path.join(self.path, FS(fragment).path) | |||
def make(self): | |||
""" | |||
@@ -372,13 +461,15 @@ class Folder(FS): | |||
the tree has been deleted before and readded now. To workaround the | |||
bug, we first walk the tree and create directories that are needed. | |||
""" | |||
with self.walk() as walker: | |||
source = self | |||
with source.walker as walker: | |||
@walker.folder_visitor | |||
def visit_folder(folder): | |||
""" | |||
Create the mirror directory | |||
""" | |||
Folder(folder.get_mirror(target)).make() | |||
if folder != source: | |||
Folder(folder.get_mirror(target, source)).make() | |||
def copy_contents_to(self, destination): | |||
""" | |||
@@ -386,20 +477,24 @@ class Folder(FS): | |||
Returns a Folder object that represents the moved directory. | |||
""" | |||
logger.info("Copying contents of %s to %s" % (self, destination)) | |||
self._create_target_tree(Folder(destination)) | |||
dir_util.copy_tree(self.path, str(destination)) | |||
return Folder(destination) | |||
target = Folder(destination) | |||
target.make() | |||
self._create_target_tree(target) | |||
dir_util.copy_tree(self.path, str(target)) | |||
return target | |||
def walk(self, pattern=None): | |||
@property | |||
def walker(self, pattern=None): | |||
""" | |||
Walks this folder using `FolderWalker` | |||
Return a `FolderWalker` object | |||
""" | |||
return FolderWalker(self, pattern) | |||
def list(self, pattern=None): | |||
@property | |||
def lister(self, pattern=None): | |||
""" | |||
Lists this folder using `FolderLister` | |||
Return a `FolderLister` object | |||
""" | |||
return FolderLister(self, pattern) |
@@ -29,20 +29,20 @@ | |||
{% block favicons %} | |||
<!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | |||
<link rel="shortcut icon" href="{% media '/favicon.ico' %}"> | |||
<link rel="apple-touch-icon" href="{% media '/apple-touch-icon.png' %}"> | |||
<link rel="shortcut icon" href="{{ media_url('/favicon.ico') }}"> | |||
<link rel="apple-touch-icon" href="{{ media_url('/apple-touch-icon.png') }}"> | |||
{% endblock favicons %} | |||
{% block css %} | |||
<!-- CSS : implied media="all" --> | |||
<link rel="stylesheet" href="{% media 'css/site.css' %}"> | |||
<link rel="stylesheet" href="{{ media_url('css/site.css') }}"> | |||
<!-- Uncomment if you are specifically targeting less enabled mobile browsers | |||
<link rel="stylesheet" media="handheld" href="css/handheld.css?v=2"> --> | |||
{% endblock css %} | |||
{% block headjs %} | |||
<!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects --> | |||
<script src="{% media 'js/libs/modernizr-1.6.min.js' %}"></script> | |||
<script src="{{ media_url('js/libs/modernizr-1.6.min.js') }}"></script> | |||
{% endblock headjs %} | |||
{% block endhead %}{% endblock endhead %} | |||
</head> | |||
@@ -68,13 +68,13 @@ | |||
{% block jquery %} | |||
<!-- Grab Google CDN's jQuery. fall back to local if necessary --> | |||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.js"></script> | |||
<script>!window.jQuery && document.write(unescape('%3Cscript src="{% media 'js/libs/jquery-1.4.4.js' %}"%3E%3C/script%3E'))</script> | |||
<script>!window.jQuery && document.write(unescape('%3Cscript src="{{ media_url('js/libs/jquery-1.4.4.js') }}"%3E%3C/script%3E'))</script> | |||
{% endblock jquery %} | |||
{% block scripts %} | |||
<!-- scripts concatenated and minified via ant build script--> | |||
<script src="{% media 'js/plugins.js' %}"></script> | |||
<script src="{% media 'js/script.js' %}"></script> | |||
<script src="{{ media_url('js/plugins.js') }}"></script> | |||
<script src="{{ media_url('js/script.js') }}"></script> | |||
<!-- end concatenated and minified scripts--> | |||
{% endblock scripts %} | |||
@@ -83,7 +83,7 @@ | |||
<script> | |||
// More information on tackling transparent PNGs for IE goo.gl/mZiyb | |||
//fix any <img> or .png_bg background-images | |||
$.getScript("{% media 'js/libs/dd_belatedpng.js' %}",function(){ DD_belatedPNG.fix('img, .png_bg'); }); | |||
$.getScript("{{ media_url('js/libs/dd_belatedpng.js') }}",function(){ DD_belatedPNG.fix('img, .png_bg'); }); | |||
</script> | |||
<![endif]--> | |||
{% endblock pngfix %} | |||
@@ -0,0 +1,22 @@ | |||
<!doctype html> | |||
<title>not found</title> | |||
<style> | |||
body { text-align: center;} | |||
h1 { font-size: 50px; } | |||
body { font: 20px Constantia, 'Hoefler Text', "Adobe Caslon Pro", Baskerville, Georgia, Times, serif; color: #999; text-shadow: 2px 2px 2px rgba(200, 200, 200, 0.5); } | |||
::-moz-selection{ background:#FF5E99; color:#fff; } | |||
::selection { background:#FF5E99; color:#fff; } | |||
details { display:block; } | |||
a { color: rgb(36, 109, 56); text-decoration:none; } | |||
a:hover { color: rgb(96, 73, 141) ; text-shadow: 2px 2px 2px rgba(36, 109, 56, 0.5); } | |||
span[frown] { transform: rotate(90deg); display:inline-block; color: #bbb; } | |||
</style> | |||
<details> | |||
<summary><h1>Not found</h1></summary> | |||
<p><span frown>:(</span></p> | |||
</details> |
@@ -0,0 +1,7 @@ | |||
{% extends "base.html" %} | |||
{% block main %} | |||
Hi! | |||
I am a test template to make sure jinja2 generation works well with hyde. | |||
{% endblock %} |
@@ -0,0 +1,9 @@ | |||
{% extends "blog/post.html" %} | |||
{% block article %} | |||
{{ lipsum() }} | |||
{% endblock %} | |||
{% block aside %} | |||
{{ lipsum() }} | |||
{% endblock %} |
@@ -0,0 +1,25 @@ | |||
<?xml version="1.0"?> | |||
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd"> | |||
<cross-domain-policy> | |||
<!-- Read this: www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html --> | |||
<!-- Most restrictive policy: --> | |||
<site-control permitted-cross-domain-policies="none"/> | |||
<!-- Least restrictive policy: --> | |||
<!-- | |||
<site-control permitted-cross-domain-policies="all"/> | |||
<allow-access-from domain="*" to-ports="*" secure="false"/> | |||
<allow-http-request-headers-from domain="*" headers="*" secure="false"/> | |||
--> | |||
<!-- | |||
If you host a crossdomain.xml file with allow-access-from domain=“*” | |||
and don’t understand all of the points described here, you probably | |||
have a nasty security vulnerability. ~ simon willison | |||
--> | |||
</cross-domain-policy> |
@@ -0,0 +1,5 @@ | |||
# www.robotstxt.org/ | |||
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 | |||
User-agent: * | |||
@@ -0,0 +1,4 @@ | |||
author: Lakshmi Vyasarajan | |||
description: A test layout for hyde. | |||
template: jinja2 (2.6) | |||
version: 0.1 |
@@ -0,0 +1,57 @@ | |||
{% extends "root.html" %} | |||
{% block all %} | |||
<!doctype html> | |||
<html lang="en"> | |||
<head> | |||
{% block starthead %}{% endblock starthead %} | |||
<meta charset="{{page.meta.charset|default('utf-8')}}"> | |||
<meta http-equiv="X-UA-Compatible" content="{{page.meta.compatibility|default('IE=edge,chrome=1')}}"> | |||
<title>{% block title %}{{page.meta.title}}{% endblock %}</title> | |||
<meta name="description" content="{{page.meta.description}}"> | |||
<meta name="author" content="{{page.meta.author}}"> | |||
<!-- Mobile viewport optimized: j.mp/bplateviewport --> | |||
<meta name="viewport" content="{{page.meta.viewport|default('width=device-width, initial-scale=1.0')}}"> | |||
{% block favicons %} | |||
<!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | |||
<link rel="shortcut icon" href="{{ media_url('/favicon.ico') }}"> | |||
<link rel="apple-touch-icon" href="{{ media_url('/apple-touch-icon.png') }}"> | |||
{% endblock favicons %} | |||
{% block css %} | |||
<link rel="stylesheet" href="{{ media_url('css/site.css') }}"> | |||
{% endblock css %} | |||
{% block endhead %}{% endblock endhead %} | |||
</head> | |||
<body id="{{page.id if page.id else page.name_without_extension}}"> | |||
{% block content %} | |||
<div id="container"> | |||
{% block container %} | |||
<header> | |||
{% block header %}{% endblock header %} | |||
</header> | |||
<div id="main" role="main"> | |||
{% block main %}{% endblock main %} | |||
</div> | |||
<footer> | |||
{% block footer %}{% endblock %} | |||
</footer> | |||
{% endblock container%} | |||
</div> <!--! end of #container --> | |||
{% endblock content%} | |||
{% block js %} | |||
<!-- Javascript at the bottom for fast page loading --> | |||
{% block jquery %} | |||
<!-- Grab Google CDN's jQuery. fall back to local if necessary --> | |||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.js"></script> | |||
{% endblock jquery %} | |||
{% block scripts %}{% endblock scripts %} | |||
{%endblock js %} | |||
</body> | |||
</html> | |||
{% endblock all %} |
@@ -0,0 +1,10 @@ | |||
{% extends "base.html" %} | |||
{% block main %} | |||
<article> | |||
{% block article %}{% endblock %} | |||
</article> | |||
<aside> | |||
{% block aside %}{% endblock %} | |||
</aside> | |||
{% endblock %} |
@@ -0,0 +1 @@ | |||
{% block all %}{% endblock all %} |
@@ -0,0 +1,4 @@ | |||
body{ | |||
margin: 0 auto; | |||
width: 960px; | |||
} |
@@ -0,0 +1,7 @@ | |||
mode: development | |||
media_root:: media # Relative path from site root (the directory where this file exists) | |||
media_url: /media | |||
template: hyde.ext.jinja2 | |||
widgets: | |||
plugins: | |||
aggregators: |
@@ -35,6 +35,7 @@ class Config(Expando): | |||
def __init__(self, site_path, config_dict=None): | |||
default_config = dict( | |||
content_root = 'content', | |||
deploy_root = 'deploy', | |||
media_root = 'media', | |||
layout_root = 'layout', | |||
media_url = '/media', | |||
@@ -46,6 +47,14 @@ class Config(Expando): | |||
super(Config, self).__init__(conf) | |||
self.site_path = Folder(site_path) | |||
@property | |||
def deploy_root_path(self): | |||
""" | |||
Derives the deploy root path from the site path | |||
""" | |||
return self.site_path.child_folder(self.deploy_root) | |||
@property | |||
def content_root_path(self): | |||
""" | |||
@@ -3,32 +3,30 @@ | |||
Parses & holds information about the site to be generated. | |||
""" | |||
from hyde.exceptions import HydeException | |||
from hyde.fs import File, Folder | |||
from hyde.model import Config, Expando | |||
from hyde.fs import FS, File, Folder | |||
from hyde.model import Config | |||
import logging | |||
import os | |||
from logging import NullHandler | |||
logger = logging.getLogger('hyde.site') | |||
logger.addHandler(NullHandler()) | |||
class Resource(object): | |||
class Processable(object): | |||
""" | |||
Represents any file that is processed by hyde | |||
A node or resource. | |||
""" | |||
def __init__(self, source_file, node): | |||
super(Resource, self).__init__() | |||
self.source_file = source_file | |||
if not node: | |||
raise HydeException("Resource cannot exist without a node") | |||
if not source_file: | |||
raise HydeException("Source file is required to instantiate a resource") | |||
self.node = node | |||
def __init__(self, source): | |||
super(Processable, self).__init__() | |||
self.source = FS.file_or_folder(source) | |||
@property | |||
def name(self): | |||
""" | |||
The resource name | |||
""" | |||
return self.source.name | |||
def __repr__(self): | |||
return self.path | |||
@@ -38,7 +36,22 @@ class Resource(object): | |||
""" | |||
Gets the source path of this node. | |||
""" | |||
return self.source_file.path | |||
return self.source.path | |||
class Resource(Processable): | |||
""" | |||
Represents any file that is processed by hyde | |||
""" | |||
def __init__(self, source_file, node): | |||
super(Resource, self).__init__(source_file) | |||
self.source_file = source_file | |||
if not node: | |||
raise HydeException("Resource cannot exist without a node") | |||
if not source_file: | |||
raise HydeException("Source file is required" | |||
" to instantiate a resource") | |||
self.node = node | |||
@property | |||
def relative_path(self): | |||
@@ -47,15 +60,17 @@ class Resource(object): | |||
""" | |||
return self.source_file.get_relative_path(self.node.root.source_folder) | |||
class Node(object): | |||
class Node(Processable): | |||
""" | |||
Represents any folder that is processed by hyde | |||
""" | |||
def __init__(self, source_folder, parent=None): | |||
super(Node, self).__init__() | |||
super(Node, self).__init__(source_folder) | |||
if not source_folder: | |||
raise HydeException("Source folder is required to instantiate a node.") | |||
raise HydeException("Source folder is required" | |||
" to instantiate a node.") | |||
self.root = self | |||
self.module = None | |||
self.site = None | |||
@@ -68,16 +83,14 @@ class Node(object): | |||
self.child_nodes = [] | |||
self.resources = [] | |||
def __repr__(self): | |||
return self.path | |||
def add_child_node(self, folder): | |||
""" | |||
Creates a new child node and adds it to the list of child nodes. | |||
""" | |||
if folder.parent != self.source_folder: | |||
raise HydeException("The given folder [%s] is not a direct descendant of [%s]" % | |||
raise HydeException("The given folder [%s] is not a" | |||
" direct descendant of [%s]" % | |||
(folder, self.source_folder)) | |||
node = Node(folder, self) | |||
self.child_nodes.append(node) | |||
@@ -89,18 +102,26 @@ class Node(object): | |||
""" | |||
if afile.parent != self.source_folder: | |||
raise HydeException("The given file [%s] is not a direct descendant of [%s]" % | |||
raise HydeException("The given file [%s] is not" | |||
" a direct descendant of [%s]" % | |||
(afile, self.source_folder)) | |||
resource = Resource(afile, self) | |||
self.resources.append(resource) | |||
return resource | |||
@property | |||
def path(self): | |||
def walk(self): | |||
yield self | |||
for child in self.child_nodes: | |||
for node in child.walk(): | |||
yield node | |||
def walk_resources(self): | |||
""" | |||
Gets the source path of this node. | |||
Walks the resources in this hierarchy. | |||
""" | |||
return self.source_folder.path | |||
for node in self.walk(): | |||
for resource in node.resources: | |||
yield resource | |||
@property | |||
def relative_path(self): | |||
@@ -109,20 +130,22 @@ class Node(object): | |||
""" | |||
return self.source_folder.get_relative_path(self.root.source_folder) | |||
class RootNode(Node): | |||
""" | |||
Represents one of the roots of site: Content, Media or Layout | |||
""" | |||
def __init__(self, source_folder, site): | |||
super(RootNode, self).__init__(source_folder) | |||
self.site = site | |||
self.node_map = {} | |||
self.resource_map = {} | |||
super(RootNode, self).__init__(source_folder) | |||
self.site = site | |||
self.node_map = {} | |||
self.resource_map = {} | |||
def node_from_path(self, path): | |||
""" | |||
Gets the node that maps to the given path. If no match is found it returns None. | |||
Gets the node that maps to the given path. | |||
If no match is found it returns None. | |||
""" | |||
if Folder(path) == self.source_folder: | |||
return self | |||
@@ -130,26 +153,32 @@ class RootNode(Node): | |||
def node_from_relative_path(self, relative_path): | |||
""" | |||
Gets the content node that maps to the given relative path. If no match is found it returns None. | |||
Gets the content node that maps to the given relative path. | |||
If no match is found it returns None. | |||
""" | |||
return self.node_from_path(self.source_folder.child(str(relative_path))) | |||
return self.node_from_path( | |||
self.source_folder.child(str(relative_path))) | |||
def resource_from_path(self, path): | |||
""" | |||
Gets the resource that maps to the given path. If no match is found it returns None. | |||
Gets the resource that maps to the given path. | |||
If no match is found it returns None. | |||
""" | |||
return self.resource_map.get(str(File(path)), None) | |||
def resource_from_relative_path(self, relative_path): | |||
""" | |||
Gets the content resource that maps to the given relative path. If no match is found it returns None. | |||
Gets the content resource that maps to the given relative path. | |||
If no match is found it returns None. | |||
""" | |||
return self.resource_from_path(self.source_folder.child(str(relative_path))) | |||
return self.resource_from_path( | |||
self.source_folder.child(str(relative_path))) | |||
def add_node(self, a_folder): | |||
""" | |||
Adds a new node to this folder's hierarchy. Also adds to to the hashtable of path to | |||
node associations for quick lookup. | |||
Adds a new node to this folder's hierarchy. | |||
Also adds to to the hashtable of path to node associations | |||
for quick lookup. | |||
""" | |||
folder = Folder(a_folder) | |||
node = self.node_from_path(folder) | |||
@@ -158,7 +187,8 @@ class RootNode(Node): | |||
return node | |||
if not folder.is_descendant_of(self.source_folder): | |||
raise HydeException("The given folder [%s] does not belong to this hierarchy [%s]" % | |||
raise HydeException("The given folder [%s] does not" | |||
" belong to this hierarchy [%s]" % | |||
(folder, self.source_folder)) | |||
p_folder = folder | |||
@@ -174,14 +204,15 @@ class RootNode(Node): | |||
for h_folder in hierarchy: | |||
node = node.add_child_node(h_folder) | |||
self.node_map[str(h_folder)] = node | |||
logger.info("Added node [%s] to [%s]" % (node.relative_path, self.source_folder)) | |||
logger.info("Added node [%s] to [%s]" % ( | |||
node.relative_path, self.source_folder)) | |||
return node | |||
def add_resource(self, a_file): | |||
""" | |||
Adds a file to the parent node. Also adds to to the hashtable of path to | |||
resource associations for quick lookup. | |||
Adds a file to the parent node. Also adds to to the | |||
hashtable of path to resource associations for quick lookup. | |||
""" | |||
afile = File(a_file) | |||
@@ -191,7 +222,8 @@ class RootNode(Node): | |||
logger.info("Resource exists at [%s]" % resource.relative_path) | |||
if not afile.is_descendant_of(self.source_folder): | |||
raise HydeException("The given file [%s] does not reside in this hierarchy [%s]" % | |||
raise HydeException("The given file [%s] does not reside" | |||
" in this hierarchy [%s]" % | |||
(afile, self.content_folder)) | |||
node = self.node_from_path(afile.parent) | |||
@@ -201,19 +233,22 @@ class RootNode(Node): | |||
resource = node.add_child_resource(afile) | |||
self.resource_map[str(afile)] = resource | |||
logger.info("Added resource [%s] to [%s]" % (resource.relative_path, self.source_folder)) | |||
logger.info("Added resource [%s] to [%s]" % | |||
(resource.relative_path, self.source_folder)) | |||
return resource | |||
def build(self): | |||
""" | |||
Walks the `source_folder` and builds the sitemap. Creates nodes and resources, | |||
reads metadata and injects attributes. This is the model for hyde. | |||
Walks the `source_folder` and builds the sitemap. | |||
Creates nodes and resources, reads metadata and injects attributes. | |||
This is the model for hyde. | |||
""" | |||
if not self.source_folder.exists: | |||
raise HydeException("The given source folder[%s] does not exist" % self.source_folder) | |||
raise HydeException("The given source folder[%s]" | |||
" does not exist" % self.source_folder) | |||
with self.source_folder.walk() as walker: | |||
with self.source_folder.walker as walker: | |||
@walker.folder_visitor | |||
def visit_folder(folder): | |||
@@ -223,6 +258,7 @@ class RootNode(Node): | |||
def visit_file(afile): | |||
self.add_resource(afile) | |||
class Site(object): | |||
""" | |||
Represents the site to be generated. | |||
@@ -232,9 +268,7 @@ class Site(object): | |||
super(Site, self).__init__() | |||
self.site_path = Folder(str(site_path)) | |||
self.config = config if config else Config(self.site_path) | |||
self.content = RootNode(self.config.content_root_path, self ) | |||
self.node_map = {} | |||
self.resource_map = {} | |||
self.content = RootNode(self.config.content_root_path, self) | |||
def build(self): | |||
""" | |||
@@ -19,9 +19,9 @@ class Template(object): | |||
""" | |||
abstract | |||
def render(self, template_name, context): | |||
def render(self, resource, context): | |||
""" | |||
Given the name of a template (partial path usually), and the context, this function | |||
Given the resource, and the context, this function | |||
must return the rendered string. | |||
""" | |||
abstract |
@@ -0,0 +1,22 @@ | |||
<!doctype html> | |||
<title>not found</title> | |||
<style> | |||
body { text-align: center;} | |||
h1 { font-size: 50px; } | |||
body { font: 20px Constantia, 'Hoefler Text', "Adobe Caslon Pro", Baskerville, Georgia, Times, serif; color: #999; text-shadow: 2px 2px 2px rgba(200, 200, 200, 0.5); } | |||
::-moz-selection{ background:#FF5E99; color:#fff; } | |||
::selection { background:#FF5E99; color:#fff; } | |||
details { display:block; } | |||
a { color: rgb(36, 109, 56); text-decoration:none; } | |||
a:hover { color: rgb(96, 73, 141) ; text-shadow: 2px 2px 2px rgba(36, 109, 56, 0.5); } | |||
span[frown] { transform: rotate(90deg); display:inline-block; color: #bbb; } | |||
</style> | |||
<details> | |||
<summary><h1>Not found</h1></summary> | |||
<p><span frown>:(</span></p> | |||
</details> |
@@ -1,9 +1,9 @@ | |||
{% extends "blog/post.html" %} | |||
{% block article %} | |||
{% lipsum n=10 %} | |||
{{ lipsum() }} | |||
{% endblock %} | |||
{% block aside %} | |||
{% lipsum n=2 %} | |||
{{ lipsum() }} | |||
{% endblock %} |
@@ -0,0 +1,25 @@ | |||
<?xml version="1.0"?> | |||
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd"> | |||
<cross-domain-policy> | |||
<!-- Read this: www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html --> | |||
<!-- Most restrictive policy: --> | |||
<site-control permitted-cross-domain-policies="none"/> | |||
<!-- Least restrictive policy: --> | |||
<!-- | |||
<site-control permitted-cross-domain-policies="all"/> | |||
<allow-access-from domain="*" to-ports="*" secure="false"/> | |||
<allow-http-request-headers-from domain="*" headers="*" secure="false"/> | |||
--> | |||
<!-- | |||
If you host a crossdomain.xml file with allow-access-from domain=“*” | |||
and don’t understand all of the points described here, you probably | |||
have a nasty security vulnerability. ~ simon willison | |||
--> | |||
</cross-domain-policy> |
@@ -0,0 +1,5 @@ | |||
# www.robotstxt.org/ | |||
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 | |||
User-agent: * | |||
@@ -16,12 +16,12 @@ | |||
{% block favicons %} | |||
<!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | |||
<link rel="shortcut icon" href="{% media '/favicon.ico' %}"> | |||
<link rel="apple-touch-icon" href="{% media '/apple-touch-icon.png' %}"> | |||
<link rel="shortcut icon" href="{{ media_url('/favicon.ico') }}"> | |||
<link rel="apple-touch-icon" href="{{ media_url('/apple-touch-icon.png') }}"> | |||
{% endblock favicons %} | |||
{% block css %} | |||
<link rel="stylesheet" href="{% media 'css/site.css' %}"> | |||
<link rel="stylesheet" href="{{ media_url('css/site.css') }}"> | |||
{% endblock css %} | |||
{% block endhead %}{% endblock endhead %} | |||
</head> | |||
@@ -1,6 +1,7 @@ | |||
mode: development | |||
media_root:: media # Relative path from site root (the directory where this file exists) | |||
media_url: /media | |||
template: hyde.ext.jinja2 | |||
widgets: | |||
plugins: | |||
aggregators: |
@@ -44,6 +44,10 @@ def test_kind(): | |||
f = File(__file__) | |||
assert f.kind == os.path.splitext(__file__)[1].lstrip('.') | |||
def test_path_expands_user(): | |||
f = File("~/abc/def") | |||
assert f.path == os.path.expanduser("~/abc/def") | |||
def test_parent(): | |||
f = File(__file__) | |||
p = f.parent | |||
@@ -104,6 +108,7 @@ JINJA2 = TEMPLATE_ROOT.child_folder('jinja2') | |||
HELPERS = File(JINJA2.child('helpers.html')) | |||
INDEX = File(JINJA2.child('index.html')) | |||
LAYOUT = File(JINJA2.child('layout.html')) | |||
LOGO = File(TEMPLATE_ROOT.child('../../../resources/hyde-logo.png')) | |||
def test_ancestors(): | |||
depth = 0 | |||
@@ -132,10 +137,11 @@ def test_is_descendant_of(): | |||
print "*" | |||
assert not INDEX.is_descendant_of(DATA_ROOT) | |||
def test_fragment(): | |||
def test_get_relative_path(): | |||
assert INDEX.get_relative_path(TEMPLATE_ROOT) == Folder(JINJA2.name).child(INDEX.name) | |||
assert INDEX.get_relative_path(TEMPLATE_ROOT.parent) == Folder( | |||
TEMPLATE_ROOT.name).child_folder(JINJA2.name).child(INDEX.name) | |||
assert JINJA2.get_relative_path(JINJA2) == "" | |||
def test_get_mirror(): | |||
mirror = JINJA2.get_mirror(DATA_ROOT, source_root=TEMPLATE_ROOT) | |||
@@ -143,6 +149,18 @@ def test_get_mirror(): | |||
mirror = JINJA2.get_mirror(DATA_ROOT, source_root=TEMPLATE_ROOT.parent) | |||
assert mirror == DATA_ROOT.child_folder(TEMPLATE_ROOT.name).child_folder(JINJA2.name) | |||
def test_mimetype(): | |||
assert HELPERS.mimetype == 'text/html' | |||
assert LOGO.mimetype == 'image/png' | |||
def test_is_text(): | |||
assert HELPERS.is_text | |||
assert not LOGO.is_text | |||
def test_is_image(): | |||
assert not HELPERS.is_image | |||
assert LOGO.is_image | |||
@nottest | |||
def setup_data(): | |||
DATA_ROOT.make() | |||
@@ -242,7 +260,7 @@ def test_walker(): | |||
files = [] | |||
complete = [] | |||
with TEMPLATE_ROOT.walk() as walker: | |||
with TEMPLATE_ROOT.walker as walker: | |||
@walker.folder_visitor | |||
def visit_folder(f): | |||
@@ -265,12 +283,34 @@ def test_walker(): | |||
assert len(folders) == 2 | |||
assert len(complete) == 1 | |||
def test_walker_walk_all(): | |||
items = list(TEMPLATE_ROOT.walker.walk_all()) | |||
assert len(items) == 6 | |||
assert TEMPLATE_ROOT in items | |||
assert JINJA2 in items | |||
assert INDEX in items | |||
assert HELPERS in items | |||
assert LAYOUT in items | |||
def test_walker_walk_files(): | |||
items = list(TEMPLATE_ROOT.walker.walk_files()) | |||
assert len(items) == 4 | |||
assert INDEX in items | |||
assert HELPERS in items | |||
assert LAYOUT in items | |||
def test_walker_walk_folders(): | |||
items = list(TEMPLATE_ROOT.walker.walk_folders()) | |||
assert len(items) == 2 | |||
assert TEMPLATE_ROOT in items | |||
assert JINJA2 in items | |||
def test_walker_templates_just_root(): | |||
folders = [] | |||
files = [] | |||
complete = [] | |||
with TEMPLATE_ROOT.walk() as walker: | |||
with TEMPLATE_ROOT.walker as walker: | |||
@walker.folder_visitor | |||
def visit_folder(f): | |||
@@ -295,7 +335,7 @@ def test_lister_templates(): | |||
files = [] | |||
complete = [] | |||
with TEMPLATE_ROOT.list() as lister: | |||
with TEMPLATE_ROOT.lister as lister: | |||
@lister.folder_visitor | |||
def visit_folder(f): | |||
@@ -315,12 +355,39 @@ def test_lister_templates(): | |||
assert len(complete) == 1 | |||
def test_lister_list_all(): | |||
items = list(TEMPLATE_ROOT.lister.list_all()) | |||
assert len(items) == 1 | |||
assert JINJA2 in items | |||
items = list(JINJA2.lister.list_all()) | |||
assert len(items) == 4 | |||
assert INDEX in items | |||
assert HELPERS in items | |||
assert LAYOUT in items | |||
def test_lister_list_files(): | |||
items = list(TEMPLATE_ROOT.lister.list_files()) | |||
assert len(items) == 0 | |||
items = list(JINJA2.lister.list_files()) | |||
assert len(items) == 4 | |||
assert INDEX in items | |||
assert HELPERS in items | |||
assert LAYOUT in items | |||
def test_lister_list_folders(): | |||
items = list(TEMPLATE_ROOT.lister.list_folders()) | |||
assert len(items) == 1 | |||
assert JINJA2 in items | |||
items = list(JINJA2.lister.list_folders()) | |||
assert len(items) == 0 | |||
def test_lister_jinja2(): | |||
folders = [] | |||
files = [] | |||
complete = [] | |||
with JINJA2.list() as lister: | |||
with JINJA2.lister as lister: | |||
@lister.folder_visitor | |||
def visit_folder(f): | |||
@@ -9,10 +9,11 @@ Use nose | |||
from hyde.engine import Engine | |||
from hyde.exceptions import HydeException | |||
from hyde.fs import FS, File, Folder | |||
from hyde.layout import Layout | |||
from nose.tools import raises, with_setup, nottest | |||
TEST_SITE = File(__file__).parent.child_folder('_test') | |||
TEST_SITE_AT_USER = Folder('~/_test') | |||
@nottest | |||
def create_test_site(): | |||
@@ -22,6 +23,14 @@ def create_test_site(): | |||
def delete_test_site(): | |||
TEST_SITE.delete() | |||
@nottest | |||
def create_test_site_at_user(): | |||
TEST_SITE_AT_USER.make() | |||
@nottest | |||
def delete_test_site_at_user(): | |||
TEST_SITE_AT_USER.delete() | |||
@raises(HydeException) | |||
@with_setup(create_test_site, delete_test_site) | |||
def test_ensure_exception_when_sitepath_exists(): | |||
@@ -39,9 +48,29 @@ def test_ensure_no_exception_when_sitepath_does_not_exist(): | |||
e = Engine() | |||
TEST_SITE.delete() | |||
e.run(e.parse(['-s', str(TEST_SITE), 'init', '-f'])) | |||
assert TEST_SITE.exists | |||
assert TEST_SITE.child_folder('layout').exists | |||
assert File(TEST_SITE.child('info.yaml')).exists | |||
verify_site_contents(TEST_SITE, Layout.find_layout()) | |||
@with_setup(create_test_site_at_user, delete_test_site_at_user) | |||
def test_ensure_can_create_site_at_user(): | |||
e = Engine() | |||
TEST_SITE_AT_USER.delete() | |||
e.run(e.parse(['-s', str(TEST_SITE_AT_USER), 'init', '-f'])) | |||
verify_site_contents(TEST_SITE_AT_USER, Layout.find_layout()) | |||
@nottest | |||
def verify_site_contents(site, layout): | |||
assert site.exists | |||
assert site.child_folder('layout').exists | |||
assert File(site.child('info.yaml')).exists | |||
expected = map(lambda f: f.get_relative_path(layout), layout.walker.walk_all()) | |||
actual = map(lambda f: f.get_relative_path(site), site.walker.walk_all()) | |||
assert actual | |||
assert expected | |||
expected.sort() | |||
actual.sort() | |||
assert actual == expected | |||
@raises(HydeException) | |||
@with_setup(create_test_site, delete_test_site) | |||
@@ -44,6 +44,7 @@ class TestConfig(object): | |||
cls.conf2 = """ | |||
mode: development | |||
deploy_root: ~/deploy_site | |||
content_root: site/stuff # Relative path from site root | |||
media_root: mmm # Relative path from site root | |||
media_url: /media | |||
@@ -62,6 +63,9 @@ class TestConfig(object): | |||
assert hasattr(c, path) | |||
assert getattr(c, path) == TEST_SITE_ROOT.child_folder(root) | |||
assert c.deploy_root_path == TEST_SITE_ROOT.child_folder('deploy') | |||
def test_conf1(self): | |||
c = Config(site_path=TEST_SITE_ROOT, config_dict=yaml.load(self.conf1)) | |||
assert c.content_root_path == TEST_SITE_ROOT.child_folder('stuff') | |||
@@ -71,3 +75,5 @@ class TestConfig(object): | |||
assert c.content_root_path == TEST_SITE_ROOT.child_folder('site/stuff') | |||
assert c.media_root_path == TEST_SITE_ROOT.child_folder('mmm') | |||
assert c.media_url == TEST_SITE_ROOT.child_folder('/media') | |||
print c.deploy_root_path | |||
assert c.deploy_root_path == Folder('~/deploy_site') |
@@ -65,6 +65,23 @@ def test_build(): | |||
assert resource.relative_path == path | |||
assert not s.content.resource_from_relative_path('/happy-festivus.html') | |||
def test_walk_resources(): | |||
s = Site(TEST_SITE_ROOT) | |||
s.build() | |||
pages = [page.name for page in s.content.walk_resources()] | |||
expected = ["404.html", | |||
"about.html", | |||
"apple-touch-icon.png", | |||
"merry-christmas.html", | |||
"crossdomain.xml", | |||
"favicon.ico", | |||
"robots.txt" | |||
] | |||
pages.sort() | |||
expected.sort() | |||
assert pages == expected | |||
class TestSiteWithConfig(object): | |||
@classmethod | |||