| @@ -1 +0,0 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| @@ -6,12 +6,22 @@ from commando import * | |||||
| from hyde.exceptions import HydeException | from hyde.exceptions import HydeException | ||||
| from hyde.fs import File, Folder | from hyde.fs import File, Folder | ||||
| from hyde.layout import Layout, HYDE_DATA | from hyde.layout import Layout, HYDE_DATA | ||||
| from hyde.model import Config | |||||
| from hyde.site import Site | |||||
| from hyde.version import __version__ | from hyde.version import __version__ | ||||
| import logging | |||||
| import os | import os | ||||
| import yaml | |||||
| HYDE_LAYOUTS = "HYDE_LAYOUTS" | HYDE_LAYOUTS = "HYDE_LAYOUTS" | ||||
| logger = logging.getLogger('hyde.engine') | |||||
| logger.setLevel(logging.DEBUG) | |||||
| import sys | |||||
| logger.addHandler(logging.StreamHandler(sys.stdout)) | |||||
| class Engine(Application): | class Engine(Application): | ||||
| """ | """ | ||||
| The Hyde Application | The Hyde Application | ||||
| @@ -29,36 +39,67 @@ class Engine(Application): | |||||
| """ | """ | ||||
| pass | 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') | @store('-l', '--layout', default='basic', help='Layout for the new site') | ||||
| @true('-f', '--force', default=False, dest='overwrite', | @true('-f', '--force', default=False, dest='overwrite', | ||||
| help='Overwrite the current site if it exists') | 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. | ||||
| """ | """ | ||||
| sitepath = File(args.sitepath) | |||||
| sitepath = Folder(args.sitepath) | |||||
| if sitepath.exists and not args.overwrite: | if sitepath.exists and not args.overwrite: | ||||
| raise HydeException("The given site path[%s] is not empty" % sitepath) | raise HydeException("The given site path[%s] is not empty" % sitepath) | ||||
| layout = Layout.find_layout(args.layout) | layout = Layout.find_layout(args.layout) | ||||
| logger.info("Creating site at [%s] with layout [%s]" % (sitepath, layout)) | |||||
| if not layout or not layout.exists: | if not layout or not layout.exists: | ||||
| raise HydeException( | raise HydeException( | ||||
| "The given layout is invalid. Please check if you have the `layout` " | "The given layout is invalid. Please check if you have the `layout` " | ||||
| "in the right place and the environment variable(%s) has been setup " | "in the right place and the environment variable(%s) has been setup " | ||||
| "properly if you are using custom path for layouts" % HYDE_DATA) | "properly if you are using custom path for layouts" % HYDE_DATA) | ||||
| layout.copy_contents_to(args.sitepath) | layout.copy_contents_to(args.sitepath) | ||||
| logger.info("Site creation complete") | |||||
| @subcommand('gen', help='Generate the site') | @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?') | @store('-d', '--deploy-path', default='deploy', help='Where should the site be generated?') | ||||
| def gen(self, args): | def gen(self, args): | ||||
| """ | """ | ||||
| The generate command. Generates the site at the given deployment directory. | 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 | # 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 | # Configure the environment | ||||
| logger.info("Configuring Template environment") | |||||
| template.configure(site.config) | |||||
| # Prepare site info | # 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 | Jinja template utilties | ||||
| """ | """ | ||||
| from hyde.fs import File, Folder | |||||
| from hyde.template import Template | 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 | # pylint: disable-msg=W0104,E0602,W0613,R0201 | ||||
| class Jinja2Template(Template): | class Jinja2Template(Template): | ||||
| """ | """ | ||||
| The Jinja2 Template implementation | The Jinja2 Template implementation | ||||
| """ | """ | ||||
| def __init__(self, sitepath): | def __init__(self, sitepath): | ||||
| super(Jinja2Template, self).__init__(sitepath) | super(Jinja2Template, self).__init__(sitepath) | ||||
| self.env = Environment(loader=FileSystemLoader(sitepath)) | |||||
| def configure(self, config): | def configure(self, config): | ||||
| """ | """ | ||||
| Uses the config object to initialize the jinja environment. | 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) | return template.render(context) | ||||
| @@ -7,14 +7,13 @@ for common operations to provide a single interface. | |||||
| """ | """ | ||||
| import codecs | import codecs | ||||
| import contextlib | |||||
| import logging | import logging | ||||
| from logging import NullHandler | from logging import NullHandler | ||||
| import mimetypes | |||||
| import os | import os | ||||
| import shutil | import shutil | ||||
| from distutils import dir_util | from distutils import dir_util | ||||
| import functools | import functools | ||||
| import itertools | |||||
| # pylint: disable-msg=E0611 | # pylint: disable-msg=E0611 | ||||
| logger = logging.getLogger('fs') | logger = logging.getLogger('fs') | ||||
| @@ -29,7 +28,7 @@ class FS(object): | |||||
| """ | """ | ||||
| def __init__(self, path): | def __init__(self, path): | ||||
| super(FS, self).__init__() | 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): | def __str__(self): | ||||
| return self.path | return self.path | ||||
| @@ -85,18 +84,23 @@ class FS(object): | |||||
| f = f.parent | f = f.parent | ||||
| def is_descendant_of(self, ancestor): | 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): | def get_relative_path(self, root): | ||||
| """ | """ | ||||
| Gets the fragment of the current path starting at 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) | 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): | 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 | Returns a File or Folder object that would represent this entity | ||||
| if it were copied or moved to `destination`. | 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 | return destination | ||||
| else: | else: | ||||
| return FS.file_or_folder(Folder(destination).child(self.name)) | return FS.file_or_folder(Folder(destination).child(self.name)) | ||||
| @@ -156,6 +160,19 @@ class File(FS): | |||||
| """ | """ | ||||
| return self.extension.lstrip(".") | 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'): | def read_all(self, encoding='utf-8'): | ||||
| """ | """ | ||||
| Reads from the file and returns the content as a string. | Reads from the file and returns the content as a string. | ||||
| @@ -224,20 +241,56 @@ class FSVisitor(object): | |||||
| class FolderWalker(FSVisitor): | class FolderWalker(FSVisitor): | ||||
| """ | """ | ||||
| Walks the entire hirearchy of this directory starting with itself. | 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 | If a pattern is provided, only the files that match the pattern are | ||||
| processed. | 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): | def __exit__(self, exc_type, exc_val, exc_tb): | ||||
| """ | """ | ||||
| Automatically walk the folder when the context manager is exited. | 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): | def __visit_folder__(folder): | ||||
| @@ -271,18 +324,54 @@ class FolderWalker(FSVisitor): | |||||
| class FolderLister(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 | If a pattern is provided, only the files that match the pattern are | ||||
| processed. | 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): | def __exit__(self, exc_type, exc_val, exc_tb): | ||||
| """ | """ | ||||
| Automatically list the folder contents when the context manager is exited. | 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) | 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 | 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): | def make(self): | ||||
| """ | """ | ||||
| @@ -372,13 +461,15 @@ class Folder(FS): | |||||
| the tree has been deleted before and readded now. To workaround the | the tree has been deleted before and readded now. To workaround the | ||||
| bug, we first walk the tree and create directories that are needed. | 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 | @walker.folder_visitor | ||||
| def visit_folder(folder): | def visit_folder(folder): | ||||
| """ | """ | ||||
| Create the mirror directory | 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): | def copy_contents_to(self, destination): | ||||
| """ | """ | ||||
| @@ -386,20 +477,24 @@ class Folder(FS): | |||||
| Returns a Folder object that represents the moved directory. | Returns a Folder object that represents the moved directory. | ||||
| """ | """ | ||||
| logger.info("Copying contents of %s to %s" % (self, destination)) | 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) | 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) | return FolderLister(self, pattern) | ||||
| @@ -29,20 +29,20 @@ | |||||
| {% block favicons %} | {% block favicons %} | ||||
| <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | <!-- 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 %} | {% endblock favicons %} | ||||
| {% block css %} | {% block css %} | ||||
| <!-- CSS : implied media="all" --> | <!-- 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 | <!-- Uncomment if you are specifically targeting less enabled mobile browsers | ||||
| <link rel="stylesheet" media="handheld" href="css/handheld.css?v=2"> --> | <link rel="stylesheet" media="handheld" href="css/handheld.css?v=2"> --> | ||||
| {% endblock css %} | {% endblock css %} | ||||
| {% block headjs %} | {% block headjs %} | ||||
| <!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects --> | <!-- 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 %} | {% endblock headjs %} | ||||
| {% block endhead %}{% endblock endhead %} | {% block endhead %}{% endblock endhead %} | ||||
| </head> | </head> | ||||
| @@ -68,13 +68,13 @@ | |||||
| {% block jquery %} | {% block jquery %} | ||||
| <!-- Grab Google CDN's jQuery. fall back to local if necessary --> | <!-- 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 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 %} | {% endblock jquery %} | ||||
| {% block scripts %} | {% block scripts %} | ||||
| <!-- scripts concatenated and minified via ant build script--> | <!-- 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--> | <!-- end concatenated and minified scripts--> | ||||
| {% endblock scripts %} | {% endblock scripts %} | ||||
| @@ -83,7 +83,7 @@ | |||||
| <script> | <script> | ||||
| // More information on tackling transparent PNGs for IE goo.gl/mZiyb | // More information on tackling transparent PNGs for IE goo.gl/mZiyb | ||||
| //fix any <img> or .png_bg background-images | //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> | </script> | ||||
| <![endif]--> | <![endif]--> | ||||
| {% endblock pngfix %} | {% 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): | def __init__(self, site_path, config_dict=None): | ||||
| default_config = dict( | default_config = dict( | ||||
| content_root = 'content', | content_root = 'content', | ||||
| deploy_root = 'deploy', | |||||
| media_root = 'media', | media_root = 'media', | ||||
| layout_root = 'layout', | layout_root = 'layout', | ||||
| media_url = '/media', | media_url = '/media', | ||||
| @@ -46,6 +47,14 @@ class Config(Expando): | |||||
| super(Config, self).__init__(conf) | super(Config, self).__init__(conf) | ||||
| self.site_path = Folder(site_path) | 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 | @property | ||||
| def content_root_path(self): | def content_root_path(self): | ||||
| """ | """ | ||||
| @@ -3,32 +3,30 @@ | |||||
| Parses & holds information about the site to be generated. | Parses & holds information about the site to be generated. | ||||
| """ | """ | ||||
| from hyde.exceptions import HydeException | 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 logging | ||||
| import os | |||||
| from logging import NullHandler | from logging import NullHandler | ||||
| logger = logging.getLogger('hyde.site') | logger = logging.getLogger('hyde.site') | ||||
| logger.addHandler(NullHandler()) | 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): | def __repr__(self): | ||||
| return self.path | return self.path | ||||
| @@ -38,7 +36,22 @@ class Resource(object): | |||||
| """ | """ | ||||
| Gets the source path of this node. | 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 | @property | ||||
| def relative_path(self): | def relative_path(self): | ||||
| @@ -47,15 +60,17 @@ class Resource(object): | |||||
| """ | """ | ||||
| return self.source_file.get_relative_path(self.node.root.source_folder) | 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 | Represents any folder that is processed by hyde | ||||
| """ | """ | ||||
| def __init__(self, source_folder, parent=None): | def __init__(self, source_folder, parent=None): | ||||
| super(Node, self).__init__() | |||||
| super(Node, self).__init__(source_folder) | |||||
| if not 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.root = self | ||||
| self.module = None | self.module = None | ||||
| self.site = None | self.site = None | ||||
| @@ -68,16 +83,14 @@ class Node(object): | |||||
| self.child_nodes = [] | self.child_nodes = [] | ||||
| self.resources = [] | self.resources = [] | ||||
| def __repr__(self): | |||||
| return self.path | |||||
| def add_child_node(self, folder): | def add_child_node(self, folder): | ||||
| """ | """ | ||||
| Creates a new child node and adds it to the list of child nodes. | Creates a new child node and adds it to the list of child nodes. | ||||
| """ | """ | ||||
| if folder.parent != self.source_folder: | 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)) | (folder, self.source_folder)) | ||||
| node = Node(folder, self) | node = Node(folder, self) | ||||
| self.child_nodes.append(node) | self.child_nodes.append(node) | ||||
| @@ -89,18 +102,26 @@ class Node(object): | |||||
| """ | """ | ||||
| if afile.parent != self.source_folder: | 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)) | (afile, self.source_folder)) | ||||
| resource = Resource(afile, self) | resource = Resource(afile, self) | ||||
| self.resources.append(resource) | self.resources.append(resource) | ||||
| return 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 | @property | ||||
| def relative_path(self): | def relative_path(self): | ||||
| @@ -109,20 +130,22 @@ class Node(object): | |||||
| """ | """ | ||||
| return self.source_folder.get_relative_path(self.root.source_folder) | return self.source_folder.get_relative_path(self.root.source_folder) | ||||
| class RootNode(Node): | class RootNode(Node): | ||||
| """ | """ | ||||
| Represents one of the roots of site: Content, Media or Layout | Represents one of the roots of site: Content, Media or Layout | ||||
| """ | """ | ||||
| def __init__(self, source_folder, site): | 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): | 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: | if Folder(path) == self.source_folder: | ||||
| return self | return self | ||||
| @@ -130,26 +153,32 @@ class RootNode(Node): | |||||
| def node_from_relative_path(self, relative_path): | 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): | 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) | return self.resource_map.get(str(File(path)), None) | ||||
| def resource_from_relative_path(self, relative_path): | 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): | 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) | folder = Folder(a_folder) | ||||
| node = self.node_from_path(folder) | node = self.node_from_path(folder) | ||||
| @@ -158,7 +187,8 @@ class RootNode(Node): | |||||
| return node | return node | ||||
| if not folder.is_descendant_of(self.source_folder): | 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)) | (folder, self.source_folder)) | ||||
| p_folder = folder | p_folder = folder | ||||
| @@ -174,14 +204,15 @@ class RootNode(Node): | |||||
| for h_folder in hierarchy: | for h_folder in hierarchy: | ||||
| node = node.add_child_node(h_folder) | node = node.add_child_node(h_folder) | ||||
| self.node_map[str(h_folder)] = node | 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 | return node | ||||
| def add_resource(self, a_file): | 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) | afile = File(a_file) | ||||
| @@ -191,7 +222,8 @@ class RootNode(Node): | |||||
| logger.info("Resource exists at [%s]" % resource.relative_path) | logger.info("Resource exists at [%s]" % resource.relative_path) | ||||
| if not afile.is_descendant_of(self.source_folder): | 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)) | (afile, self.content_folder)) | ||||
| node = self.node_from_path(afile.parent) | node = self.node_from_path(afile.parent) | ||||
| @@ -201,19 +233,22 @@ class RootNode(Node): | |||||
| resource = node.add_child_resource(afile) | resource = node.add_child_resource(afile) | ||||
| self.resource_map[str(afile)] = resource | 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 | return resource | ||||
| def build(self): | 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: | 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 | @walker.folder_visitor | ||||
| def visit_folder(folder): | def visit_folder(folder): | ||||
| @@ -223,6 +258,7 @@ class RootNode(Node): | |||||
| def visit_file(afile): | def visit_file(afile): | ||||
| self.add_resource(afile) | self.add_resource(afile) | ||||
| class Site(object): | class Site(object): | ||||
| """ | """ | ||||
| Represents the site to be generated. | Represents the site to be generated. | ||||
| @@ -232,9 +268,7 @@ class Site(object): | |||||
| super(Site, self).__init__() | super(Site, self).__init__() | ||||
| self.site_path = Folder(str(site_path)) | self.site_path = Folder(str(site_path)) | ||||
| self.config = config if config else Config(self.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): | def build(self): | ||||
| """ | """ | ||||
| @@ -19,9 +19,9 @@ class Template(object): | |||||
| """ | """ | ||||
| abstract | 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. | must return the rendered string. | ||||
| """ | """ | ||||
| abstract | 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" %} | {% extends "blog/post.html" %} | ||||
| {% block article %} | {% block article %} | ||||
| {% lipsum n=10 %} | |||||
| {{ lipsum() }} | |||||
| {% endblock %} | {% endblock %} | ||||
| {% block aside %} | {% block aside %} | ||||
| {% lipsum n=2 %} | |||||
| {{ lipsum() }} | |||||
| {% endblock %} | {% 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 %} | {% block favicons %} | ||||
| <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | <!-- 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 %} | {% endblock favicons %} | ||||
| {% block css %} | {% block css %} | ||||
| <link rel="stylesheet" href="{% media 'css/site.css' %}"> | |||||
| <link rel="stylesheet" href="{{ media_url('css/site.css') }}"> | |||||
| {% endblock css %} | {% endblock css %} | ||||
| {% block endhead %}{% endblock endhead %} | {% block endhead %}{% endblock endhead %} | ||||
| </head> | </head> | ||||
| @@ -1,6 +1,7 @@ | |||||
| mode: development | mode: development | ||||
| media_root:: media # Relative path from site root (the directory where this file exists) | media_root:: media # Relative path from site root (the directory where this file exists) | ||||
| media_url: /media | media_url: /media | ||||
| template: hyde.ext.jinja2 | |||||
| widgets: | widgets: | ||||
| plugins: | plugins: | ||||
| aggregators: | aggregators: | ||||
| @@ -44,6 +44,10 @@ def test_kind(): | |||||
| f = File(__file__) | f = File(__file__) | ||||
| assert f.kind == os.path.splitext(__file__)[1].lstrip('.') | 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(): | def test_parent(): | ||||
| f = File(__file__) | f = File(__file__) | ||||
| p = f.parent | p = f.parent | ||||
| @@ -104,6 +108,7 @@ JINJA2 = TEMPLATE_ROOT.child_folder('jinja2') | |||||
| HELPERS = File(JINJA2.child('helpers.html')) | HELPERS = File(JINJA2.child('helpers.html')) | ||||
| INDEX = File(JINJA2.child('index.html')) | INDEX = File(JINJA2.child('index.html')) | ||||
| LAYOUT = File(JINJA2.child('layout.html')) | LAYOUT = File(JINJA2.child('layout.html')) | ||||
| LOGO = File(TEMPLATE_ROOT.child('../../../resources/hyde-logo.png')) | |||||
| def test_ancestors(): | def test_ancestors(): | ||||
| depth = 0 | depth = 0 | ||||
| @@ -132,10 +137,11 @@ def test_is_descendant_of(): | |||||
| print "*" | print "*" | ||||
| assert not INDEX.is_descendant_of(DATA_ROOT) | 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) == Folder(JINJA2.name).child(INDEX.name) | ||||
| assert INDEX.get_relative_path(TEMPLATE_ROOT.parent) == Folder( | assert INDEX.get_relative_path(TEMPLATE_ROOT.parent) == Folder( | ||||
| TEMPLATE_ROOT.name).child_folder(JINJA2.name).child(INDEX.name) | TEMPLATE_ROOT.name).child_folder(JINJA2.name).child(INDEX.name) | ||||
| assert JINJA2.get_relative_path(JINJA2) == "" | |||||
| def test_get_mirror(): | def test_get_mirror(): | ||||
| mirror = JINJA2.get_mirror(DATA_ROOT, source_root=TEMPLATE_ROOT) | 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) | mirror = JINJA2.get_mirror(DATA_ROOT, source_root=TEMPLATE_ROOT.parent) | ||||
| assert mirror == DATA_ROOT.child_folder(TEMPLATE_ROOT.name).child_folder(JINJA2.name) | 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 | @nottest | ||||
| def setup_data(): | def setup_data(): | ||||
| DATA_ROOT.make() | DATA_ROOT.make() | ||||
| @@ -242,7 +260,7 @@ def test_walker(): | |||||
| files = [] | files = [] | ||||
| complete = [] | complete = [] | ||||
| with TEMPLATE_ROOT.walk() as walker: | |||||
| with TEMPLATE_ROOT.walker as walker: | |||||
| @walker.folder_visitor | @walker.folder_visitor | ||||
| def visit_folder(f): | def visit_folder(f): | ||||
| @@ -265,12 +283,34 @@ def test_walker(): | |||||
| assert len(folders) == 2 | assert len(folders) == 2 | ||||
| assert len(complete) == 1 | 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(): | def test_walker_templates_just_root(): | ||||
| folders = [] | folders = [] | ||||
| files = [] | files = [] | ||||
| complete = [] | complete = [] | ||||
| with TEMPLATE_ROOT.walk() as walker: | |||||
| with TEMPLATE_ROOT.walker as walker: | |||||
| @walker.folder_visitor | @walker.folder_visitor | ||||
| def visit_folder(f): | def visit_folder(f): | ||||
| @@ -295,7 +335,7 @@ def test_lister_templates(): | |||||
| files = [] | files = [] | ||||
| complete = [] | complete = [] | ||||
| with TEMPLATE_ROOT.list() as lister: | |||||
| with TEMPLATE_ROOT.lister as lister: | |||||
| @lister.folder_visitor | @lister.folder_visitor | ||||
| def visit_folder(f): | def visit_folder(f): | ||||
| @@ -315,12 +355,39 @@ def test_lister_templates(): | |||||
| assert len(complete) == 1 | 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(): | def test_lister_jinja2(): | ||||
| folders = [] | folders = [] | ||||
| files = [] | files = [] | ||||
| complete = [] | complete = [] | ||||
| with JINJA2.list() as lister: | |||||
| with JINJA2.lister as lister: | |||||
| @lister.folder_visitor | @lister.folder_visitor | ||||
| def visit_folder(f): | def visit_folder(f): | ||||
| @@ -9,10 +9,11 @@ Use nose | |||||
| from hyde.engine import Engine | from hyde.engine import Engine | ||||
| from hyde.exceptions import HydeException | from hyde.exceptions import HydeException | ||||
| from hyde.fs import FS, File, Folder | from hyde.fs import FS, File, Folder | ||||
| from hyde.layout import Layout | |||||
| from nose.tools import raises, with_setup, nottest | from nose.tools import raises, with_setup, nottest | ||||
| TEST_SITE = File(__file__).parent.child_folder('_test') | TEST_SITE = File(__file__).parent.child_folder('_test') | ||||
| TEST_SITE_AT_USER = Folder('~/_test') | |||||
| @nottest | @nottest | ||||
| def create_test_site(): | def create_test_site(): | ||||
| @@ -22,6 +23,14 @@ def create_test_site(): | |||||
| def delete_test_site(): | def delete_test_site(): | ||||
| TEST_SITE.delete() | 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) | @raises(HydeException) | ||||
| @with_setup(create_test_site, delete_test_site) | @with_setup(create_test_site, delete_test_site) | ||||
| def test_ensure_exception_when_sitepath_exists(): | def test_ensure_exception_when_sitepath_exists(): | ||||
| @@ -39,9 +48,29 @@ def test_ensure_no_exception_when_sitepath_does_not_exist(): | |||||
| e = Engine() | e = Engine() | ||||
| TEST_SITE.delete() | TEST_SITE.delete() | ||||
| e.run(e.parse(['-s', str(TEST_SITE), 'init', '-f'])) | 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) | @raises(HydeException) | ||||
| @with_setup(create_test_site, delete_test_site) | @with_setup(create_test_site, delete_test_site) | ||||
| @@ -44,6 +44,7 @@ class TestConfig(object): | |||||
| cls.conf2 = """ | cls.conf2 = """ | ||||
| mode: development | mode: development | ||||
| deploy_root: ~/deploy_site | |||||
| content_root: site/stuff # Relative path from site root | content_root: site/stuff # Relative path from site root | ||||
| media_root: mmm # Relative path from site root | media_root: mmm # Relative path from site root | ||||
| media_url: /media | media_url: /media | ||||
| @@ -62,6 +63,9 @@ class TestConfig(object): | |||||
| assert hasattr(c, path) | assert hasattr(c, path) | ||||
| assert getattr(c, path) == TEST_SITE_ROOT.child_folder(root) | 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): | def test_conf1(self): | ||||
| c = Config(site_path=TEST_SITE_ROOT, config_dict=yaml.load(self.conf1)) | c = Config(site_path=TEST_SITE_ROOT, config_dict=yaml.load(self.conf1)) | ||||
| assert c.content_root_path == TEST_SITE_ROOT.child_folder('stuff') | 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.content_root_path == TEST_SITE_ROOT.child_folder('site/stuff') | ||||
| assert c.media_root_path == TEST_SITE_ROOT.child_folder('mmm') | assert c.media_root_path == TEST_SITE_ROOT.child_folder('mmm') | ||||
| assert c.media_url == TEST_SITE_ROOT.child_folder('/media') | 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 resource.relative_path == path | ||||
| assert not s.content.resource_from_relative_path('/happy-festivus.html') | 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): | class TestSiteWithConfig(object): | ||||
| @classmethod | @classmethod | ||||