from jinja2 import Environment
-print Environment().from_string("""\
-{{ foo.bar }}
+print Environment(parser_extensions=['jinja2.i18n.trans']).from_string("""\
{% trans %}Hello {{ user }}!{% endtrans %}
{% trans count=users|count %}{{ count }} user{% pluralize %}{{ count }} users{% endtrans %}
""").render(user="someone")
from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \
DictLoader
from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined
+from jinja2.filters import environmentfilter, contextfilter
from jinja2.utils import Markup, escape
switch (*inp++) {
case '&':
len += 5;
- erepl++;
+ ++erepl;
break;
case '"':
len += 6;
- erepl++;
+ ++erepl;
break;
case '<':
case '>':
len += 4;
- erepl++;
+ ++erepl;
break;
default:
- len++;
+ ++len;
}
/* Do we need to escape anything at all? */
case '&':
Py_UNICODE_COPY(outp, amp, 5);
outp += 5;
- repl++;
+ ++repl;
break;
case '"':
Py_UNICODE_COPY(outp, qt, 6);
outp += 6;
- repl++;
+ ++repl;
break;
case '<':
Py_UNICODE_COPY(outp, lt, 4);
outp += 4;
- repl++;
+ ++repl;
break;
case '>':
Py_UNICODE_COPY(outp, gt, 4);
outp += 4;
- repl++;
+ ++repl;
break;
default:
*outp++ = *inp;
static PyObject*
-escape(PyObject *self, PyObject *args)
+soft_unicode(PyObject *self, PyObject *s)
{
- PyObject *text = NULL, *s = NULL, *rv = NULL;
- if (!PyArg_UnpackTuple(args, "escape", 1, 1, &text))
- return NULL;
+ if (!PyUnicode_Check(s))
+ return PyObject_Unicode(s);
+ Py_INCREF(s);
+ return s;
+}
+
+
+static PyObject*
+escape(PyObject *self, PyObject *text)
+{
+ PyObject *s = NULL, *rv = NULL;
/* we don't have to escape integers, bools or floats */
if (PyInt_CheckExact(text) || PyLong_CheckExact(text) ||
PyFloat_CheckExact(text) || PyBool_Check(text) ||
text == Py_None) {
- args = PyTuple_New(1);
+ PyObject *args = PyTuple_New(1);
if (!args) {
Py_DECREF(s);
return NULL;
s = escape_unicode((PyUnicodeObject*)text);
/* convert the unicode string into a markup object. */
- args = PyTuple_New(1);
- if (!args) {
- Py_DECREF(s);
- return NULL;
- }
- PyTuple_SET_ITEM(args, 0, (PyObject*)s);
- rv = PyObject_CallObject(markup, args);
- Py_DECREF(args);
- return rv;
+ return PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
}
static PyMethodDef module_methods[] = {
- {"escape", (PyCFunction)escape, METH_VARARGS,
+ {"escape", (PyCFunction)escape, METH_O,
"escape(s) -> string\n\n"
"Convert the characters &, <, >, and \" in string s to HTML-safe\n"
"sequences. Use this if you need to display text that might contain\n"
"such characters in HTML."},
+ {"soft_unicode", (PyCFunction)soft_unicode, METH_O,
+ "soft_unicode(object) -> string\n\n"
+ "Make a string unicode if it isn't already. That way a markup\n"
+ "string is not converted back to unicode."},
{"tb_set_next", (PyCFunction)tb_set_next, METH_VARARGS,
"Set the tb_next member of a traceback object."},
{NULL, NULL, 0, NULL} /* Sentinel */
# the current indentation
self._indentation = 0
+ def get_visitor(self, node):
+ """Custom nodes have their own compilation method."""
+ if isinstance(node, nodes.CustomStmt):
+ return node.compile
+ return NodeVisitor.get_visitor(self, node)
+
def temporary_identifier(self):
"""Get a new unique identifier."""
self._last_identifier += 1
from jinja2.exceptions import TemplateSyntaxError, TemplateRuntimeError
-_missing = object()
-
-
class Token(tuple):
"""
Token class.
return self.type
elif self.type in reverse_operators:
return reverse_operators[self.type]
- return self.value
+ return '%s:%s' % (self.type, self.value)
+
+ def test(self, expr):
+ """Test a token against a token expression. This can either be a
+ token type or 'token_type:token_value'. This can only test against
+ string values!
+ """
+ # here we do a regular string equality check as test_many is usually
+ # passed an iterable of not interned strings.
+ if self.type == expr:
+ return True
+ elif ':' in expr:
+ return expr.split(':', 1) == [self.type, self.value]
+ return False
+
+ def test_many(self, iterable):
+ """Test against multiple token expressions."""
+ for expr in iterable:
+ if self.test(expr):
+ return True
+ return False
def __repr__(self):
return 'Token(%r, %r, %r)' % (
self.current = Token(self.current.lineno, 'eof', '')
self._next = None
- def expect(self, token_type, token_value=_missing):
+ def expect(self, expr):
"""Expect a given token type and return it"""
- if self.current.type is not token_type:
+ if not self.current.test(expr):
raise TemplateSyntaxError("expected token %r, got %r" %
- (token_type, self.current.type),
- self.current.lineno,
- self.filename)
- elif token_value is not _missing and \
- self.current.value != token_value:
- raise TemplateSyntaxError("expected %r, got %r" %
- (token_value, self.current.value),
+ (expr, self.current),
self.current.lineno,
self.filename)
try:
"""
import sys
from jinja2.lexer import Lexer
-from jinja2.parser import Parser
+from jinja2.parser import Parser, ParserExtension
from jinja2.optimizer import optimize
from jinja2.compiler import generate
from jinja2.runtime import Undefined
from jinja2.debug import translate_exception
+from jinja2.utils import import_string
from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE
optimized=True,
undefined=Undefined,
loader=None,
+ parser_extensions=(),
finalize=unicode):
"""Here the possible initialization parameters:
`undefined` a subclass of `Undefined` that is used to
represent undefined variables.
`loader` the loader which should be used.
+ `parser_extensions` List of parser extensions to use.
+ `finalize` A callable that finalizes the variable. Per
+ default this is `unicode`, other useful
+ builtin finalizers are `escape`.
========================= ============================================
"""
self.comment_end_string = comment_end_string
self.line_statement_prefix = line_statement_prefix
self.trim_blocks = trim_blocks
+ self.parser_extensions = {}
+ for extension in parser_extensions:
+ if isinstance(extension, basestring):
+ extension = import_string(extension)
+ self.parser_extensions[extension.tag] = extension
+
+
+ # runtime information
self.undefined = undefined
self.optimized = optimized
self.finalize = finalize
return f
+def do_forceescape(value):
+ """Enforce HTML escaping. This will probably double escape variables."""
+ if hasattr(value, '__html__'):
+ value = value.__html__()
+ return escape(unicode(value))
+
+
def do_replace(s, old, new, count=None):
"""Return a copy of the value with all occurrences of a substring
replaced with a new one. The first argument is the substring
try:
return int(value)
except (TypeError, ValueError):
+ # this quirk is necessary so that "42.23"|int gives 42.
try:
return int(float(value))
except (TypeError, ValueError):
def do_striptags(value):
"""Strip SGML/XML tags and replace adjacent whitespace by one space.
"""
- return ' '.join(_striptags_re.sub('', value).split())
+ if hasattr(value, '__html__'):
+ value = value.__html__()
+ return u' '.join(_striptags_re.sub('', value).split())
def do_slice(value, slices, fill_with=None):
'lower': do_lower,
'escape': escape,
'e': escape,
- 'xmlattr': do_xmlattr,
+ 'forceescape': do_forceescape,
'capitalize': do_capitalize,
'title': do_title,
'default': do_default,
:copyright: Copyright 2008 by Armin Ronacher.
:license: BSD.
"""
+from collections import deque
from jinja2 import nodes
-from jinja2.parser import _statement_end_tokens
+from jinja2.environment import Environment
+from jinja2.parser import ParserExtension, statement_end_tokens
from jinja2.exceptions import TemplateAssertionError
+# the only real useful gettext functions for a Jinja template. Note
+# that ugettext must be assigned to gettext as Jinja doesn't support
+# non unicode strings.
+GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext')
+
+
+def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS):
+ """Extract localizable strings from the given template node.
+
+ For every string found this function yields a ``(lineno, function,
+ message)`` tuple, where:
+
+ * ``lineno`` is the number of the line on which the string was found,
+ * ``function`` is the name of the ``gettext`` function used (if the
+ string was extracted from embedded Python code), and
+ * ``message`` is the string itself (a ``unicode`` object, or a tuple
+ of ``unicode`` objects for functions with multiple string arguments).
+ """
+ for call in node.find_all(nodes.Call):
+ if not isinstance(node.node, nodes.Name) or \
+ node.node.name not in gettext_functions:
+ continue
+
+ strings = []
+ for arg in node.args:
+ if isinstance(arg, nodes.Const) and \
+ isinstance(arg.value, basestring):
+ strings.append(arg)
+ else:
+ strings.append(None)
+
+ if len(strings) == 1:
+ strings = strings[0]
+ else:
+ strings = tuple(strings)
+ yield node.lineno, node.node.name, strings
+
+
+def babel_extract(fileobj, keywords, comment_tags, options):
+ """Babel extraction method for Jinja templates.
+
+ :param fileobj: the file-like object the messages should be extracted from
+ :param keywords: a list of keywords (i.e. function names) that should be
+ recognized as translation functions
+ :param comment_tags: a list of translator tags to search for and include
+ in the results. (Unused)
+ :param options: a dictionary of additional options (optional)
+ :return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
+ (comments will be empty currently)
+ """
+ encoding = options.get('encoding', 'utf-8')
+ environment = Environment(
+ options.get('block_start_string', '{%'),
+ options.get('block_end_string', '%}'),
+ options.get('variable_start_string', '{{'),
+ options.get('variable_end_string', '}}'),
+ options.get('comment_start_string', '{#'),
+ options.get('comment_end_string', '#}'),
+ options.get('line_statement_prefix') or None,
+ options.get('trim_blocks', '').lower() in ('1', 'on', 'yes', 'true')
+ )
+ node = environment.parse(fileobj.read().decode(encoding))
+ for lineno, func, message in extract_from_ast(node, keywords):
+ yield lineno, func, message, []
+
+
def parse_trans(parser):
"""Parse a translatable tag."""
- lineno = parser.stream.expect('trans').lineno
+ lineno = parser.stream.next().lineno
# skip colon for python compatibility
if parser.stream.current.type is 'colon':
plural_expr = nodes.Name(singular_names[0], 'load')
# if we have a pluralize block, we parse that too
- if parser.stream.current.type is 'pluralize':
+ if parser.stream.current.test('name:pluralize'):
have_plural = True
parser.stream.next()
if parser.stream.current.type is not 'block_end':
parser.stream.expect('variable_end')
elif parser.stream.current.type is 'block_begin':
parser.stream.next()
- if parser.stream.current.type is 'endtrans':
+ if parser.stream.current.test('name:endtrans'):
break
- elif parser.stream.current.type is 'pluralize':
+ elif parser.stream.current.test('name:pluralize'):
if allow_pluralize:
break
raise TemplateSyntaxError('a translatable section can have '
def _make_node(singular, plural, variables, plural_expr):
- """Generates a useful node from the data provided."""
+ """Generates a useful node from the data provided. The nodes won't have
+ a line number information so the caller must set it via `set_lineno` later.
+ """
# singular only:
if plural_expr is None:
gettext = nodes.Name('gettext', 'load')
if variables:
node = nodes.Mod(node, variables)
return nodes.Output([node])
+
+
+trans = ParserExtension('trans', parse_trans)
# set of used keywords
keywords = set(['and', 'block', 'elif', 'else', 'endblock', 'print',
'endfilter', 'endfor', 'endif', 'endmacro', 'endraw',
- 'endtrans', 'extends', 'filter', 'for', 'if', 'in',
- 'include', 'is', 'macro', 'not', 'or', 'pluralize', 'raw',
- 'recursive', 'set', 'trans', 'call', 'endcall'])
+ 'extends', 'filter', 'for', 'if', 'in',
+ 'include', 'is', 'macro', 'not', 'or', 'raw',
+ 'recursive', 'set', 'call', 'endcall'])
# bind operators to token types
operators = {
storage.extend(d.get(attr, ()))
assert len(storage) == len(set(storage))
d[attr] = tuple(storage)
- return type.__new__(cls, name, bases, d)
+ rv = type.__new__(cls, name, bases, d)
+
+ # unless the node is a subclass of `CustomNode` it may not
+ # be defined in any other module than the jinja2.nodes module.
+ # the reason for this is that the we don't want users to take
+ # advantage of the fact that the parser is using the node name
+ # only as callback name for non custom nodes. This could lead
+ # to broken code in the future and is disallowed because of this.
+ if rv.__module__ != 'jinja2.nodes' and not \
+ isinstance(rv, CustomStmt):
+ raise TypeError('non builtin node %r is not a subclass of '
+ 'CustomStmt.' % node.__class__.__name__)
+ return rv
class Node(object):
"""Nodes that exist in a specific context only."""
+class CustomStmt(Stmt):
+ """Custom statements must extend this node."""
+
+ def compile(self, compiler):
+ """The compiler calls this method to get the python sourcecode
+ for the statement.
+ """
+
+
class Template(Node):
"""Node that represents a template."""
fields = ('body',)
:copyright: 2008 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
+from operator import itemgetter
from jinja2 import nodes
from jinja2.exceptions import TemplateSyntaxError
_statement_keywords = frozenset(['for', 'if', 'block', 'extends', 'print',
- 'macro', 'include', 'trans'])
+ 'macro', 'include'])
_compare_operators = frozenset(['eq', 'ne', 'lt', 'lteq', 'gt', 'gteq', 'in'])
-_statement_end_tokens = set(['elif', 'else', 'endblock', 'endfilter',
+statement_end_tokens = set(['elif', 'else', 'endblock', 'endfilter',
'endfor', 'endif', 'endmacro', 'variable_end',
'in', 'recursive', 'endcall', 'block_end'])
self.filename = filename
self.closed = False
self.stream = environment.lexer.tokenize(source, filename)
+ self.extensions = environment.parser_extensions
def end_statement(self):
"""Make sure that the statement ends properly."""
if self.stream.current.type is 'semicolon':
self.stream.next()
- elif self.stream.current.type not in _statement_end_tokens:
+ elif self.stream.current.type not in statement_end_tokens:
raise TemplateSyntaxError('ambigous end of statement',
self.stream.current.lineno,
self.filename)
return self.parse_call_block()
elif token_type is 'filter':
return self.parse_filter_block()
+ elif token_type is 'name':
+ ext = self.extensions.get(self.stream.current.value)
+ if ext is not None:
+ return ext(self)
lineno = self.stream.current
expr = self.parse_tuple()
if self.stream.current.type == 'assign':
return nodes.Assign(target, expr, lineno=lineno)
def parse_statements(self, end_tokens, drop_needle=False):
- """
- Parse multiple statements into a list until one of the end tokens
- is reached. This is used to parse the body of statements as it
- also parses template data if appropriate.
+ """Parse multiple statements into a list until one of the end tokens
+ is reached. This is used to parse the body of statements as it also
+ parses template data if appropriate.
"""
# the first token may be a colon for python compatibility
if self.stream.current.type is 'colon':
result = self.subparse(end_tokens)
else:
result = []
- while self.stream.current.type not in end_tokens:
+ while not self.stream.current.test_many(end_tokens):
if self.stream.current.type is 'block_end':
self.stream.next()
result.extend(self.subparse(end_tokens))
def parse_print(self):
node = nodes.Output(lineno=self.stream.expect('print').lineno)
node.nodes = []
- while self.stream.current.type not in _statement_end_tokens:
+ while self.stream.current.type not in statement_end_tokens:
if node.nodes:
self.stream.expect('comma')
node.nodes.append(self.parse_expression())
self.end_statement()
return node
- def parse_trans(self):
- """Parse a translatable section."""
- # lazily imported because we don't want the i18n overhead
- # if it's not used. (Even though the overhead is low)
- from jinja2.i18n import parse_trans
- return parse_trans(self)
-
def parse_expression(self, no_condexpr=False):
"""Parse an expression."""
if no_condexpr:
while 1:
if args:
self.stream.expect('comma')
- if self.stream.current.type in _statement_end_tokens:
+ if self.stream.current.type in statement_end_tokens:
break
args.append(parse())
if self.stream.current.type is not 'comma':
elif token.type is 'variable_begin':
self.stream.next()
want_comma = False
- while self.stream.current.type not in _statement_end_tokens:
+ while not self.stream.current.test_many(statement_end_tokens):
if want_comma:
self.stream.expect('comma')
add_data(self.parse_expression())
flush_data()
self.stream.next()
if end_tokens is not None and \
- self.stream.current.type in end_tokens:
+ self.stream.current.test_many(end_tokens):
return body
while self.stream.current.type is not 'block_end':
body.append(self.parse_statement())
result = nodes.Template(self.subparse(), lineno=1)
result.set_environment(self.environment)
return result
+
+
+class ParserExtension(tuple):
+ """Instances of this class store parser extensions."""
+ __slots__ = ()
+
+ def __new__(cls, tag, parse_func):
+ return tuple.__new__(cls, (tag, parse_func))
+
+ def __call__(self, parser):
+ return self.parse_func(parser)
+
+ def __repr__(self):
+ return '<%s %r>' % (self.__class__.__name__, self.tag)
+
+ tag = property(itemgetter(0))
+ parse_func = property(itemgetter(1))
self._rendered_body = u''.join(gen)
self._context = context.get_exported()
- def __getitem__(self, name):
- return self._context[name]
-
- def __unicode__(self):
- return self._rendered_body
-
- def __html__(self):
- return self._rendered_body
+ __getitem__ = lambda x, n: x._context[n]
+ __html__ = __unicode__ = lambda x: x._rendered_body
def __repr__(self):
return '<%s %r>' % (
return value % 2 == 0
+def test_divisibleby(value, num):
+ """Check if a variable is divisible by a number."""
+ return value % num == 0
+
+
def test_defined(value):
"""Return true if the variable is defined:
.. sourcecode:: jinja
- {% if foo.attribute is sameas(false) %}
+ {% if foo.attribute is sameas false %}
the foo attribute really is the `False` singleton
{% endif %}
"""
TESTS = {
'odd': test_odd,
'even': test_even,
+ 'divisibleby': test_divisibleby,
'defined': test_defined,
'lower': test_lower,
'upper': test_upper,
_simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
-def soft_unicode(s):
- """Make a string unicode if it isn't already. That way a markup
- string is not converted back to unicode.
+def import_string(import_name, silent=False):
+ """Imports an object based on a string. This use useful if you want to
+ use import paths as endpoints or something similar. An import path can
+ be specified either in dotted notation (``xml.sax.saxutils.escape``)
+ or with a colon as object delimiter (``xml.sax.saxutils:escape``).
+
+ If the `silent` is True the return value will be `None` if the import
+ fails.
+
+ :return: imported object
"""
- if not isinstance(s, unicode):
- s = unicode(s)
- return s
+ try:
+ if ':' in import_name:
+ module, obj = import_name.split(':', 1)
+ elif '.' in import_name:
+ items = import_name.split('.')
+ module = '.'.join(items[:-1])
+ obj = items[-1]
+ else:
+ return __import__(import_name)
+ return getattr(__import__(module, None, None, [obj]), obj)
+ except (ImportError, AttributeError):
+ if not silent:
+ raise
def pformat(obj, verbose=False):
happen. If you want to use autoescaping in Jinja just set the finalizer
of the environment to `escape`.
"""
-
__slots__ = ()
def __html__(self):
def join(self, seq):
return self.__class__(unicode.join(self, imap(escape, seq)))
+ join.__doc__ = unicode.join.__doc__
def split(self, *args, **kwargs):
return map(self.__class__, unicode.split(self, *args, **kwargs))
+ split.__doc__ = unicode.split.__doc__
def rsplit(self, *args, **kwargs):
return map(self.__class__, unicode.rsplit(self, *args, **kwargs))
+ rsplit.__doc__ = unicode.rsplit.__doc__
def splitlines(self, *args, **kwargs):
return map(self.__class__, unicode.splitlines(self, *args, **kwargs))
+ splitlines.__doc__ = unicode.splitlines.__doc__
def make_wrapper(name):
orig = getattr(unicode, name)
# we have to import it down here as the speedups module imports the
# markup type which is define above.
try:
- from jinja2._speedups import escape
+ from jinja2._speedups import escape, soft_unicode
except ImportError:
def escape(obj):
"""Convert the characters &, <, >, and " in string s to HTML-safe
.replace('<', '<')
.replace('"', '"')
)
+
+ def soft_unicode(s):
+ """Make a string unicode if it isn't already. That way a markup
+ string is not converted back to unicode.
+ """
+ if not isinstance(s, unicode):
+ s = unicode(s)
+ return s
# in form of html and txt files it's a better idea to extract the files
zip_safe=False,
classifiers=[
- 'Development Status :: 1 - Alpha',
+ 'Development Status :: 4 Beta',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
]
)
},
- extras_require={'plugin': ['setuptools>=0.6a2']}
+ extras_require={'i18n': ['Babel>=0.8']}
+ entry_points="""
+ [babel.extractors]
+ jinja2 = jinja.i18n:babel_extract[i18n]
+ """
)