Browse Source

Generation first pass complete

main
Lakshmi Vyasarajan 14 years ago
parent
commit
9cdd3b7a1b
36 changed files with 658 additions and 128 deletions
  1. +0
    -1
      hyde/aggregator.py
  2. +49
    -8
      hyde/engine.py
  3. +36
    -7
      hyde/ext/templates/jinja.py
  4. +129
    -34
      hyde/fs.py
  5. +8
    -8
      hyde/layouts/basic/layout/base.html
  6. +22
    -0
      hyde/layouts/test/content/404.html
  7. +7
    -0
      hyde/layouts/test/content/about.html
  8. BIN
      hyde/layouts/test/content/apple-touch-icon.png
  9. +9
    -0
      hyde/layouts/test/content/blog/2010/december/merry-christmas.html
  10. +25
    -0
      hyde/layouts/test/content/crossdomain.xml
  11. BIN
      hyde/layouts/test/content/favicon.ico
  12. +5
    -0
      hyde/layouts/test/content/robots.txt
  13. +4
    -0
      hyde/layouts/test/info.yaml
  14. +57
    -0
      hyde/layouts/test/layout/base.html
  15. +10
    -0
      hyde/layouts/test/layout/blog/post.html
  16. +1
    -0
      hyde/layouts/test/layout/root.html
  17. +4
    -0
      hyde/layouts/test/media/css/site.css
  18. +7
    -0
      hyde/layouts/test/site.yaml
  19. +9
    -0
      hyde/model.py
  20. +88
    -54
      hyde/site.py
  21. +2
    -2
      hyde/template.py
  22. +22
    -0
      hyde/tests/sites/test_jinja/content/404.html
  23. BIN
      hyde/tests/sites/test_jinja/content/apple-touch-icon.png
  24. +2
    -2
      hyde/tests/sites/test_jinja/content/blog/2010/december/merry-christmas.html
  25. +25
    -0
      hyde/tests/sites/test_jinja/content/crossdomain.xml
  26. BIN
      hyde/tests/sites/test_jinja/content/favicon.ico
  27. +5
    -0
      hyde/tests/sites/test_jinja/content/robots.txt
  28. +3
    -3
      hyde/tests/sites/test_jinja/layout/base.html
  29. +1
    -0
      hyde/tests/sites/test_jinja/site.yaml
  30. +72
    -5
      hyde/tests/test_fs.py
  31. +33
    -4
      hyde/tests/test_initialize.py
  32. +6
    -0
      hyde/tests/test_model.py
  33. +17
    -0
      hyde/tests/test_site.py
  34. BIN
      resources/hyde-logo.png
  35. BIN
      resources/hyde-lt-b.png
  36. BIN
      resources/hyde-lt.png

+ 0
- 1
hyde/aggregator.py View File

@@ -1 +0,0 @@
# -*- coding: utf-8 -*-

+ 49
- 8
hyde/engine.py View File

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

+ 36
- 7
hyde/ext/templates/jinja.py View File

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

+ 129
- 34
hyde/fs.py View File

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

+ 8
- 8
hyde/layouts/basic/layout/base.html View File

@@ -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 %}


+ 22
- 0
hyde/layouts/test/content/404.html View File

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

+ 7
- 0
hyde/layouts/test/content/about.html View File

@@ -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 %}

BIN
hyde/layouts/test/content/apple-touch-icon.png View File

Before After
Width: 57  |  Height: 57  |  Size: 1.8 KiB

+ 9
- 0
hyde/layouts/test/content/blog/2010/december/merry-christmas.html View File

@@ -0,0 +1,9 @@
{% extends "blog/post.html" %}

{% block article %}
{{ lipsum() }}
{% endblock %}

{% block aside %}
{{ lipsum() }}
{% endblock %}

+ 25
- 0
hyde/layouts/test/content/crossdomain.xml View File

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

BIN
hyde/layouts/test/content/favicon.ico View File

Before After

+ 5
- 0
hyde/layouts/test/content/robots.txt View File

@@ -0,0 +1,5 @@
# www.robotstxt.org/
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449

User-agent: *


+ 4
- 0
hyde/layouts/test/info.yaml View File

@@ -0,0 +1,4 @@
author: Lakshmi Vyasarajan
description: A test layout for hyde.
template: jinja2 (2.6)
version: 0.1

+ 57
- 0
hyde/layouts/test/layout/base.html View File

@@ -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 %}

+ 10
- 0
hyde/layouts/test/layout/blog/post.html View File

@@ -0,0 +1,10 @@
{% extends "base.html" %}

{% block main %}
<article>
{% block article %}{% endblock %}
</article>
<aside>
{% block aside %}{% endblock %}
</aside>
{% endblock %}

+ 1
- 0
hyde/layouts/test/layout/root.html View File

@@ -0,0 +1 @@
{% block all %}{% endblock all %}

+ 4
- 0
hyde/layouts/test/media/css/site.css View File

@@ -0,0 +1,4 @@
body{
margin: 0 auto;
width: 960px;
}

+ 7
- 0
hyde/layouts/test/site.yaml View File

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

+ 9
- 0
hyde/model.py View File

@@ -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):
"""


+ 88
- 54
hyde/site.py View File

@@ -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):
"""


+ 2
- 2
hyde/template.py View File

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

+ 22
- 0
hyde/tests/sites/test_jinja/content/404.html View File

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

BIN
hyde/tests/sites/test_jinja/content/apple-touch-icon.png View File

Before After
Width: 57  |  Height: 57  |  Size: 1.8 KiB

+ 2
- 2
hyde/tests/sites/test_jinja/content/blog/2010/december/merry-christmas.html View File

@@ -1,9 +1,9 @@
{% extends "blog/post.html" %}

{% block article %}
{% lipsum n=10 %}
{{ lipsum() }}
{% endblock %}

{% block aside %}
{% lipsum n=2 %}
{{ lipsum() }}
{% endblock %}

+ 25
- 0
hyde/tests/sites/test_jinja/content/crossdomain.xml View File

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

BIN
hyde/tests/sites/test_jinja/content/favicon.ico View File

Before After

+ 5
- 0
hyde/tests/sites/test_jinja/content/robots.txt View File

@@ -0,0 +1,5 @@
# www.robotstxt.org/
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449

User-agent: *


+ 3
- 3
hyde/tests/sites/test_jinja/layout/base.html View File

@@ -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
- 0
hyde/tests/sites/test_jinja/site.yaml View File

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

+ 72
- 5
hyde/tests/test_fs.py View File

@@ -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):


+ 33
- 4
hyde/tests/test_initialize.py View File

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


+ 6
- 0
hyde/tests/test_model.py View File

@@ -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')

+ 17
- 0
hyde/tests/test_site.py View File

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


BIN
resources/hyde-logo.png View File

Before After
Width: 125  |  Height: 96  |  Size: 1.9 KiB

BIN
resources/hyde-lt-b.png View File

Before After
Width: 538  |  Height: 132  |  Size: 7.9 KiB

BIN
resources/hyde-lt.png View File

Before After
Width: 122  |  Height: 56  |  Size: 2.4 KiB

Loading…
Cancel
Save