# -*- coding: utf-8 -*- """ Parses & holds information about the site to be generated. """ import os import fnmatch import urlparse from hyde.exceptions import HydeException from hyde.fs import FS, File, Folder from hyde.model import Config from hyde.util import getLoggerWithNullHandler from functools import wraps def path_normalized(f): @wraps(f) def wrapper(self, path): return f(self, str(path).replace('/', os.sep)) return wrapper logger = getLoggerWithNullHandler('hyde.engine') class Processable(object): """ A node or resource. """ def __init__(self, source): super(Processable, self).__init__() self.source = FS.file_or_folder(source) self.is_processable = True self.uses_template = True @property def name(self): """ The resource name """ return self.source.name def __repr__(self): return self.path @property def path(self): """ Gets the source path of this node. """ 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 self.site = node.site self._relative_deploy_path = None @property def relative_path(self): """ Gets the path relative to the root folder (Content) """ return self.source_file.get_relative_path(self.node.root.source_folder) @property def slug(self): #TODO: Add a more sophisticated slugify method return self.source.name_without_extension def get_relative_deploy_path(self): """ Gets the path where the file will be created after its been processed. """ return self._relative_deploy_path \ if self._relative_deploy_path \ else self.relative_path def set_relative_deploy_path(self, path): """ Sets the path where the file ought to be created after its been processed. """ self._relative_deploy_path = path self.site.content.resource_deploy_path_changed(self) relative_deploy_path = property(get_relative_deploy_path, set_relative_deploy_path) url = relative_deploy_path @property def full_url(self): """ Returns the full url for the resource. """ return self.site.full_url(self.relative_path) class Node(Processable): """ Represents any folder that is processed by hyde """ def __init__(self, source_folder, parent=None): super(Node, self).__init__(source_folder) 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 contains_resource(self, resource_name): """ Returns True if the given resource name exists as a file in this node's source folder. """ return File(self.source_folder.child(resource_name)).exists def get_resource(self, resource_name): """ Gets the resource if the given resource name exists as a file in this node's source folder. """ if self.contains_resource(resource_name): return self.root.resource_from_path( self.source_folder.child(resource_name)) return None 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 def walk(self): """ Walks the node, first yielding itself then yielding the child nodes depth-first. """ yield self for child in self.child_nodes: for node in child.walk(): yield node def walk_resources(self): """ Walks the resources in this hierarchy. """ for node in self.walk(): for resource in node.resources: yield resource @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) @property def url(self): return '/' + self.relative_path @property def full_url(self): return self.site.full_url(self.relative_path) 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.node_deploy_map = {} self.resource_map = {} self.resource_deploy_map = {} @path_normalized 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) @path_normalized 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))) @path_normalized 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) @path_normalized 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 resource_deploy_path_changed(self, resource): """ Handles the case where the relative deploy path of a resource has changed. """ self.resource_deploy_map[str(resource.relative_deploy_path)] = resource @path_normalized def resource_from_relative_deploy_path(self, relative_deploy_path): """ Gets the content resource whose deploy path maps to the given relative path. If no match is found it returns None. """ if relative_deploy_path in self.resource_deploy_map: return self.resource_deploy_map[relative_deploy_path] return self.resource_from_relative_path(relative_deploy_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.debug("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.debug("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.debug("Resource exists at [%s]" % resource.relative_path) return resource if not afile.is_descendant_of(self.source_folder): raise HydeException("The given file [%s] does not reside" " in this hierarchy [%s]" % (afile, self.source_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.debug("Added resource [%s] to [%s]" % (resource.relative_path, self.source_folder)) return resource def load(self): """ Walks the `source_folder` and loads 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.walker as walker: @walker.folder_visitor def visit_folder(folder): self.add_node(folder) @walker.file_visitor def visit_file(afile): for pattern in self.site.config.ignore: if fnmatch.fnmatch(afile.name, pattern): return self.add_resource(afile) class Site(object): """ Represents the site to be generated. """ def __init__(self, sitepath=None, config=None): super(Site, self).__init__() self.sitepath = Folder(Folder(sitepath).fully_expanded_path) self.config = config if config else Config(self.sitepath) self.content = RootNode(self.config.content_root_path, self) self.plugins = [] self.context = {} def load(self): """ Walks the content and media folders to load up the sitemap. """ self.content.load() def content_url(self, path): """ Returns the content url by appending the base url from the config with the given path. """ return Folder(self.config.base_url).child(path).replace(os.sep, '/') def media_url(self, path): """ Returns the media url by appending the media base url from the config with the given path. """ return Folder(self.config.media_url).child(path).replace(os.sep, '/') def full_url(self, path): """ Determines if the given path is media or content based on the configuration and returns the appropriate url. """ if urlparse.urlparse(path)[:2] != ("",""): return path if self.is_media(path): relative_path = File(path).get_relative_path( Folder(self.config.media_root)) return self.media_url(relative_path) else: return self.content_url(path) def is_media(self, path): """ Given the relative path, determines if it is content or media. """ folder = self.content.source.child_folder(path) return folder.is_descendant_of(self.config.media_root_path)