Source code for core

import os
import shutil
import codecs
import os.path
import traceback
from functools import partial

from carcade.conf import settings
from carcade.i18n import get_translations
from carcade.environments import create_jinja2_env, create_assets_env
from carcade.utils import sort, paginate, read_context
from carcade.exceptions import UnknownPathException, UnknownOrderingException


[docs]class Node(object): """Node of the site tree. .. attribute:: name Name. .. attribute:: children List of child nodes. .. attribute:: source_dir Path of the directory reflected by this node. .. attribute:: parent Parent node. """ def __init__(self, source_dir, name): self.name = name self.children = [] self.source_dir = source_dir self.parent = None
[docs] def add_child(self, node): """Adds `node` to the child nodes.""" self.children.append(node) node.parent = self
[docs] def get_child(self, name): """First tries to find and return immediate child called `name`. Then continues search by calling `get_child` on every immediate child :class:`PageNode`. If nothing found, returns ``None``. """ for child in self.children: if child.name == name: return child for child in self.children: if not isinstance(child, PageNode): continue page_child = child.get_child(name) if page_child: return page_child
[docs] def find_descendant(self, path): """Returns descendant node identified by `path`. If nothing found, returns ``None``. """ if '/' in path: child_name, rest = path.split('/', 1) child = self.get_child(child_name) return child and child.find_descendant(rest) else: return self.get_child(path)
[docs] def get_slug(self, intermediate=False): """Returns node slug -- string used to build page URL. Slug can vary depend on where it's going to be used: in the middle of the URL (in that case `intermediate` is ``True``) or in the end (`intermediate` is ``False``). """ if self.name == 'ROOT' or self.get_path() == settings.DEFAULT_PAGE: return '' return self.name
[docs] def get_path(self): """Returns node path.""" names = [] node = self while node.parent: names.append(node.name) node = node.parent return '/'.join(reversed(names))
[docs] def get_slugs(self): """Returns ancestor nodes slugs ordered from top (root) to bottom (this node). """ slugs = [] intermediate = False node = self while node.parent: slugs.append(node.get_slug(intermediate=intermediate)) intermediate = True node = node.parent return reversed(slugs)
[docs]class PageNode(Node): """Represents pages created during automatic pagination (see :func:`paginate_tree`). Subclasses :class:`Node`. .. attribute:: index 1-based index. """ def __init__(self, source_dir, index): self.index = index super(PageNode, self).__init__(source_dir, settings.PAGE_NAME % index)
[docs] def get_slug(self, intermediate=False): """If node represents first page or if slug going to be used in the middle of the URL, returns empty string. Otherwise returns node name. """ if self.index == 1 or intermediate: return '' return super(PageNode, self).get_slug()
[docs]def create_tree(page_dir, page_name): """Creates tree that reflects the structure of `page_dir`.""" node = Node(page_dir, page_name) for subpage_name in os.listdir(page_dir): subpage_dir = os.path.join(page_dir, subpage_name) if os.path.isdir(subpage_dir): child = create_tree(subpage_dir, subpage_name) node.add_child(child) return node
[docs]def sort_tree(node, ordering_dict): """Recursively sorts the tree according to the `ordering_dict` -- a dictionary where keys are node paths and values are the following: 1. ``'alphabetically'``: children will be sorted by their names 2. ``names list``: children will be sorted in the order in which their names appear in the list (see :func:`utils.sort`) 3. ``callable``: will be called with list of children and must return it sorted. """ path = node.get_path() key = path or '*' ordering = ordering_dict.get(key) if ordering: if ordering == 'alphabetically': node.children.sort(key=lambda child: child.name) elif isinstance(ordering, list): node.children = sort(node.children, ordering, key=lambda child: child.name) elif callable(ordering): node.children = ordering(node.children) else: raise UnknownOrderingException(key) node.children = [sort_tree(child, ordering_dict) for child in node.children] return node
[docs]def paginate_tree(node, pagination_dict): """Recursively paginates tree according to the `pagination_dict` -- a dictionary where keys are node paths and values are the numbers of the items per page (let's denote it `n`). If `node` path is in `pagination_dict`, it's children detached from it, split into the chunks of size `n` and then each chunk attached back to the `node` through intermediate :class:`PageNode`. """ path = node.get_path() key = path or '*' items_per_page = pagination_dict.get(key) if items_per_page: pages = paginate(node.children, items_per_page) node.children = [] for index, page_items in enumerate(pages, start=1): page = PageNode(node.source_dir, index) for item in page_items: page.add_child(item) node.add_child(page) node.children = [ paginate_tree(child, pagination_dict) for child in node.children] return node
[docs]def fill_tree(node, language=None): """Recursively walks the tree and annotates each node with context. Calls :func:`utils.read_context` and combines it's result with the following data: * ``NAME``: `node.name`; * ``PATH``: `node.get_path()`; * ``LANGUAGE``: `language`; * ``CHILDREN``: list of the child contexts; * ``SIBLINGS``: list of the sibling contexts; * ``PARENT``: parent's context; * ``PREV_SIBLING``, ``NEXT_SIBLING``: adjacent siblings contexts. """ context = read_context(node.source_dir, language=language) child_contexts = [] for child in node.children: fill_tree(child, language=language) child_contexts.append(child.context) context.update({ 'NAME': node.name, 'PATH': node.get_path(), 'LANGUAGE': language, 'CHILDREN': child_contexts, }) node.context = context for index, child_context in enumerate(child_contexts): prev_sibling = None if index - 1 >= 0: prev_sibling = child_contexts[index - 1] next_sibling = None if index + 1 < len(child_contexts): next_sibling = child_contexts[index + 1] child_context.update({ 'PARENT': context, 'SIBLINGS': child_contexts, 'PREV_SIBLING': prev_sibling, 'NEXT_SIBLING': next_sibling, }) return node
[docs]def build_site(jinja2_env, build_dir, node, root=None): """Given the site tree, builds the site. The main steps are the following: 1. Build chidren subtrees; 2. Determine `index.html` directory (which is basically `build_dir`-related url of `node`); 3. Define which template to use based on a `LAYOUTS` setting; 4. Render template with node context and write result to `index.html`. """ for child in node.children: build_site(jinja2_env, build_dir, child, root=root or node) if node.name == 'ROOT': return path = node.get_path() url = url_for(root, path, language=node.context['LANGUAGE']) target_dir = os.path.join(build_dir, url.lstrip('/')) target_filename = os.path.join(target_dir, 'index.html') if os.path.exists(target_filename): return if not os.path.exists(target_dir): os.makedirs(target_dir) layout_key = path if isinstance(node, PageNode): layout_key = node.parent.get_path() template = jinja2_env.get_template(settings.LAYOUTS[layout_key]) template.stream(ROOT=root.context, **node.context).dump( target_filename, encoding='utf-8')
[docs]def url_for(root, path, language=None): """If page at `path` exists, returns it's root-relative URL; otherwise throws an exception. """ base_url = settings.BASE_URL if language and language != settings.DEFAULT_LANGUAGE: base_url += '%s/' % language node = root.find_descendant(path) if not node: raise UnknownPathException(path) slugs = node.get_slugs() if path != settings.DEFAULT_PAGE: cleaned_slugs = filter(bool, slugs) if cleaned_slugs: return base_url + '/'.join(cleaned_slugs) + '/' return base_url
[docs]def build_(source_dir, build_dir, static_dir, language=None): """ 1. Creates the tree from `source_dir` (:func:`create_tree`), sorts it (:func:`sort_tree`), paginates (:func:`paginate_tree`) and fills with contexts in given `language` (:func:`fill_tree`); 2. Tries to load translation from `./translations/<language>.po`; 3. Creates Jinja2 environment with webassets and i18n extensions and passes it to :func:`build_site`. """ source_path = lambda *args: os.path.join(source_dir, *args) tree = create_tree(source_path('pages'), 'ROOT') tree = sort_tree(tree, settings.ORDERING) tree = paginate_tree(tree, settings.PAGINATION) tree = fill_tree(tree, language=language) translations = None if language: translations_path = source_path('translations/%s.po' % language) if os.path.exists(translations_path): translations = get_translations(translations_path) assets_env = create_assets_env( source_path('static'), static_dir, settings.STATIC_URL, settings.BUNDLES) jinja2_env = create_jinja2_env( url_for=partial(url_for, tree), assets_env=assets_env, translations=translations) build_site(jinja2_env, build_dir, tree)
def build(source_dir, build_dir): static_dir = os.path.join(build_dir, settings.STATIC_URL.lstrip('/')) shutil.copytree(os.path.join(source_dir, 'static'), static_dir) try: if settings.LANGUAGES: for language in settings.LANGUAGES: build_(source_dir, build_dir, static_dir, language=language) else: build_(source_dir, static_dir, build_dir) except: shutil.rmtree(build_dir) raise