diff options
-rw-r--r-- | doc/build/content/filtering.txt | 109 | ||||
-rw-r--r-- | doc/build/content/syntax.txt | 14 | ||||
-rw-r--r-- | doc/build/content/usage.txt | 23 | ||||
-rw-r--r-- | doc/build/genhtml.py | 1 | ||||
-rw-r--r-- | doc/build/templates/base.html | 2 | ||||
-rw-r--r-- | doc/docs.css | 2 | ||||
-rw-r--r-- | lib/mako/codegen.py | 5 | ||||
-rw-r--r-- | lib/mako/filters.py | 15 | ||||
-rw-r--r-- | lib/mako/lexer.py | 2 | ||||
-rw-r--r-- | lib/mako/parsetree.py | 3 | ||||
-rw-r--r-- | test/alltests.py | 1 | ||||
-rw-r--r-- | test/filters.py | 27 | ||||
-rw-r--r-- | test/template.py | 6 |
13 files changed, 174 insertions, 36 deletions
diff --git a/doc/build/content/filtering.txt b/doc/build/content/filtering.txt new file mode 100644 index 0000000..90f6624 --- /dev/null +++ b/doc/build/content/filtering.txt @@ -0,0 +1,109 @@ +Filtering and Buffering {@name=filtering} +================================= + +### Expression Filtering + +As described in the Syntax chapter, the "`|`" operator can be applied to a "`${}`" expression to apply escape filters to the output: + + ${"this is some text" | u} + +The above expression applies URL escaping to the expression, and produces `this+is+some+text`. + +The built-in escape flags are: + +* `u` : URL escaping, provided by `urllib.quote_plus(string.encode('utf-8'))` +* `h` : HTML escaping, provided by `cgi.escape(string, True)` +* `x` : XML escaping +* `trim` : whitespace trimming, provided by `string.strip()` +* `entity` : produces HTML entity references for applicable strings, derived from `htmlentitydefs` + +To apply more than one filter, separate them by a comma: + + ${" <tag>some value</tag> " | h,trim} + +The above produces `<tag>some value</tag>`, with no leading or trailing whitespace. The HTML escaping function is applied first, the "trim" function second. + +Naturally, you can make your own filters too. A filter is just a Python function that accepts a single string argument, and returns the filtered result. The expressions after the `|` operator draw upon the local namespace of the template in which they appear, meaning you can define escaping functions locally: + + <%! + def myescape(text): + return "<TAG>" + text + "</TAG>" + %> + + Heres some tagged text: ${"text" | myescape} + +Or from any Python module: + + <%! + import myfilters + %> + + Heres some tagged text: ${"text" | myfilters.tagfilter} + +A page can apply a default set of filters to all expression tags using the `expression_filter` argument to the `%page` tag: + + <%page expression_filter="h"/> + + Escaped text: ${"<html>some html</html>"} + +Result: + + Escaped text: <html>some html</html> + +### Filtering Defs + +The `%def` tag has a filter argument which will apply the given list of filter functions to the output of the `%def`: + + <%def name="foo()" filter="h, trim"> + <b>this is bold</b> + </%def> + +When the filter attribute is applied to a def as above, the def is automatically **buffered** as well. This is described next. + +### Buffering + +One of Mako's central design goals is speed. To this end, all of the textual content within a template and its various callables is by default piped directly to the single buffer that is stored within the `Context` object. While this normally is easy to miss, it has certain side effects. The main one is that when you call a def using the normal expression syntax, i.e. `${somedef()}`, it may appear that the return value of the function is the content it produced, which is then delivered to your template just like any other expression substitution, except that normally, this is not the case; the return value of `${somedef()}` is simply the empty string `''`. By the time you receive this empty string, the output of `somedef()` has been sent to the underlying buffer. + +You may not want this effect, if for example you are doing something like this: + + ${" results " + somedef() + " more results "} + +If the `somedef()` function produced the content "`somedef's results`", the above template would produce this output: + + somedef's results results more results + +This is because `somedef()` fully executes before the expression returns the results of its concatenation; the concatenation in turn receives just the empty string as its middle expression. + +Mako provides two ways to work around this. One is by applying buffering to the `%def` itself: + + <%def name="somedef()" buffered="true"> + somedef's results + </%def> + +The above definition will generate code similar to this: + + def somedef(): + context.push_buffer() + try: + context.write("somedef's results") + finally: + buf = context.pop_buffer() + return buf.getvalue() + +So that the content of `somedef()` is sent to a second buffer, which is then popped off the stack and its value returned. The speed hit inherent in buffering the output of a def is also apparent. + +Note that the `filter` argument on %def also causes the def to be buffered. This is so that the final content of the %def can be delivered to the escaping function in one batch, which reduces method calls and also produces more deterministic behavior for the filtering function itself, which can possibly be useful for a filtering function that wishes to apply a transformation to the text as a whole. + +The other way to buffer the output of a def or any Mako callable is by using the built-in `capture` function. This function performs an operation similar to the above buffering operation except it is specified by the caller. + + ${" results " + capture(somedef) + " more results "} + +Note that the first argument to the `capture` function is **the function itself**, not the result of calling it. This is because the `capture` function takes over the job of actually calling the target function, after setting up a buffered environment. To send arguments to the function, just send them to `capture` instead: + + ${capture(somedef, 17, 'hi', use_paging=True)} + +The above call is equivalent to the unbuffered call: + + ${somedef(17, 'hi', use_paging=True)} + +
\ No newline at end of file diff --git a/doc/build/content/syntax.txt b/doc/build/content/syntax.txt index 3f4deb2..d15a521 100644 --- a/doc/build/content/syntax.txt +++ b/doc/build/content/syntax.txt @@ -5,17 +5,17 @@ A Mako template is parsed from a text stream containing any kind of content, XML ### Expression Substiution -The simplest expression is just a variable substitution. The syntax for this is the `${}` construct, which is inspired by Perl, Genshi, JSP EL, and many others: +The simplest expression is just a variable substitution. The syntax for this is the `${}` construct, which is inspired by Perl, Genshi, JSP EL, and others: this is x: ${x} -Above, the string representation of `x` is applied to the template's output stream. If you're wondering where `x` comes from, it usually would have been supplied to the template's context when the template is rendered. If `x` was not supplied to the template and was not otherwise assigned locally, it evaluates to a special value `UNDEFINED`. More on that later. +Above, the string representation of `x` is applied to the template's output stream. If you're wondering where `x` comes from, its usually from the `Context` supplied to the template's rendering function. If `x` was not supplied to the template and was not otherwise assigned locally, it evaluates to a special value `UNDEFINED`. More on that later. The contents within the `${}` tag are evaluated by Python directly, so full expressions are OK: pythagorean theorem: ${pow(x,2) + pow(y,2)} -The results of the expression are evaluated into a string result in all cases before being rendered to the output stream, so our above example which produces a numeric result is OK. +The results of the expression are evaluated into a string result in all cases before being rendered to the output stream, such as the above example where the expression produces a numeric result. #### Expression Escaping @@ -23,13 +23,9 @@ Mako includes a number of built-in escaping mechanisms, including HTML, URI and ${"this is some text" | u} -The above expression applies URL escaping to the expression, and produces `this+is+some+text`. the `u` name indicates URL escaping, whereas `h` represents HTML escaping, `x` represents XML escaping, and `trim` applies a trim function: +The above expression applies URL escaping to the expression, and produces `this+is+some+text`. The `u` name indicates URL escaping, whereas `h` represents HTML escaping, `x` represents XML escaping, and `trim` applies a trim function. - ${" <tag>some value</tag> " | h,trim} - -The above produces `<tag>some value</tag>`. The HTML escaping function is applied first, the "trim" function second, which trims the leading and trailing whitespace from the expression. - -Naturally, you can make your own expression filters - that's described in [filtering](rel:filtering). +Read more about built in filtering functions, including how to make your own filter functions, in [filtering](rel:filtering). ### Control Structures diff --git a/doc/build/content/usage.txt b/doc/build/content/usage.txt index bd4c7b4..e4624e8 100644 --- a/doc/build/content/usage.txt +++ b/doc/build/content/usage.txt @@ -10,9 +10,9 @@ The most basic way to create a template and render it is through the `Template` mytemplate = Template("hello world!") print mytemplate.render() -Above, the text argument to `Template` is **compiled** into a Python module representation. This module contains a function called `render_body()`, which when executed produces the output of the template. When you call `mytemplate.render()`, Mako sets up a runtime environment for the template and calls the `render_body()` method, capturing the output into a buffer and returning it's string contents. +Above, the text argument to `Template` is **compiled** into a Python module representation. This module contains a function called `render_body()`, which produces the output of the template. When `mytemplate.render()` is called, Mako sets up a runtime environment for the template and calls the `render_body()` function, capturing the output into a buffer and returning it's string contents. -The code inside the `render_body()` method has access to a namespace of variables. You can specify these variables by sending them as additional keyword arguments to the `render()` method: +The code inside the `render_body()` function has access to a namespace of variables. You can specify these variables by sending them as additional keyword arguments to the `render()` method: from mako.template import Template @@ -74,15 +74,20 @@ Usually, an application will store most or all of its templates as text files on In the example above, we create a `TemplateLookup` which will look for templates in the `/docs` directory, and will store generated module files in the `/tmp/mako_modules` directory. The lookup locates templates by appending the given URI to each of its search directories; so if you gave it a URI of `/etc/beans/info.txt`, it would search for the file `/docs/etc/beans/info.txt`, else raise a `TopLevelNotFound` exception, which is a custom Mako exception. -The `TemplateLookup` also serves the important need of caching a fixed set of templates in memory at a given time, so that successive URI-lookups do not result in full filesystem lookups each time. By default, the `TemplateLookup` size is unbounded. You can specify a fixed size using the `collection_size` argument: +When the lookup locates templates, it will also assign a `uri` property to the `Template` which is the uri passed to the `get_template()` call. `Template` uses this uri to calculate the name of its module file. So in the above example, a `templatename` argument of `/etc/beans/info.txt` will create a module file `/tmp/mako_modules/etc/beans/info.txt.py`. - mylookup = TemplateLookup(directories=['/docs'], module_directory='/tmp/mako_modules', collection_size=500) +#### Setting the Collection Size {@name=size} + +The `TemplateLookup` also serves the important need of caching a fixed set of templates in memory at a given time, so that successive uri lookups do not result in full template compilations and/or module reloads on each request. By default, the `TemplateLookup` size is unbounded. You can specify a fixed size using the `collection_size` argument: + + mylookup = TemplateLookup(directories=['/docs'], + module_directory='/tmp/mako_modules', collection_size=500) The above lookup will continue to load templates into memory until it reaches a count of around 500. At that point, it will clean out a certain percentage of templates using a **least recently used** scheme. -Another important flag on `TemplateLookup` is `filesystem_checks`. This defaults to `True`, and says that each time a template is returned by the `get_template()` method, the revision time of the original template file is checked against the last time the template was loaded, and if the file is newer will reload its contents and recompile the template. On a production system, seting `filesystem_checks` to `False` can afford a small to moderate performance increase (depending on the type of filesystem used). +#### Setting Filesystem Checks {@name=checks} -When the lookup locates templates, it will also assign a `uri` property to the `Template` which is the uri passed to the `get_template()` call. This argument is provided to the `Template` itself, which the template then uses to calculate the name of its module file. So in the above example, the `templatename` argument of `/etc/beans/info.txt` will create a module file `/tmp/mako_modules/etc/beans/info.txt.py`. +Another important flag on `TemplateLookup` is `filesystem_checks`. This defaults to `True`, and says that each time a template is returned by the `get_template()` method, the revision time of the original template file is checked against the last time the template was loaded, and if the file is newer will reload its contents and recompile the template. On a production system, seting `filesystem_checks` to `False` can afford a small to moderate performance increase (depending on the type of filesystem used). ### Using Unicode and Encoding @@ -106,9 +111,13 @@ The above method disgards the output encoding keyword argument; you can encode y The above methods deal only with the encoding of the template output. To indicate an encoding for the template's source file, place a Python-style "magic encoding comment" as the very first line in the template file: - # -*- encoding: utf-8 -*- + # -*- coding: utf-8 -*- <template text> Mako's `Lexer` module will detect the above marker and decode the template text internally into a `unicode` object using the given encoding. +For the picky, the regular expression used is derived from that of [pep-0263](http://www.python.org/dev/peps/pep-0263/): + + #.*coding[:=]\s*([-\w.]+).*\n + diff --git a/doc/build/genhtml.py b/doc/build/genhtml.py index 4d463f8..bb05d8b 100644 --- a/doc/build/genhtml.py +++ b/doc/build/genhtml.py @@ -16,6 +16,7 @@ files = [ 'syntax', 'defs', 'namespaces', + 'filtering', 'caching', ] diff --git a/doc/build/templates/base.html b/doc/build/templates/base.html index 9044638..ac12cca 100644 --- a/doc/build/templates/base.html +++ b/doc/build/templates/base.html @@ -5,8 +5,8 @@ <%page cached="False" cache_key="${self.filename}"/> <%def name="style()"> - ${parent.style()} <link rel="stylesheet" href="docs.css"></link> + ${parent.style()} </%def> # base.html - common to all documentation pages. intentionally separate diff --git a/doc/docs.css b/doc/docs.css index 1c07fd8..7964c6b 100644 --- a/doc/docs.css +++ b/doc/docs.css @@ -80,7 +80,7 @@ td { pre { margin: 1.5em; - padding: .5em; + padding: .7em; font-size: 1em; line-height:1em; background-color: #eee; diff --git a/lib/mako/codegen.py b/lib/mako/codegen.py index 96a0a03..9ec7808 100644 --- a/lib/mako/codegen.py +++ b/lib/mako/codegen.py @@ -372,6 +372,9 @@ class _GenerateRenderMethod(object): """write a filter-applying expression based on the filters present in the given filter names, adjusting for the global 'default' filter aliases as needed.""" d = dict([(k, "filters." + v.func_name) for k, v in filters.DEFAULT_ESCAPES.iteritems()]) + + if self.compiler.pagetag: + args += self.compiler.pagetag.filter_args.args for e in args: e = d.get(e, e) target = "%s(%s)" % (e, target) @@ -379,7 +382,7 @@ class _GenerateRenderMethod(object): def visitExpression(self, node): self.write_source_comment(node) - if len(node.escapes): + if len(node.escapes) or (self.compiler.pagetag is not None and len(self.compiler.pagetag.filter_args.args)): s = self.create_filter_callable(node.escapes_code.args, node.text) self.printer.writeline("context.write(unicode(%s))" % s) else: diff --git a/lib/mako/filters.py b/lib/mako/filters.py index 1369dc5..d4a7626 100644 --- a/lib/mako/filters.py +++ b/lib/mako/filters.py @@ -38,13 +38,6 @@ def url_unescape(string): def trim(string): return string.strip() -# TODO: options to make this dynamic per-compilation will be added in a later release -DEFAULT_ESCAPES = { - 'x':xml_escape, - 'h':html_escape, - 'u':url_escape, - 'trim':trim, -} _ASCII_re = re.compile(r'\A[\x00-\x7f]*\Z') @@ -148,5 +141,13 @@ def htmlentityreplace_errors(ex): codecs.register_error('htmlentityreplace', htmlentityreplace_errors) +# TODO: options to make this dynamic per-compilation will be added in a later release +DEFAULT_ESCAPES = { + 'x':xml_escape, + 'h':html_escape, + 'u':url_escape, + 'trim':trim, + 'entity':html_entities_escape, +} diff --git a/lib/mako/lexer.py b/lib/mako/lexer.py index 5c45ec0..cf6e126 100644 --- a/lib/mako/lexer.py +++ b/lib/mako/lexer.py @@ -122,7 +122,7 @@ class Lexer(object): return self.template def match_encoding(self): - match = self.match(r'#[\t ]*-\*- encoding: (.+?) -\*-\n') + match = self.match(r'#.*coding[:=]\s*([-\w.]+).*\n') if match: return match.group(1) else: diff --git a/lib/mako/parsetree.py b/lib/mako/parsetree.py index 33bcfe5..7038f45 100644 --- a/lib/mako/parsetree.py +++ b/lib/mako/parsetree.py @@ -274,8 +274,9 @@ class InheritTag(Tag): class PageTag(Tag): __keyword__ = 'page' def __init__(self, keyword, attributes, **kwargs): - super(PageTag, self).__init__(keyword, attributes, ('cached', 'cache_key', 'cache_timeout', 'cache_type', 'cache_dir', 'args'), (), (), **kwargs) + super(PageTag, self).__init__(keyword, attributes, ('cached', 'cache_key', 'cache_timeout', 'cache_type', 'cache_dir', 'args', 'expression_filter'), (), (), **kwargs) self.body_decl = ast.FunctionArgs(attributes.get('args', ''), self.lineno, self.pos, self.filename) + self.filter_args = ast.ArgumentList(attributes.get('expression_filter', ''), self.lineno, self.pos, self.filename) def declared_identifiers(self): return self.body_decl.argnames diff --git a/test/alltests.py b/test/alltests.py index 870c451..f832c67 100644 --- a/test/alltests.py +++ b/test/alltests.py @@ -10,6 +10,7 @@ def suite(): 'lookup', 'def', 'namespace', + 'filters', 'inheritance', 'call', 'cache' diff --git a/test/filters.py b/test/filters.py index b844f3a..86a1224 100644 --- a/test/filters.py +++ b/test/filters.py @@ -21,13 +21,30 @@ class FilterTest(unittest.TestCase): def test_def(self): t = Template(""" - <%def name="foo" filter="myfilter"> + <%def name="foo()" filter="myfilter"> this is foo </%def> ${foo()} """) assert flatten_result(t.render(x="this is x", myfilter=lambda t: "MYFILTER->%s<-MYFILTER" % t)) == "MYFILTER-> this is foo <-MYFILTER" + def test_import(self): + t = Template(""" + <%! + from mako import filters + %>\ + trim this string: ${" some string to trim " | filters.trim} continue\ + """) + + assert t.render().strip()=="trim this string: some string to trim continue" + + def test_global(self): + t = Template(""" + <%page expression_filter="h"/> + ${"<tag>this is html</tag>"} + """) + assert t.render().strip() == "<tag>this is html</tag>" + def test_builtins(self): t = Template(""" ${"this is <text>" | h} @@ -42,7 +59,7 @@ class FilterTest(unittest.TestCase): class BufferTest(unittest.TestCase): def test_buffered_def(self): t = Template(""" - <%def name="foo" buffered="True"> + <%def name="foo()" buffered="True"> this is foo </%def> ${"hi->" + foo() + "<-hi"} @@ -51,7 +68,7 @@ class BufferTest(unittest.TestCase): def test_unbuffered_def(self): t = Template(""" - <%def name="foo" buffered="False"> + <%def name="foo()" buffered="False"> this is foo </%def> ${"hi->" + foo() + "<-hi"} @@ -60,7 +77,7 @@ class BufferTest(unittest.TestCase): def test_capture(self): t = Template(""" - <%def name="foo" buffered="False"> + <%def name="foo()" buffered="False"> this is foo </%def> ${"hi->" + capture(foo) + "<-hi"} @@ -105,7 +122,7 @@ class BufferTest(unittest.TestCase): def test_capture_ccall(self): t = Template(""" - <%def name="foo"> + <%def name="foo()"> <% x = capture(caller.body) %> diff --git a/test/template.py b/test/template.py index 00de06c..616330d 100644 --- a/test/template.py +++ b/test/template.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- from mako.template import Template import unittest, re, os @@ -6,7 +6,7 @@ from util import flatten_result, result_lines if not os.access('./test_htdocs', os.F_OK): os.mkdir('./test_htdocs') -file('./test_htdocs/unicode.html', 'w').write("""# -*- encoding: utf-8 -*- +file('./test_htdocs/unicode.html', 'w').write("""# -*- coding: utf-8 -*- Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""") class EncodingTest(unittest.TestCase): @@ -25,7 +25,7 @@ class EncodingTest(unittest.TestCase): def test_unicode_memory(self): val = u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""" - val = "# -*- encoding: utf-8 -*-\n" + val.encode('utf-8') + val = "# -*- coding: utf-8 -*-\n" + val.encode('utf-8') template = Template(val) assert template.render_unicode() == u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""" |