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