@@ -11,4 +11,4 @@ test_site | |||
dist | |||
build | |||
*egg* | |||
.idea |
@@ -50,6 +50,7 @@ class Engine(Application): | |||
layout.copy_contents_to(args.sitepath) | |||
@subcommand('gen', help='Generate the site') | |||
@store('-c', '--config-path', default='site.yaml', 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): | |||
""" | |||
@@ -1,678 +0,0 @@ | |||
""" | |||
Unified interface for performing file system tasks. Uses os, os.path. shutil | |||
and distutil to perform the tasks. The behavior of some functions is slightly | |||
contaminated with requirements from Hyde: For example, the backup function | |||
deletes the directory that is being backed up. | |||
""" | |||
import os | |||
import shutil | |||
import codecs | |||
import fnmatch | |||
from datetime import datetime | |||
# pylint: disable-msg=E0611 | |||
from distutils import dir_util, file_util | |||
@staticmethod | |||
def filter_hidden_inplace(item_list): | |||
""" | |||
Given a list of filenames, removes filenames for invisible files (whose | |||
names begin with dots) or files whose names end in tildes '~'. | |||
Does not remove files with the filname '.htaccess'. | |||
The list is modified in-place; this function has no return value. | |||
""" | |||
if not item_list: | |||
return | |||
wanted = filter( | |||
lambda item: | |||
not ((item.startswith('.') and item != ".htaccess") | |||
or item.endswith('~')), | |||
item_list) | |||
count = len(item_list) | |||
good_item_count = len(wanted) | |||
if count == good_item_count: | |||
return | |||
item_list[:good_item_count] = wanted | |||
for _ in range(good_item_count, count): | |||
item_list.pop() | |||
@staticmethod | |||
def get_path_fragment(root_dir, a_dir): | |||
""" | |||
Gets the path fragment starting at root_dir to a_dir | |||
""" | |||
current_dir = a_dir | |||
current_fragment = '' | |||
while not current_dir == root_dir: | |||
(current_dir, current_fragment_part) = os.path.split(current_dir) | |||
current_fragment = os.path.join( | |||
current_fragment_part, current_fragment) | |||
return current_fragment | |||
@staticmethod | |||
def get_mirror_dir(directory, source_root, mirror_root, ignore_root = False): | |||
""" | |||
Returns the mirror directory from source_root to mirror_root | |||
""" | |||
current_fragment = get_path_fragment(source_root, directory) | |||
if not current_fragment: | |||
return mirror_root | |||
mirror_directory = mirror_root | |||
if not ignore_root: | |||
mirror_directory = os.path.join( | |||
mirror_root, | |||
os.path.basename(source_root)) | |||
mirror_directory = os.path.join( | |||
mirror_directory, current_fragment) | |||
return mirror_directory | |||
@staticmethod | |||
def mirror_dir_tree(directory, source_root, mirror_root, ignore_root = False): | |||
""" | |||
Create the mirror directory tree | |||
""" | |||
mirror_directory = get_mirror_dir( | |||
directory, source_root, | |||
mirror_root, ignore_root) | |||
try: | |||
os.makedirs(mirror_directory) | |||
except os.error: | |||
pass | |||
return mirror_directory | |||
class FileSystemEntity(object): | |||
""" | |||
Base class for files and folders. | |||
""" | |||
def __init__(self, path): | |||
super(FileSystemEntity, self).__init__() | |||
if path is FileSystemEntity: | |||
self.path = path.path | |||
else: | |||
self.path = path | |||
def __str__(self): | |||
return self.path | |||
def __repr__(self): | |||
return self.path | |||
def allow(self, include=None, exclude=None): | |||
""" | |||
Given a set of wilcard patterns in the include and exclude arguments, | |||
tests if the patterns allow this item for processing. | |||
The exclude parameter is processed first as a broader filter and then | |||
include is used as a narrower filter to override the results for more | |||
specific files. | |||
Example: | |||
exclude = (".*", "*~") | |||
include = (".htaccess") | |||
""" | |||
if not include: | |||
include = () | |||
if not exclude: | |||
exclude = () | |||
if reduce(lambda result, | |||
pattern: result or | |||
fnmatch.fnmatch(self.name, pattern), include, False): | |||
return True | |||
if reduce(lambda result, pattern: | |||
result and not fnmatch.fnmatch(self.name, pattern), | |||
exclude, True): | |||
return True | |||
return False | |||
@property | |||
def humblepath(self): | |||
""" | |||
Expands variables, user, normalizes path and case and coverts | |||
to absolute. | |||
""" | |||
return os.path.abspath( | |||
os.path.normpath( | |||
os.path.normcase( | |||
os.path.expandvars( | |||
os.path.expanduser(self.path))))) | |||
def same_as(self, other): | |||
""" | |||
Checks if the path of this object is same as `other`. `other` must | |||
be a FileSystemEntity. | |||
""" | |||
return (self.humblepath.rstrip(os.sep) == | |||
other.humblepath.rstrip(os.sep)) | |||
@property | |||
def exists(self): | |||
""" | |||
Checks if the entity exists in the file system. | |||
""" | |||
return os.path.exists(self.path) | |||
@property | |||
def isdir(self): | |||
""" | |||
Is this a folder. | |||
""" | |||
return os.path.isdir(self.path) | |||
@property | |||
def stats(self): | |||
""" | |||
Shortcut for os.stat. | |||
""" | |||
return os.stat(self.path) | |||
@property | |||
def name(self): | |||
""" | |||
Name of the entity. Calls os.path.basename. | |||
""" | |||
return os.path.basename(self.path) | |||
@property | |||
def parent(self): | |||
""" | |||
The parent folder. Returns a `Folder` object. | |||
""" | |||
return Folder(os.path.dirname(self.path)) | |||
def __get_destination__(self, destination): | |||
""" | |||
Returns a File or Folder object that would represent this entity | |||
if it were copied or moved to `destination`. `destination` must be | |||
an instance of File or Folder. | |||
""" | |||
if os.path.isdir(str(destination)): | |||
target = destination.child(self.name) | |||
if os.path.isdir(self.path): | |||
return Folder(target) | |||
else: return File(target) | |||
else: | |||
return destination | |||
# pylint: disable-msg=R0904,W0142 | |||
class File(FileSystemEntity): | |||
""" | |||
Encapsulates commonly used functions related to files. | |||
""" | |||
def __init__(self, path): | |||
super(File, self).__init__(path) | |||
@property | |||
def size(self): | |||
""" | |||
Gets the file size | |||
""" | |||
return os.path.getsize(self.path) | |||
#return 1 | |||
def has_extension(self, extension): | |||
""" | |||
Checks if this file has the given extension. | |||
""" | |||
return self.extension == extension | |||
def delete(self): | |||
""" | |||
Deletes if the file exists. | |||
""" | |||
if self.exists: | |||
os.remove(self.path) | |||
@property | |||
def last_modified(self): | |||
""" | |||
Returns a datetime object representing the last modified time. | |||
Calls os.path.getmtime. | |||
""" | |||
return datetime.fromtimestamp(os.path.getmtime(self.path)) | |||
def changed_since(self, basetime): | |||
""" | |||
Returns True if the file has been changed since the given time. | |||
""" | |||
return self.last_modified > basetime | |||
def older_than(self, another_file): | |||
""" | |||
Checks if this file is older than the given file. Uses last_modified to | |||
determine age. | |||
""" | |||
return another_file.last_modified > self.last_modified | |||
@property | |||
def path_without_extension(self): | |||
""" | |||
The full path of the file without its extension. | |||
""" | |||
return os.path.splitext(self.path)[0] | |||
@property | |||
def name_without_extension(self): | |||
""" | |||
Name of the file without its extension. | |||
""" | |||
return os.path.splitext(self.name)[0] | |||
@property | |||
def extension(self): | |||
""" | |||
File's extension prefixed with a dot. | |||
""" | |||
return os.path.splitext(self.path)[1] | |||
@property | |||
def kind(self): | |||
""" | |||
File's extension without a dot prefix. | |||
""" | |||
return self.extension.lstrip(".") | |||
def move_to(self, destination): | |||
""" | |||
Moves the file to the given destination. Returns a File | |||
object that represents the target file. `destination` must | |||
be a File or Folder object. | |||
""" | |||
shutil.move(self.path, str(destination)) | |||
return self.__get_destination__(destination) | |||
def copy_to(self, destination): | |||
""" | |||
Copies the file to the given destination. Returns a File | |||
object that represents the target file. `destination` must | |||
be a File or Folder object. | |||
""" | |||
shutil.copy(self.path, str(destination)) | |||
return self.__get_destination__(destination) | |||
def write(self, text, encoding="utf-8"): | |||
""" | |||
Writes the given text to the file using the given encoding. | |||
""" | |||
fout = codecs.open(self.path, 'w', encoding) | |||
fout.write(text) | |||
fout.close() | |||
def read_all(self): | |||
""" | |||
Reads from the file and returns the content as a string. | |||
""" | |||
fin = codecs.open(self.path, 'r') | |||
read_text = fin.read() | |||
fin.close() | |||
return read_text | |||
# pylint: disable-msg=R0904,W0142 | |||
class Folder(FileSystemEntity): | |||
""" | |||
Encapsulates commonly used directory functions. | |||
""" | |||
def __init__(self, path): | |||
super(Folder, self).__init__(path) | |||
def __str__(self): | |||
return self.path | |||
def __repr__(self): | |||
return self.path | |||
def delete(self): | |||
""" | |||
Deletes the directory if it exists. | |||
""" | |||
if self.exists: | |||
shutil.rmtree(self.path) | |||
def depth(self): | |||
""" | |||
Returns the number of ancestors of this directory. | |||
""" | |||
return len(self.path.split(os.sep)) | |||
def make(self): | |||
""" | |||
Creates this directory and any of the missing directories in the path. | |||
Any errors that may occur are eaten. | |||
""" | |||
try: | |||
if not self.exists: | |||
os.makedirs(self.path) | |||
except os.error: | |||
pass | |||
return self | |||
def is_parent_of(self, other_entity): | |||
""" | |||
Returns True if this directory is a direct parent of the the given | |||
directory. | |||
""" | |||
return self.same_as(other_entity.parent) | |||
def is_ancestor_of(self, other_entity): | |||
""" | |||
Returns True if this directory is in the path of the given directory. | |||
Note that this will return True if the given directory is same as this. | |||
""" | |||
folder = other_entity | |||
while not folder.parent.same_as(folder): | |||
folder = folder.parent | |||
if self.same_as(folder): | |||
return True | |||
return False | |||
def child(self, name): | |||
""" | |||
Returns a path of a child item represented by `name`. | |||
""" | |||
return os.path.join(self.path, name) | |||
def child_folder(self, *args): | |||
""" | |||
Returns a Folder object by joining the path component in args | |||
to this directory's path. | |||
""" | |||
return Folder(os.path.join(self.path, *args)) | |||
def child_folder_with_fragment(self, fragment): | |||
""" | |||
Returns a Folder object by joining the fragment to | |||
this directory's path. | |||
""" | |||
return Folder(os.path.join(self.path, fragment.lstrip(os.sep))) | |||
def get_fragment(self, root): | |||
""" | |||
Returns the path fragment of this directory starting with the given | |||
directory. | |||
""" | |||
return get_path_fragment(str(root), self.path) | |||
def get_mirror_folder(self, root, mirror_root, ignore_root=False): | |||
""" | |||
Returns a Folder object that reperesents if the entire fragment of this | |||
directory starting with `root` were copied to `mirror_root`. If ignore_root | |||
is True, the mirror does not include `root` directory itself. | |||
Example: | |||
Current Directory: /usr/local/hyde/stuff | |||
root: /usr/local/hyde | |||
mirror_root: /usr/tmp | |||
Result: | |||
if ignore_root == False: | |||
Folder(/usr/tmp/hyde/stuff) | |||
if ignore_root == True: | |||
Folder(/usr/tmp/stuff) | |||
""" | |||
path = get_mirror_dir(self.path, | |||
str(root), str(mirror_root), ignore_root) | |||
return Folder(path) | |||
def create_mirror_folder(self, root, mirror_root, ignore_root=False): | |||
""" | |||
Creates the mirror directory returned by `get_mirror_folder` | |||
""" | |||
mirror_folder = self.get_mirror_folder( | |||
root, mirror_root, ignore_root) | |||
mirror_folder.make() | |||
return mirror_folder | |||
def backup(self, destination): | |||
""" | |||
Creates a backup of this directory in the given destination. The backup is | |||
suffixed with a number for uniqueness. Deletes this directory after backup | |||
is complete. | |||
""" | |||
new_name = self.name | |||
count = 0 | |||
dest = Folder(destination.child(new_name)) | |||
while(True): | |||
dest = Folder(destination.child(new_name)) | |||
if not dest.exists: | |||
break | |||
else: | |||
count = count + 1 | |||
new_name = self.name + str(count) | |||
dest.make() | |||
dest.move_contents_of(self) | |||
self.delete() | |||
return dest | |||
def move_to(self, destination): | |||
""" | |||
Moves this directory to the given destination. Returns a Folder object | |||
that represents the moved directory. | |||
""" | |||
shutil.copytree(self.path, str(destination)) | |||
shutil.rmtree(self.path) | |||
return self.__get_destination__(destination) | |||
def copy_to(self, destination): | |||
""" | |||
Copies this directory to the given destination. Returns a Folder object | |||
that represents the moved directory. | |||
""" | |||
shutil.copytree(self.path, str(destination)) | |||
return self.__get_destination__(destination) | |||
def move_folder_from(self, source, incremental=False): | |||
""" | |||
Moves the given source directory to this directory. If incremental is True | |||
only newer objects are overwritten. | |||
""" | |||
self.copy_folder_from(source, incremental) | |||
shutil.rmtree(str(source)) | |||
def copy_folder_from(self, source, incremental=False): | |||
""" | |||
Copies the given source directory to this directory. If incremental is True | |||
only newer objects are overwritten. | |||
""" | |||
# There is a bug in dir_util that makes copy_tree crash if a folder in | |||
# the tree has been deleted before and readded now. To workaround the | |||
# bug, we first walk the tree and create directories that are needed. | |||
# | |||
# pylint: disable-msg=C0111,W0232 | |||
target_root = self | |||
# pylint: disable-msg=R0903 | |||
class _DirCreator: | |||
@staticmethod | |||
def visit_folder(folder): | |||
target = folder.get_mirror_folder( | |||
source.parent, target_root, ignore_root=True) | |||
target.make() | |||
source.walk(_DirCreator) | |||
dir_util.copy_tree(str(source), | |||
self.child(source.name), | |||
update=incremental) | |||
def move_contents_of(self, source, move_empty_folders=True, | |||
incremental=False): | |||
""" | |||
Moves the contents of the given source directory to this directory. If | |||
incremental is True only newer objects are overwritten. | |||
""" | |||
# pylint: disable-msg=C0111,W0232 | |||
class _Mover: | |||
@staticmethod | |||
def visit_folder(folder): | |||
self.move_folder_from(folder, incremental) | |||
@staticmethod | |||
def visit_file(a_file): | |||
self.move_file_from(a_file, incremental) | |||
source.list(_Mover, move_empty_folders) | |||
def copy_contents_of(self, source, copy_empty_folders=True, | |||
incremental=False): | |||
""" | |||
Copies the contents of the given source directory to this directory. If | |||
incremental is True only newer objects are overwritten. | |||
""" | |||
# pylint: disable-msg=C0111,W0232 | |||
class _Copier: | |||
@staticmethod | |||
def visit_folder(folder): | |||
self.copy_folder_from(folder, incremental) | |||
@staticmethod | |||
def visit_file(a_file): | |||
self.copy_file_from(a_file, incremental) | |||
source.list(_Copier, copy_empty_folders) | |||
def move_file_from(self, source, incremental=False): | |||
""" | |||
Moves the given source file to this directory. If incremental is True the | |||
move is performed only if the source file is newer. | |||
""" | |||
self.copy_file_from(source, incremental) | |||
source.delete() | |||
def copy_file_from(self, source, incremental=False): | |||
""" | |||
Copies the given source file to this directory. If incremental is True the | |||
move is performed only if the source file is newer. | |||
""" | |||
file_util.copy_file(str(source), self.path, update=incremental) | |||
def list(self, visitor, list_empty_folders=True): | |||
""" | |||
Calls the visitor.visit_file or visitor.visit_folder for each file or folder | |||
in this directory. If list_empty_folders is False folders that are empty are | |||
skipped. | |||
""" | |||
a_files = os.listdir(self.path) | |||
for a_file in a_files: | |||
path = os.path.join(self.path, str(a_file)) | |||
if os.path.isdir(path): | |||
if not list_empty_folders: | |||
if Folder(path).empty(): | |||
continue | |||
visitor.visit_folder(Folder(path)) | |||
else: | |||
visitor.visit_file(File(path)) | |||
def empty(self): | |||
""" | |||
Checks if this directory or any of its subdirectories contain files. | |||
""" | |||
paths = os.listdir(self.path) | |||
for path in paths: | |||
if os.path.isdir(path): | |||
if not Folder(path).empty(): | |||
return False | |||
else: | |||
return False | |||
return True | |||
def walk(self, visitor = None, pattern = None): | |||
""" | |||
Walks the entire hirearchy of this directory starting with itself. | |||
Calls visitor.visit_folder first and then calls visitor.visit_file for | |||
any files found. After all files and folders have been exhausted | |||
visitor.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 __visit_folder__(visitor, folder): | |||
process_folder = True | |||
if visitor and hasattr(visitor,'visit_folder'): | |||
process_folder = visitor.visit_folder(folder) | |||
# If there is no return value assume true | |||
# | |||
if process_folder is None: | |||
process_folder = True | |||
return process_folder | |||
def __visit_file__(visitor, a_file): | |||
if visitor and hasattr(visitor,'visit_file'): | |||
visitor.visit_file(a_file) | |||
def __visit_complete__(visitor): | |||
if visitor and hasattr(visitor,'visit_complete'): | |||
visitor.visit_complete() | |||
for root, dirs, a_files in os.walk(self.path): | |||
folder = Folder(root) | |||
if not __visit_folder__(visitor, folder): | |||
dirs[:] = [] | |||
continue | |||
for a_file in a_files: | |||
if not pattern or fnmatch.fnmatch(a_file, pattern): | |||
__visit_file__(visitor, File(folder.child(a_file))) | |||
__visit_complete__(visitor) |
@@ -14,6 +14,7 @@ import os | |||
import shutil | |||
from distutils import dir_util | |||
import functools | |||
import itertools | |||
# pylint: disable-msg=E0611 | |||
logger = logging.getLogger('fs') | |||
@@ -28,7 +29,7 @@ class FS(object): | |||
""" | |||
def __init__(self, path): | |||
super(FS, self).__init__() | |||
self.path = str(path).strip() | |||
self.path = str(path).strip().rstrip(os.sep) | |||
def __str__(self): | |||
return self.path | |||
@@ -63,6 +64,14 @@ class FS(object): | |||
""" | |||
return Folder(os.path.dirname(self.path)) | |||
@property | |||
def depth(self): | |||
""" | |||
Returns the number of ancestors of this directory. | |||
""" | |||
return len(self.path.rstrip(os.sep).split(os.sep)) | |||
def ancestors(self, stop=None): | |||
""" | |||
Generates the parents until stop or the absolute | |||
@@ -75,7 +84,16 @@ class FS(object): | |||
yield f.parent | |||
f = f.parent | |||
def get_fragment(self, root): | |||
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 | |||
def get_relative_path(self, root): | |||
""" | |||
Gets the fragment of the current path starting at root. | |||
""" | |||
@@ -89,7 +107,7 @@ class FS(object): | |||
>>> Folder('/usr/local/hyde/stuff').get_mirror('/usr/tmp', source_root='/usr/local/hyde') | |||
Folder('/usr/tmp/stuff') | |||
""" | |||
fragment = self.get_fragment(source_root if source_root else self.parent) | |||
fragment = self.get_relative_path(source_root if source_root else self.parent) | |||
return Folder(target_root).child(fragment) | |||
@staticmethod | |||
@@ -0,0 +1,9 @@ | |||
site: | |||
mode: development | |||
media: | |||
root: | |||
path: media # Relative path from site root (the directory where this file exists) | |||
url: /media | |||
widgets: | |||
plugins: | |||
aggregators: |
@@ -1 +1,5 @@ | |||
# -*- coding: utf-8 -*- | |||
# -*- coding: utf-8 -*- | |||
# 1. start_node | |||
# 2. start_resource | |||
# 3. end_resource | |||
# 4. end_node |
@@ -0,0 +1,245 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
Parses & holds information about the site to be generated. | |||
""" | |||
from hyde.fs import File, Folder | |||
from hyde.exceptions import HydeException | |||
import logging | |||
import os | |||
from logging import NullHandler | |||
logger = logging.getLogger('hyde.site') | |||
logger.addHandler(NullHandler()) | |||
class Resource(object): | |||
""" | |||
Represents any file that is processed by hyde | |||
""" | |||
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 __repr__(self): | |||
return self.path | |||
@property | |||
def path(self): | |||
""" | |||
Gets the source path of this node. | |||
""" | |||
return self.source_file.path | |||
@property | |||
def relative_path(self): | |||
""" | |||
Gets the path relative to the root folder (Content, Media, Layout) | |||
""" | |||
return self.source_file.get_relative_path(self.node.root.source_folder) | |||
class Node(object): | |||
""" | |||
Represents any folder that is processed by hyde | |||
""" | |||
def __init__(self, source_folder, parent=None): | |||
super(Node, self).__init__() | |||
if not source_folder: | |||
raise HydeException("Source folder is required to instantiate a node.") | |||
self.root = self | |||
self.module = None | |||
self.site = None | |||
self.source_folder = Folder(str(source_folder)) | |||
self.parent = parent | |||
if parent: | |||
self.root = self.parent.root | |||
self.module = self.parent.module if self.parent.module else self | |||
self.site = parent.site | |||
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]" % | |||
(folder, self.source_folder)) | |||
node = Node(folder, self) | |||
self.child_nodes.append(node) | |||
return node | |||
def add_child_resource(self, afile): | |||
""" | |||
Creates a new resource and adds it to the list of child resources. | |||
""" | |||
if afile.parent != self.source_folder: | |||
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): | |||
""" | |||
Gets the source path of this node. | |||
""" | |||
return self.source_folder.path | |||
@property | |||
def relative_path(self): | |||
""" | |||
Gets the path relative to the root folder (Content, Media, Layout) | |||
""" | |||
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 = {} | |||
def node_from_path(self, path): | |||
""" | |||
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 | |||
return self.node_map.get(str(Folder(path)), None) | |||
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. | |||
""" | |||
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. | |||
""" | |||
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. | |||
""" | |||
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. | |||
""" | |||
folder = Folder(a_folder) | |||
node = self.node_from_path(folder) | |||
if node: | |||
logger.info("Node exists at [%s]" % node.relative_path) | |||
return node | |||
if not folder.is_descendant_of(self.source_folder): | |||
raise HydeException("The given folder [%s] does not belong to this hierarchy [%s]" % | |||
(folder, self.source_folder)) | |||
p_folder = folder | |||
parent = None | |||
hierarchy = [] | |||
while not parent: | |||
hierarchy.append(p_folder) | |||
p_folder = p_folder.parent | |||
parent = self.node_from_path(p_folder) | |||
hierarchy.reverse() | |||
node = parent if parent else self | |||
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)) | |||
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. | |||
""" | |||
afile = File(a_file) | |||
resource = self.resource_from_path(afile) | |||
if resource: | |||
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]" % | |||
(afile, self.content_folder)) | |||
node = self.node_from_path(afile.parent) | |||
if not node: | |||
node = self.add_node(afile.parent) | |||
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)) | |||
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. | |||
""" | |||
if not self.source_folder.exists: | |||
raise HydeException("The given source folder[%s] does not exist" % self.source_folder) | |||
with self.source_folder.walk() as walker: | |||
@walker.folder_visitor | |||
def visit_folder(folder): | |||
self.add_node(folder) | |||
@walker.file_visitor | |||
def visit_file(afile): | |||
self.add_resource(afile) | |||
class Site(object): | |||
""" | |||
Represents the site to be generated | |||
""" | |||
def __init__(self, site_path): | |||
super(Site, self).__init__() | |||
self.site_path = Folder(str(site_path)) | |||
# TODO: Get the value from config | |||
content_folder = self.site_path.child_folder('content') | |||
self.content = RootNode(content_folder, self) | |||
self.node_map = {} | |||
self.resource_map = {} | |||
def build(self): | |||
""" | |||
Walks the content and media folders to build up the sitemap. | |||
""" | |||
self.content.build() | |||
@@ -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 n=10 %} | |||
{% endblock %} | |||
{% block aside %} | |||
{% lipsum n=2 %} | |||
{% endblock %} |
@@ -0,0 +1,4 @@ | |||
author: Lakshmi Vyasarajan | |||
description: A test layout for hyde. | |||
template: jinja2 (2.6) | |||
version: 0.1 |
@@ -0,0 +1,56 @@ | |||
{% 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 '/favicon.ico' %}"> | |||
<link rel="apple-touch-icon" href="{% media '/apple-touch-icon.png' %}"> | |||
{% endblock favicons %} | |||
{% block css %} | |||
<link rel="stylesheet" href="{% media '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 %} | |||
</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 %} |
@@ -0,0 +1,4 @@ | |||
body{ | |||
margin: 0 auto; | |||
width: 960px; | |||
} |
@@ -0,0 +1,9 @@ | |||
site: | |||
mode: development | |||
media: | |||
root: | |||
path: media # Relative path from site root (the directory where this file exists) | |||
url: /media | |||
widgets: | |||
plugins: | |||
aggregators: |
@@ -22,6 +22,16 @@ def test_name(): | |||
f = FS(__file__) | |||
assert f.name == os.path.basename(__file__) | |||
def test_equals(): | |||
f = FS('/blog/2010/december') | |||
g = FS('/blog/2010/december') | |||
h = FS('/blog/2010/december/') | |||
i = FS('/blog/2010/november') | |||
assert f == f.path | |||
assert f == g | |||
assert f == h | |||
assert f != i | |||
def test_name_without_extension(): | |||
f = File(__file__) | |||
assert f.name_without_extension == "test_fs" | |||
@@ -108,16 +118,23 @@ def test_ancestors_stop(): | |||
depth = 0 | |||
next = JINJA2 | |||
for folder in INDEX.ancestors(stop=TEMPLATE_ROOT.parent): | |||
print folder | |||
assert folder == next | |||
depth += 1 | |||
next = folder.parent | |||
assert depth == 2 | |||
def test_is_descendant_of(): | |||
assert INDEX.is_descendant_of(JINJA2) | |||
print "*" | |||
assert JINJA2.is_descendant_of(TEMPLATE_ROOT) | |||
print "*" | |||
assert INDEX.is_descendant_of(TEMPLATE_ROOT) | |||
print "*" | |||
assert not INDEX.is_descendant_of(DATA_ROOT) | |||
def test_fragment(): | |||
print INDEX.get_fragment(TEMPLATE_ROOT) | |||
assert INDEX.get_fragment(TEMPLATE_ROOT) == Folder(JINJA2.name).child(INDEX.name) | |||
assert INDEX.get_fragment(TEMPLATE_ROOT.parent) == Folder( | |||
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) | |||
def test_get_mirror(): | |||
@@ -0,0 +1,62 @@ | |||
# -*- coding: utf-8 -*- | |||
""" | |||
Use nose | |||
`$ pip install nose` | |||
`$ nosetests` | |||
""" | |||
from hyde.fs import File, Folder | |||
from hyde.site import Node, RootNode, Site | |||
TEST_SITE_ROOT = File(__file__).parent.child_folder('sites/test_jinja') | |||
def test_node_site(): | |||
s = Site(TEST_SITE_ROOT) | |||
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) | |||
assert r.site == s | |||
n = Node(r.source_folder.child_folder('blog'), r) | |||
assert n.site == s | |||
def test_node_root(): | |||
s = Site(TEST_SITE_ROOT) | |||
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) | |||
assert r.root == r | |||
n = Node(r.source_folder.child_folder('blog'), r) | |||
assert n.root == r | |||
def test_node_parent(): | |||
s = Site(TEST_SITE_ROOT) | |||
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) | |||
c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december')) | |||
assert c.parent == r.node_from_relative_path('blog/2010') | |||
def test_node_module(): | |||
s = Site(TEST_SITE_ROOT) | |||
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) | |||
assert not r.module | |||
n = r.add_node(TEST_SITE_ROOT.child_folder('content/blog')) | |||
assert n.module == n | |||
c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december')) | |||
assert c.module == n | |||
def test_node_relative_path(): | |||
s = Site(TEST_SITE_ROOT) | |||
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) | |||
assert not r.module | |||
n = r.add_node(TEST_SITE_ROOT.child_folder('content/blog')) | |||
assert n.relative_path == 'blog' | |||
c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december')) | |||
assert c.relative_path == 'blog/2010/december' | |||
def test_build(): | |||
s = Site(TEST_SITE_ROOT) | |||
s.build() | |||
path = 'blog/2010/december' | |||
node = s.content.node_from_relative_path(path) | |||
assert node | |||
assert Folder(node.relative_path) == Folder(path) | |||
path += '/merry-christmas.html' | |||
resource = s.content.resource_from_relative_path(path) | |||
assert resource | |||
assert resource.relative_path == path | |||
assert not s.content.resource_from_relative_path('/happy-festivus.html') |
@@ -1,4 +1,9 @@ | |||
from django.utils.html import strip_spaces_between_tags | |||
def strip_spaces_between_tags(value): | |||
""" | |||
Stolen from `django.util.html` | |||
Returns the given HTML with spaces between tags removed. | |||
""" | |||
return re.sub(r'>\s+<', '><', force_unicode(value)) | |||
def assert_html_equals(expected, actual, sanitize=None): | |||
expected = strip_spaces_between_tags(expected.strip()) | |||