Source code for shoop.xtheme.parsing

# -*- coding: utf-8 -*-
# This file is part of Shoop.
#
# Copyright (c) 2012-2016, Shoop Ltd. All rights reserved.
#
# This source code is licensed under the AGPLv3 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import unicode_literals

import pytoml as toml
import six
from jinja2.ext import Extension
from jinja2.nodes import Const, EvalContext, ExprStmt, Impossible, Name, Output
from jinja2.utils import contextfunction

from shoop.xtheme.rendering import render_placeholder
from shoop.xtheme.view_config import Layout


[docs]class Unflattenable(Exception): """ Exception raised when a node list can't be flattened into a constant. """
[docs]class NonConstant(ValueError): """ Exception raised when something expected to be constant... is not. """
[docs]class NestingError(ValueError): """ Exception raised when a template's placeholder/column/row/plugin hierarchy is out of whack. """
[docs]def flatten_const_node_list(environment, node_list): """ Try to flatten the given node list into a single string. :param environment: Jinja2 environment :type environment: jinja2.environment.Environment :param node_list: List of nodes :type node_list: list[jinja2.nodes.Node] :return: String of content :rtype: str :raise Unflattenable: Raised when the node list can't be flattened into a constant """ output = [] eval_ctx = EvalContext(environment) for node in node_list: if isinstance(node, Output): # pragma: no branch for node in node.nodes: try: const = node.as_const(eval_ctx=eval_ctx) if not isinstance(const, six.text_type): raise Unflattenable(const) output.append(const) except Impossible: raise Unflattenable(node) else: # Very unlikely, but you know. raise Unflattenable(node) # pragma: no cover return "".join(output)
[docs]def parse_constantlike(environment, parser): """ Parse the next expression as a "constantlike" expression. Expression trees that fold into constants are constantlike, as are bare variable names. :param environment: Jinja2 environment :type environment: jinja2.environment.Environment :param parser: Template parser :type parser: jinja2.parser.Parser :return: constant value of any type :rtype: object """ expr = parser.parse_expression() if isinstance(expr, Name): # bare names are accepted return expr.name try: return expr.as_const(EvalContext(environment)) except Impossible: raise NonConstant("Not constant: %r" % expr)
class _PlaceholderManagingExtension(Extension): """ Superclass (could be mixin) with helpers for getting the currently active layout object from a parser. """ def _get_layout(self, parser, accept_none=False): """ Get the currently managed Layout from the parser. :param parser: Template parser :type parser: jinja2.parser.Parser :param accept_none: Whether or not to accept the eventuality that there's no current layout. If False (the default), a `NestingError` is raised. :type accept_none: bool :return: The current layout :rtype: shoop.xtheme.view_config.Layout :raises NestingError: Raised if there's no current layout and that's not okay. """ cfg = getattr(parser, "_xtheme_placeholder_layout", None) if not accept_none and cfg is None: raise NestingError("No current `placeholder` block!") return cfg def _new_layout(self, parser, placeholder_name): """ Begin a new layout for the given placeholder in the parser. :param parser: Template parser :type parser: jinja2.parser.Parser :param placeholder_name: The name of the placeholder. :type placeholder_name: str :return: The new layout :rtype: shoop.xtheme.view_config.Layout :raises NestingError: Raised if there's a layout going on already. """ curr_layout = self._get_layout(parser, accept_none=True) if curr_layout is not None: raise NestingError( "Can't nest `placeholder`s! (Currently in %r, trying to start %r)" % ( curr_layout.placeholder_name, placeholder_name ) ) layout = Layout(placeholder_name=placeholder_name) parser._xtheme_placeholder_layout = layout return self._get_layout(parser) def _end_layout(self, parser): """ End the current layout in the parser and return the serialized contents. :param parser: Template parser :type parser: jinja2.parser.Parser :return: The serialized layout :rtype: dict """ layout = self._get_layout(parser) parser._xtheme_placeholder_layout = None return layout.serialize()
[docs]def noop_node(lineno): """ Return a no-op node (compiled into a single `0`). :param lineno: Line number for the node :type lineno: int :return: Node :rtype: jinja2.nodes.ExprStmt """ return ExprStmt(Const(0)).set_lineno(lineno)
[docs]class PlaceholderExtension(_PlaceholderManagingExtension): """ `PlaceholderExtension` manages `{% placeholder <NAME> %}` ... `{% endplaceholder %}`. * The `name` can be any Jinja2 expression that can be folded into a constant, with the addition of bare variable names such as `name` meaning the same as `"name"`. This makes it slightly easier to write templates. * The body of this block is actually discarded; only the inner `column`, `row` and `plugin` directives have any meaning. (A parser-time `Layout` object is created and populated during parsing of this block.) """ tags = set(['placeholder'])
[docs] def parse(self, parser): """ Parse a placeholder! :param parser: Template parser :type parser: jinja2.parser.Parser :return: Output node for rendering a placeholder. :rtype: jinja2.nodes.Output """ lineno = next(parser.stream).lineno placeholder_name = six.text_type(parse_constantlike(self.environment, parser)) self._new_layout(parser, placeholder_name) parser.parse_statements(['name:endplaceholder'], drop_needle=True) # Body parsing will have, as a side effect, populated the current layout layout = self._end_layout(parser) args = [ Const(placeholder_name), Const(layout), Const(parser.name) ] return Output([self.call_method('_render_placeholder', args)]).set_lineno(lineno)
@contextfunction def _render_placeholder(self, context, placeholder_name, layout, template_name): return render_placeholder( context, placeholder_name=placeholder_name, default_layout=layout, template_name=template_name )
[docs]class LayoutPartExtension(_PlaceholderManagingExtension): """ Parser for row and column tags. Syntax for the row and column tags is:: {% row %} {% column [SIZES] %}...{% endcolumn %} {% endrow %} * Rows map to `LayoutRow` objects and columns map to `LayoutCell`. * For a single-cell layout, these are not necessary. ``{% plugin %}`` invocations without preceding ``{% row %}``/``{% column %}`` directives imply a single row and a single column. """ tags = set(['column', 'row'])
[docs] def parse(self, parser): """ Parse a column or row. :param parser: Template parser :type parser: jinja2.parser.Parser :return: A null output node. :rtype: jinja2.nodes.Node """ start = next(parser.stream) lineno = start.lineno arg = None if parser.stream.current.type != "block_end": arg = parser.parse_expression() # Parse any expression cfg = self._get_layout(parser) if start.value == "row": self._begin_row(cfg, arg) elif start.value == "column": self._begin_column(cfg, arg) parser.parse_statements(["name:end%s" % start.value], drop_needle=True) # Body parsing is also a no-op here; the layout is populated already return noop_node(lineno)
def _begin_row(self, cfg, arg): if arg is not None: raise ValueError("`row`s do not take arguments at present (got %r)" % arg) cfg.begin_row() def _begin_column(self, cfg, arg): sizes = {} if arg is not None: try: sizes = arg.as_const(eval_ctx=EvalContext(self.environment)) except Impossible: raise ValueError("Invalid argument for `column`: %r" % arg) if not isinstance(sizes, dict): raise ValueError("Argument for `column` must be a dict: %r" % arg) cfg.begin_column(sizes)
[docs]class PluginExtension(_PlaceholderManagingExtension): """ Parser for plugin tags. Syntax for plugin tag is:: {% plugin <NAME> %}...{% endplugin %} * The (optional) body of the plugin block is expected to be a Jinja2 AST that can be folded into a constant. Generally this means a single block of text (``{% raw }``/``{% endraw %}`` is okay!). * The contents of the body, if set, must be valid `TOML markup <https://github.com/toml-lang/toml>`_. The TOML is parsed during Jinja2 parse time into a dict, which in turn is folded into the layout description object. This means only the initial parsing of the template incurs whatever performance hit there is in parsing TOML; the Jinja2 bccache should take care of the rest. """ tags = set(['plugin'])
[docs] def parse(self, parser): """ Parse a column or row. :param parser: Template parser :type parser: jinja2.parser.Parser :return: A null output node. :rtype: jinja2.nodes.Node """ lineno = next(parser.stream).lineno name = parse_constantlike(self.environment, parser) # Parse the plugin name. body = parser.parse_statements(['name:endplugin'], drop_needle=True) layout = self._get_layout(parser) config = None if body: try: config = flatten_const_node_list(self.environment, body) except Unflattenable as uf: raise NonConstant("A `plugin` block may only contain static layout (found: %r)" % uf.args[0]) config = toml.loads(config, "<%s.%s in %s>" % (layout.placeholder_name, name, parser.name)) layout.add_plugin(name, config) return noop_node(lineno)
EXTENSIONS = [ LayoutPartExtension, PlaceholderExtension, PluginExtension ]