initial commit
authorfabien <fabien@93ef8e89-cb99-4229-a87c-7fa0fa45744b>
Wed, 7 Oct 2009 21:21:11 +0000 (21:21 +0000)
committerfabien <fabien@93ef8e89-cb99-4229-a87c-7fa0fa45744b>
Wed, 7 Oct 2009 21:21:11 +0000 (21:21 +0000)
git-svn-id: http://svn.twig-project.org/trunk@4 93ef8e89-cb99-4229-a87c-7fa0fa45744b

140 files changed:
AUTHORS [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.markdown [new file with mode: 0644]
doc/01-Introduction.markdown [new file with mode: 0644]
doc/02-Twig-for-Template-Designers.markdown [new file with mode: 0644]
doc/03-Twig-for-Developers.markdown [new file with mode: 0644]
doc/04-Extending-Twig.markdown [new file with mode: 0644]
doc/05-Hacking-Twig.markdown [new file with mode: 0644]
lib/Twig/Autoloader.php [new file with mode: 0644]
lib/Twig/Compiler.php [new file with mode: 0644]
lib/Twig/CompilerInterface.php [new file with mode: 0644]
lib/Twig/Environment.php [new file with mode: 0644]
lib/Twig/Error.php [new file with mode: 0644]
lib/Twig/ExpressionParser.php [new file with mode: 0644]
lib/Twig/Extension.php [new file with mode: 0644]
lib/Twig/Extension/Core.php [new file with mode: 0644]
lib/Twig/Extension/Escaper.php [new file with mode: 0644]
lib/Twig/Extension/Macro.php [new file with mode: 0644]
lib/Twig/Extension/Sandbox.php [new file with mode: 0644]
lib/Twig/Extension/Set.php [new file with mode: 0644]
lib/Twig/ExtensionInterface.php [new file with mode: 0644]
lib/Twig/Lexer.php [new file with mode: 0644]
lib/Twig/LexerInterface.php [new file with mode: 0644]
lib/Twig/Loader.php [new file with mode: 0644]
lib/Twig/Loader/Array.php [new file with mode: 0644]
lib/Twig/Loader/Filesystem.php [new file with mode: 0644]
lib/Twig/Loader/String.php [new file with mode: 0644]
lib/Twig/LoaderInterface.php [new file with mode: 0644]
lib/Twig/Node.php [new file with mode: 0644]
lib/Twig/Node/AutoEscape.php [new file with mode: 0644]
lib/Twig/Node/Block.php [new file with mode: 0644]
lib/Twig/Node/BlockReference.php [new file with mode: 0644]
lib/Twig/Node/Call.php [new file with mode: 0644]
lib/Twig/Node/Expression.php [new file with mode: 0644]
lib/Twig/Node/Expression/AssignName.php [new file with mode: 0644]
lib/Twig/Node/Expression/Binary.php [new file with mode: 0644]
lib/Twig/Node/Expression/Binary/Add.php [new file with mode: 0644]
lib/Twig/Node/Expression/Binary/And.php [new file with mode: 0644]
lib/Twig/Node/Expression/Binary/Concat.php [new file with mode: 0644]
lib/Twig/Node/Expression/Binary/Div.php [new file with mode: 0644]
lib/Twig/Node/Expression/Binary/Mod.php [new file with mode: 0644]
lib/Twig/Node/Expression/Binary/Mul.php [new file with mode: 0644]
lib/Twig/Node/Expression/Binary/Or.php [new file with mode: 0644]
lib/Twig/Node/Expression/Binary/Sub.php [new file with mode: 0644]
lib/Twig/Node/Expression/Compare.php [new file with mode: 0644]
lib/Twig/Node/Expression/Conditional.php [new file with mode: 0644]
lib/Twig/Node/Expression/Constant.php [new file with mode: 0644]
lib/Twig/Node/Expression/Filter.php [new file with mode: 0644]
lib/Twig/Node/Expression/GetAttr.php [new file with mode: 0644]
lib/Twig/Node/Expression/Name.php [new file with mode: 0644]
lib/Twig/Node/Expression/Unary.php [new file with mode: 0644]
lib/Twig/Node/Expression/Unary/Neg.php [new file with mode: 0644]
lib/Twig/Node/Expression/Unary/Not.php [new file with mode: 0644]
lib/Twig/Node/Expression/Unary/Pos.php [new file with mode: 0644]
lib/Twig/Node/Filter.php [new file with mode: 0644]
lib/Twig/Node/For.php [new file with mode: 0644]
lib/Twig/Node/If.php [new file with mode: 0644]
lib/Twig/Node/Include.php [new file with mode: 0644]
lib/Twig/Node/Macro.php [new file with mode: 0644]
lib/Twig/Node/Module.php [new file with mode: 0644]
lib/Twig/Node/Parent.php [new file with mode: 0644]
lib/Twig/Node/Print.php [new file with mode: 0644]
lib/Twig/Node/Set.php [new file with mode: 0644]
lib/Twig/Node/Text.php [new file with mode: 0644]
lib/Twig/NodeList.php [new file with mode: 0644]
lib/Twig/NodeListInterface.php [new file with mode: 0644]
lib/Twig/NodeTransformer.php [new file with mode: 0644]
lib/Twig/NodeTransformer/Chain.php [new file with mode: 0644]
lib/Twig/NodeTransformer/Escaper.php [new file with mode: 0644]
lib/Twig/NodeTransformer/Filter.php [new file with mode: 0644]
lib/Twig/NodeTransformer/Sandbox.php [new file with mode: 0644]
lib/Twig/Parser.php [new file with mode: 0644]
lib/Twig/ParserInterface.php [new file with mode: 0644]
lib/Twig/RuntimeError.php [new file with mode: 0644]
lib/Twig/Sandbox/SecurityError.php [new file with mode: 0644]
lib/Twig/Sandbox/SecurityPolicy.php [new file with mode: 0644]
lib/Twig/Sandbox/SecurityPolicyInterface.php [new file with mode: 0644]
lib/Twig/SyntaxError.php [new file with mode: 0644]
lib/Twig/Template.php [new file with mode: 0644]
lib/Twig/TemplateInterface.php [new file with mode: 0644]
lib/Twig/Token.php [new file with mode: 0644]
lib/Twig/TokenParser.php [new file with mode: 0644]
lib/Twig/TokenParser/AutoEscape.php [new file with mode: 0644]
lib/Twig/TokenParser/Block.php [new file with mode: 0644]
lib/Twig/TokenParser/Call.php [new file with mode: 0644]
lib/Twig/TokenParser/Display.php [new file with mode: 0644]
lib/Twig/TokenParser/Extends.php [new file with mode: 0644]
lib/Twig/TokenParser/Filter.php [new file with mode: 0644]
lib/Twig/TokenParser/For.php [new file with mode: 0644]
lib/Twig/TokenParser/If.php [new file with mode: 0644]
lib/Twig/TokenParser/Include.php [new file with mode: 0644]
lib/Twig/TokenParser/Macro.php [new file with mode: 0644]
lib/Twig/TokenParser/Parent.php [new file with mode: 0644]
lib/Twig/TokenParser/Set.php [new file with mode: 0644]
lib/Twig/TokenStream.php [new file with mode: 0644]
lib/Twig/runtime.php [new file with mode: 0644]
lib/Twig/runtime_escaper.php [new file with mode: 0644]
lib/Twig/runtime_for.php [new file with mode: 0644]
test/bin/coverage.php [new file with mode: 0644]
test/bin/prove.php [new file with mode: 0644]
test/fixtures/expressions/binary.test [new file with mode: 0644]
test/fixtures/expressions/comparison.test [new file with mode: 0644]
test/fixtures/expressions/grouping.test [new file with mode: 0644]
test/fixtures/expressions/ternary_operator.test [new file with mode: 0644]
test/fixtures/expressions/unary.test [new file with mode: 0644]
test/fixtures/filters/date.test [new file with mode: 0644]
test/fixtures/filters/default.test [new file with mode: 0644]
test/fixtures/filters/even.test [new file with mode: 0644]
test/fixtures/filters/format.test [new file with mode: 0644]
test/fixtures/filters/length.test [new file with mode: 0644]
test/fixtures/filters/odd.test [new file with mode: 0644]
test/fixtures/filters/sort.test [new file with mode: 0644]
test/fixtures/tags/autoescape/basic.test [new file with mode: 0644]
test/fixtures/tags/autoescape/double_escaping.test [new file with mode: 0644]
test/fixtures/tags/autoescape/nested.test [new file with mode: 0644]
test/fixtures/tags/autoescape/safe.test [new file with mode: 0644]
test/fixtures/tags/filter/basic.test [new file with mode: 0644]
test/fixtures/tags/filter/nested.test [new file with mode: 0644]
test/fixtures/tags/filter/with_for_tag.test [new file with mode: 0644]
test/fixtures/tags/filter/with_if_tag.test [new file with mode: 0644]
test/fixtures/tags/for/context.test [new file with mode: 0644]
test/fixtures/tags/for/else.test [new file with mode: 0644]
test/fixtures/tags/for/keys.test [new file with mode: 0644]
test/fixtures/tags/for/keys_and_values.test [new file with mode: 0644]
test/fixtures/tags/for/loop_context.test [new file with mode: 0644]
test/fixtures/tags/for/loop_context_local.test [new file with mode: 0644]
test/fixtures/tags/for/nested_else.test [new file with mode: 0644]
test/fixtures/tags/for/objects.test [new file with mode: 0644]
test/fixtures/tags/for/recursive.test [new file with mode: 0644]
test/fixtures/tags/for/values.test [new file with mode: 0644]
test/fixtures/tags/if/basic.test [new file with mode: 0644]
test/fixtures/tags/if/expression.test [new file with mode: 0644]
test/fixtures/tags/include/basic.test [new file with mode: 0644]
test/fixtures/tags/include/expression.test [new file with mode: 0644]
test/fixtures/tags/inheritance/basic.test [new file with mode: 0644]
test/fixtures/tags/inheritance/parent.test [new file with mode: 0644]
test/lib/Twig_Loader_Var.php [new file with mode: 0644]
test/unit/Twig/AutoloaderTest.php [new file with mode: 0644]
test/unit/Twig/Extension/Sandbox.php [new file with mode: 0644]
test/unit/integrationTest.php [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..eb5db05
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,9 @@
+Twig is written and maintained by the Twig Team:
+
+Lead Developer:
+
+- Fabien Potencier <fabien.potencier@symfony-project.org>
+
+Project Founder:
+
+- Armin Ronacher <armin.ronacher@active-4.com>
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..5063d8d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,31 @@
+Copyright (c) 2009 by the Twig Team, see AUTHORS for more details.
+
+Some rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * The names of the contributors may not be used to endorse or
+      promote products derived from this software without specific
+      prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.markdown b/README.markdown
new file mode 100644 (file)
index 0000000..3c77450
--- /dev/null
@@ -0,0 +1,8 @@
+Twig, the flexible, fast, and secure template language for PHP
+==============================================================
+
+Twig is a template language for PHP, released under the new BSD license (code
+and documentation).
+
+Twig uses a syntax similar to the Django and Jinja template languages which
+inspired the Twig runtime environment.
diff --git a/doc/01-Introduction.markdown b/doc/01-Introduction.markdown
new file mode 100644 (file)
index 0000000..5b3d72c
--- /dev/null
@@ -0,0 +1,85 @@
+Introduction
+============
+
+This is the documentation for Twig, the flexible, fast, and secure template
+language for PHP.
+
+If you have any exposure to other text-based template languages, such as
+Smarty, Django, or Jinja, you should feel right at home with Twig. It's both
+designer and developer friendly by sticking to PHP's principles and adding
+functionality useful for templating environments.
+
+The key-features are...
+
+ * *Fast*: Twig compiles templates down to plain optimized PHP code. The
+   overhead compared to regular PHP code was reduced to the very minimum.
+
+ * *Secure*: Twig has a sandbox mode to evaluate untrusted template code. This
+   allows Twig to be used as a templating language for applications where
+   users may modify the template design.
+
+ * *Flexible*: Twig is powered by a flexible lexer and parser. This allows the
+   developer to define its own custom tags and filters, and create its own
+   DSL.
+
+Prerequisites
+-------------
+
+Twig needs at least **PHP 5.2.4** to run.
+
+Installation
+------------
+
+You have multiple ways to install Twig. If you are unsure what to do, go with
+the tarball.
+
+### From the tarball release
+
+ 1. Download the most recent tarball from the [download page](http://www.twig-project.org/installation)
+ 2. Unpack the tarball
+ 3. Move the files somewhere in your project
+
+### Installing the development version
+
+ 1. Install Subversion
+ 2. `svn co http://svn.twig-project.org/trunk/ twig`
+
+Basic API Usage
+---------------
+
+This section gives you a brief introduction to the PHP API for Twig.
+
+The first step to use Twig is to register its autoloader:
+
+    [php]
+    require_once '/path/to/lib/Twig/Autoloader.php';
+    Twig_Autoloader::register();
+
+Replace the `/path/to/lib/` path with the path you used for Twig installation.
+
+>**NOTE**
+>Twig follows the PEAR convention names for its classes, which means you can
+>easily integrate Twig classes loading in your own autoloader.
+
+    [php]
+    $loader = new Twig_Loader_String();
+    $twig = new Twig_Environment($loader);
+
+    $template = $twig->loadTemplate('Hello {{ name }}!');
+
+    $template->display(array('name' => 'Fabien'));
+
+Twig uses a loader (`Twig_Loader_String`) to locate templates, and an
+environment (`Twig_Environment`) to store the configuration.
+
+The `loadTemplate()` method uses the loader to locate and load the template
+and returns a template object (`Twig_Template`) which is suitable for
+rendering with the `display()` method.
+
+Twig also comes with a filesystem loader:
+
+    [php]
+    $loader = new Twig_Loader_Filesystem('/path/to/templates');
+    $twig = new Twig_Environment($loader);
+
+    $template = $twig->loadTemplate('index.html');
diff --git a/doc/02-Twig-for-Template-Designers.markdown b/doc/02-Twig-for-Template-Designers.markdown
new file mode 100644 (file)
index 0000000..71444b0
--- /dev/null
@@ -0,0 +1,692 @@
+Twig for Template Designers
+===========================
+
+This document describes the syntax and semantics of the template engine and
+will be most useful as reference to those creating Twig templates.
+
+Synopsis
+--------
+
+A template is simply a text file. It can generate any text-based format (HTML,
+XML, CSV, LaTeX, etc.). It doesn't have a specific extension, `.html` or
+`.xml` are just fine.
+
+A template contains **variables** or **expressions**, which get replaced with
+values when the template is evaluated, and tags, which control the logic of
+the template.
+
+Below is a minimal template that illustrates a few basics. We will cover the
+details later in that document:
+
+    [twig]
+    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+    <html lang="en">
+      <head>
+        <title>My Webpage</title>
+      </head>
+      <body>
+        <ul id="navigation">
+        {% for item in navigation %}
+          <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
+        {% endfor %}
+        </ul>
+
+        <h1>My Webpage</h1>
+        {{ a_variable }}
+      </body>
+    </html>
+
+There are two kinds of delimiters: `{% ... %}` and `{{ ... }}`. The first one
+is used to execute statements such as for-loops, the latter prints the result
+of an expression to the template.
+
+Variables
+---------
+
+The application passes variables to the templates you can mess around in the
+template. Variables may have attributes or elements on them you can access
+too. How a variable looks like, heavily depends on the application providing
+those.
+
+You can use a dot (`.`) to access attributes of a variable, alternative the
+so-called "subscript" syntax (`[]`) can be used. The following lines do the
+same::
+
+    [twig]
+    {{ foo.bar }}
+    {{ foo['bar'] }}
+
+>**NOTE**
+>It's important to know that the curly braces are *not* part of the variable
+>but the print statement. If you access variables inside tags don't put the
+>braces around.
+
+If a variable or attribute does not exist you will get back a `null` value.
+
+>**SIDEBAR**
+>Implementation
+>
+>For convenience sake `foo.bar` does the following things on
+>the PHP layer:
+>
+> * check if `foo` is an array and `bar` a valid element;
+> * if not, and if `foo` is an object, check that `bar` is a valid method;
+> * if not, and if `foo` is an object, check that `getBar` is a valid method;
+> * if not, return a `null` value.
+>
+>`foo['bar']` on the other hand works mostly the same with the a small
+>difference in the order:
+>
+> * check if `foo` is an array and `bar` a valid element;
+> * if not, return a `null` value.
+>
+>Using the alternative syntax is also useful to dynamically get attributes
+>from arrays:
+>
+>     [twig]
+>     foo[bar]
+
+Filters
+-------
+
+Variables can by modified by **filters**. Filters are separated from the
+variable by a pipe symbol (`|`) and may have optional arguments in
+parentheses. Multiple filters can be chained. The output of one filter is
+applied to the next.
+
+`{{ name|striptags|title }}` for example will remove all HTML tags from the
+`name` and title-cases it. Filters that accept arguments have parentheses
+around the arguments, like a function call. This example will join a list by
+commas: `{{ list|join(', ') }}`.
+
+The builtin filters section below describes all the builtin filters.
+
+Comments
+--------
+
+To comment-out part of a line in a template, use the comment syntax `{# ... #}`.
+This is useful to comment out parts of the template for debugging or to
+add information for other template designers or yourself:
+
+    [twig]
+    {# note: disabled template because we no longer use this
+      {% for user in users %}
+          ...
+      {% endfor %}
+    #}
+
+Whitespace Control
+------------------
+
+In the default configuration whitespace is not further modified by the
+template engine, so each whitespace (spaces, tabs, newlines etc.) is returned
+unchanged. If the application configures Twig to `trim_blocks` the first
+newline after a template tag is removed automatically (like in PHP).
+
+Escaping
+--------
+
+It is sometimes desirable or even necessary to have Twig ignore parts it would
+otherwise handle as variables or blocks. For example if the default syntax is
+used and you want to use `{{` as raw string in the template and not start a
+variable you have to use a trick.
+
+The easiest way is to output the variable delimiter (`{{`) by using a variable
+expression:
+
+    [twig]
+    {{ '{{' }}
+
+For bigger sections it makes sense to mark a block `raw`. For example to put
+Twig syntax as example into a template you can use this snippet:
+
+    [twig]
+    {% raw %}
+      <ul>
+      {% for item in seq %}
+        <li>{{ item }}</li>
+      {% endfor %}
+      </ul>
+    {% endraw %}
+
+Template Inheritance
+--------------------
+
+The most powerful part of Twig is template inheritance. Template inheritance
+allows you to build a base "skeleton" template that contains all the common
+elements of your site and defines **blocks** that child templates can
+override.
+
+Sounds complicated but is very basic. It's easiest to understand it by
+starting with an example.
+
+### Base Template
+
+This template, which we'll call `base.html`, defines a simple HTML skeleton
+document that you might use for a simple two-column page. It's the job of
+"child" templates to fill the empty blocks with content:
+
+    [twig]
+    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+    <html lang="en">
+    <html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+      {% block head %}
+        <link rel="stylesheet" href="style.css" />
+        <title>{% block title %}{% endblock %} - My Webpage</title>
+      {% endblock %}
+    </head>
+    <body>
+      <div id="content">{% block content %}{% endblock %}</div>
+      <div id="footer">
+        {% block footer %}
+          &copy; Copyright 2009 by <a href="http://domain.invalid/">you</a>.
+        {% endblock %}
+      </div>
+    </body>
+
+In this example, the `{% block %}` tags define four blocks that child
+templates can fill in. All the `block` tag does is to tell the template engine
+that a child template may override those portions of the template.
+
+### Child Template
+
+A child template might look like this:
+
+    [twig]
+    {% extends "base.html" %}
+
+    {% block title %}Index{% endblock %}
+    {% block head %}
+      {% parent %}
+      <style type="text/css">
+        .important { color: #336699; }
+      </style>
+    {% endblock %}
+    {% block content %}
+      <h1>Index</h1>
+      <p class="important">
+        Welcome on my awesome homepage.
+      </p>
+    {% endblock %}
+
+The `{% extends %}` tag is the key here. It tells the template engine that
+this template "extends" another template. When the template system evaluates
+this template, first it locates the parent. The extends tag should be the
+first tag in the template.
+
+The filename of the template depends on the template loader. For example the
+`Twig_Loader_Filesystem` allows you to access other templates by giving the
+filename. You can access templates in subdirectories with a slash:
+
+    [twig]
+    {% extends "layout/default.html" %}
+
+But this behavior can depend on the application embedding Twig. Note that
+since the child template doesn't define the `footer` block, the value from the
+parent template is used instead.
+
+You can't define multiple `{% block %}` tags with the same name in the same
+template. This limitation exists because a block tag works in "both"
+directions. That is, a block tag doesn't just provide a hole to fill - it also
+defines the content that fills the hole in the *parent*. If there were two
+similarly-named `{% block %}` tags in a template, that template's parent
+wouldn't know which one of the blocks' content to use.
+
+If you want to print a block multiple times you can however use the `display`
+tag:
+
+    [twig]
+    <title>{% block title %}{% endblock %}</title>
+    <h1>{% display title %}</h1>
+    {% block body %}{% endblock %}
+
+Like PHP, Twig does not support multiple inheritance. So you can only have one
+extends tag called per rendering.
+
+### Parent Blocks
+
+It's possible to render the contents of the parent block by using the `parent`
+tag. This gives back the results of the parent block:
+
+    [twig]
+    {% block sidebar %}
+      <h3>Table Of Contents</h3>
+      ...
+      {% parent %}
+    {% endblock %}
+
+### Named Block End-Tags
+
+Twig allows you to put the name of the block after the end tag for better
+readability:
+
+    [twig]
+    {% block sidebar %}
+      {% block inner_sidebar %}
+          ...
+      {% endblock inner_sidebar %}
+    {% endblock sidebar %}
+
+However the name after the `endblock` word must match the block name.
+
+### Block Nesting and Scope
+
+Blocks can be nested for more complex layouts. Per default, blocks have access
+to variables from outer scopes:
+
+    [twig]
+    {% for item in seq %}
+      <li>{% block loop_item %}{{ item }}{% endblock %}</li>
+    {% endfor %}
+
+Import Context Behavior
+-----------------------
+
+Per default included templates are passed the current context.
+
+The context that is passed to the included template includes variables defined
+in the template:
+
+    [twig]
+    {% for box in boxes %}
+      {% include "render_box.html" %}
+    {% endfor %}
+
+The included template `render_box.html` is able to access `box`.
+
+HTML Escaping
+-------------
+
+When generating HTML from templates, there's always a risk that a variable
+will include characters that affect the resulting HTML. There are two
+approaches: manually escaping each variable or automatically escaping
+everything by default.
+
+Twig supports both, but what is used depends on the application configuration.
+The default configuration is no automatic escaping for various reasons:
+
+ * Escaping everything except of safe values will also mean that Twig is
+   escaping variables known to not include HTML such as numbers which is a
+   huge performance hit.
+
+ * The information about the safety of a variable is very fragile. It could
+   happen that by coercing safe and unsafe values the return value is double
+   escaped HTML.
+
+>**NOTE**
+>Escaping is only supported if the *escaper* extension has been enabled.
+
+### Working with Manual Escaping
+
+If manual escaping is enabled it's **your** responsibility to escape variables
+if needed. What to escape? If you have a variable that *may* include any of
+the following chars (`>`, `<`, `&`, or `"`) you **have to** escape it unless
+the variable contains well-formed and trusted HTML. Escaping works by piping
+the variable through the `|e` filter: `{{ user.username|e }}`.
+
+### Working with Automatic Escaping
+
+Automatic escaping is enabled when the `escaper` extension has been enabled.
+
+Whether automatic escaping is enabled or not, you can mark a section of a
+template to be escaped or not by using the `autoescape` tag:
+
+    [twig]
+    {% autoescape on %}
+      Everything will be automatically escaped in this block
+    {% endautoescape %}
+
+    {% autoescape off %}
+      Everything will be outputed as is in this block
+    {% endautoescape %}
+
+When automatic escaping is enabled everything is escaped by default except for
+values explicitly marked as safe. Those can be marked in the template by using
+the `|safe` filter.
+
+Functions returning template data (like `parent`) return safe markup always.
+
+>**NOTE**
+>Twig is smart enough to not escape an already escaped value by the `escape`
+>filter.
+
+List of Control Structures
+--------------------------
+
+A control structure refers to all those things that control the flow of a
+program - conditionals (i.e. `if`/`elseif`/`else`), `for`-loops, as well as
+things like blocks. Control structures appear inside `{% ... %}` blocks.
+
+### For
+
+Loop over each item in a sequence. For example, to display a list of users
+provided in a variable called `users`:
+
+    [twig]
+    <h1>Members</h1>
+    <ul>
+      {% for user in users %}
+        <li>{{ user.username|e }}</li>
+      {% endfor %}
+    </ul>
+
+Inside of a for loop block you can access some special variables:
+
+| Variable              | Description
+| --------------------- | -------------------------------------------------------------
+| `loop.index`          | The current iteration of the loop. (1 indexed)
+| `loop.index0`         | The current iteration of the loop. (0 indexed)
+| `loop.revindex`       | The number of iterations from the end of the loop (1 indexed)
+| `loop.revindex0`      | The number of iterations from the end of the loop (0 indexed)
+| `loop.first`          | True if first iteration
+| `loop.last`           | True if last iteration
+| `loop.length`         | The number of items in the sequence
+
+Unlike in PHP it's not possible to `break` or `continue` in a loop.
+
+If no iteration took place because the sequence was empty, you can render a
+replacement block by using `else`:
+
+    [twig]
+    <ul>
+      {% for user in users %}
+        <li>{{ user.username|e }}</li>
+      {% else %}
+        <li><em>no users found</em></li>
+      {% endif %}
+    </ul>
+
+### If
+
+The `if` statement in Twig is comparable with the if statements of PHP. In the
+simplest form you can use it to test if a variable is defined, not empty or
+not false:
+
+    [twig]
+    {% if users %}
+      <ul>
+        {% for user in users %}
+          <li>{{ user.username|e }}</li>
+        {% endfor %}
+      </ul>
+    {% endif %}
+
+For multiple branches `elseif` and `else` can be used like in PHP. You can use
+more complex `expressions` there too:
+
+    {% if kenny.sick %}
+      Kenny is sick.
+    {% elseif kenny.dead %}
+      You killed Kenny!  You bastard!!!
+    {% else %}
+      Kenny looks okay --- so far
+    {% endif %}
+
+### Filters
+
+Filter sections allow you to apply regular Twig filters on a block of template
+data. Just wrap the code in the special `filter` section:
+
+    [twig]
+    {% filter upper %}
+      This text becomes uppercase
+    {% endfilter %}
+
+### Extends
+
+The `extends` tag can be used to extend a template from another one. You can
+have multiple of them in a file but only one of them may be executed at the
+time. There is no support for multiple inheritance. See the section about
+Template inheritance above.
+
+### Block
+
+Blocks are used for inheritance and act as placeholders and replacements at
+the same time. They are documented in detail as part of the section about
+Template inheritance above.
+
+### Include
+
+The `include` statement is useful to include a template and return the
+rendered contents of that file into the current namespace:
+
+    [twig]
+    {% include 'header.html' %}
+      Body
+    {% include 'footer.html' %}
+
+Included templates have access to the variables of the active context.
+
+An included file can be evaluated in the sandbox environment by appending
+`sandboxed` at the end if the `escaper` extension has been enabled:
+
+    [twig]
+    {% include 'user.html' sandboxed %}
+
+Expressions
+-----------
+
+Twig allows basic expressions everywhere. These work very similar to regular
+PHP and even if you're not working with PHP you should feel comfortable with
+it.
+
+### Literals
+
+The simplest form of expressions are literals. Literals are representations
+for PHP types such as strings and numbers. The following literals exist:
+
+ * `"Hello World"`: Everything between two double or single quotes is a
+   string. They are useful whenever you need a string in the template (for
+   example as arguments to function calls, filters or just to extend or
+   include a template).
+
+ * `42` / `42.23`: Integers and floating point numbers are created by just
+   writing the number down. If a dot is present the number is a float,
+   otherwise an integer.
+
+### Math
+
+Twig allows you to calculate with values. This is rarely useful in templates
+but exists for completeness' sake. The following operators are supported:
+
+ * `+`: Adds two objects together. Usually the objects are numbers but if both
+   are strings or lists you can concatenate them this way. This however is not
+   the preferred way to concatenate strings! For string concatenation have a
+   look at the `~` operator. `{{ 1 + 1 }}` is `2`.
+
+ * `-`: Substract the second number from the first one. `{{ 3 - 2 }}` is `1`.
+
+ * `/`: Divide two numbers. The return value will be a floating point number.
+   `{{ 1 / 2 }}` is `{{ 0.5 }}`.
+
+ * `//`: Divide two numbers and return the truncated integer result. `{{ 20 //
+   7 }}` is `2`.
+
+ * `%`: Calculate the remainder of an integer division. `{{ 11 % 7 }}` is `4`.
+
+ * `*`: Multiply the left operand with the right one. `{{ 2 * 2 }}` would
+   return `4`. This can also be used to repeat a string multiple times. `{{
+   '=' * 80 }}` would print a bar of 80 equal signs.
+
+ * `**`: Raise the left operand to the power of the right operand. `{{ 2**3
+   }}` would return `8`.
+
+### Logic
+
+For `if` statements, `for` filtering or `if` expressions it can be useful to
+combine multiple expressions:
+
+ * `and`: Return true if the left and the right operand is true.
+
+ * `or`: Return true if the left or the right operand is true.
+
+ * `not`: Negate a statement.
+
+ * `(expr)`: Group an expression.
+
+### Comparisons
+
+The following comparison operators are supported in any expression: `==`,
+`!=`, `<`, `>`, `>=`, and `<=`.
+
+>**TIP**
+>Besides PHP classic comparison operators, Twig also supports a shortcut
+>notation when you want to test a value in a range:
+>
+>     [twig]
+>     {% if 1 < foo < 4 %}foo is between 1 and 4{% endif %}
+
+### Other Operators
+
+The following operators are very useful but don't fit into any of the other
+two categories:
+
+ * `|`: Applies a filter.
+
+ * `~`: Converts all operands into strings and concatenates them. `{{ "Hello "
+   ~ name ~ "!" }}` would return (assuming `name` is `'John'`) `Hello John!`.
+
+ * `.`, `[]`: Get an attribute of an object.
+
+ * `?:`: Twig supports the PHP ternary operator:
+
+        [twig]
+        {{ foo ? 'yes' : 'no' }}
+
+List of Builtin Filters
+-----------------------
+
+### `date`
+
+The `date` filter is able to format a date to a given format:
+
+    [twig]
+    {{ post.published_at|date("m/d/Y") }}
+
+### `format`
+
+The `format` filter formats a given string by replacing the placeholders:
+
+
+    [twig]
+    {# string is a format string like: I like %s and %s. #}
+    {{ string|format(foo, "bar") }}
+    {# returns I like foo and bar. (if the foo parameter equals to the foo string) #}
+
+### `even`
+
+The `even` filter returns `true` if the given number is even, `false`
+otherwise:
+
+    [twig]
+    {{ var|even ? 'even' : 'odd' }}
+
+### `odd`
+
+The `odd` filter returns `true` if the given number is odd, `false`
+otherwise:
+
+    [twig]
+    {{ var|odd ? 'odd' : 'even' }}
+
+### `encoding`
+
+The `encoding` filter URL encode a given string.
+
+### `title`
+
+The `title` filter returns a titlecased version of the value. I.e. words will
+start with uppercase letters, all remaining characters are lowercase.
+
+### `capitalize`
+
+The `capitalize` filter capitalizes a value. The first character will be
+uppercase, all others lowercase.
+
+### `upper`
+
+The `upper` filter converts a value to uppercase.
+
+### `lower`
+
+The `lower` filter converts a value to lowercase.
+
+### `striptags`
+
+The `striptags` filter strips SGML/XML tags and replace adjacent whitespace by
+one space.
+
+### `join`
+
+The `join` filter returns a string which is the concatenation of the strings
+in the sequence. The separator between elements is an empty string per
+default, you can define it with the optional parameter:
+
+    [twig]
+    {{ [1, 2, 3]|join('|') }}
+    {# returns 1|2|3 #}
+
+    {{ [1, 2, 3]|join }}
+    {# returns 123 #}
+
+### `reverse`
+
+The `reverse` filter reverses an array or an object if it implements the
+`Iterator` interface.
+
+### `length`
+
+The `length` filters returns the number of items of a sequence or mapping, or
+the length of a string.
+
+### `sort`
+
+The `sort` filter sorts an array.
+
+### `default`
+
+The `default` filter returns the passed default value if the value is
+undefined, otherwise the value of the variable:
+
+    [twig]
+    {{ my_variable|default('my_variable is not defined') }}
+
+### `keys`
+
+The `keys` filter returns the keys of an array. It is useful when you want to
+iterate over the keys of an array:
+
+    [twig]
+    {% for key in array|keys %}
+        ...
+    {% endfor %}
+
+### `items`
+
+The `items` filter is mainly useful when using the `for` tag to iterate over
+both the keys and the values of an array:
+
+    [twig]
+    {% for key, value in array|items %}
+        ...
+    {% endfor %}
+
+### `escape`, `e`
+
+The `escape` filter converts the characters `&`, `<`, `>`, `'`, and `"` in
+strings to HTML-safe sequences. Use this if you need to display text that
+might contain such characters in HTML.
+
+>**NOTE**
+>Internally, `escape` uses the PHP `htmlspecialchars` function.
+
+### `safe`
+
+The `safe` filter marks the value as safe which means that in an environment
+with automatic escaping enabled this variable will not be escaped.
+
+    [twig]
+    {% autoescape on }
+      {{ var|safe }} {# var won't be escaped #}
+    {% autoescape off %}
diff --git a/doc/03-Twig-for-Developers.markdown b/doc/03-Twig-for-Developers.markdown
new file mode 100644 (file)
index 0000000..ce844c4
--- /dev/null
@@ -0,0 +1,284 @@
+Twig for Developers
+===================
+
+This chapter describes the API to Twig and not the template language. It will
+be most useful as reference to those implementing the template interface to
+the application and not those who are creating Twig templates.
+
+Basics
+------
+
+Twig uses a central object called the **environment** (of class
+`Twig_Environment`). Instances of this class are used to store the
+configuration and extensions, and are used to load templates from the file
+system or other locations.
+
+Most applications will create one `Twig_Environment` object on application
+initialization and use that to load templates. In some cases it's however
+useful to have multiple environments side by side, if different configurations
+are in use.
+
+The simplest way to configure Twig to load templates for your application
+looks roughly like this:
+
+    [php]
+    require_once '/path/to/lib/Twig/Autoloader.php';
+    Twig_Autoloader::register();
+
+    $loader = new Twig_Loader_Filesystem('/path/to/templates');
+    $twig = new Twig_Environment($loader);
+
+This will create a template environment with the default settings and a loader
+that looks up the templates in the `/path/to/templates/` folder. Different
+loaders are available and you can also write your own if you want to load
+templates from a database or other resources.
+
+To load a template from this environment you just have to call the
+`loadTemplate()` method which then returns a `Twig_Template` instance:
+
+    [php]
+    $template = $twig->loadTemplate('index.html');
+
+To render the template with some variables, call the `render()` method:
+
+    [php]
+    echo $template->render(array('the' => 'variables', 'go' => 'here'));
+
+>**NOTE**
+>The `display()` method is a shortcut to output the template directly.
+
+Environment Options
+-------------------
+
+When creating a new `Twig_Environment` instance, you can pass an array of
+options as the constructor second argument:
+
+    [php]
+    $twig = new Twig_Environment($loader, array('debug' => true));
+
+The following options are available:
+
+ * `debug`: When set to `true`, the generated templates have a `__toString()`
+   method that you can use to display the generated nodes (default to
+   `false`).
+
+ * `trim_blocks`: Mimicks the behavior of PHP by removing the newline that
+   follows instructions if present (default to `false`).
+
+ * `charset`: The charset used by the templates (default to `utf-8`).
+
+ * `base_template_class`: The base template class to use for generated
+   templates (default to `Twig_Template`).
+
+Loaders
+-------
+
+Loaders are responsible for loading templates from a resource such as the file
+system. The environment will keep the compiled templates in memory.
+
+### Built-in Loaders
+
+Here a list of the built-in loaders Twig provides:
+
+ * `Twig_Loader_Filesystem`: Loads templates from the file system. This loader
+   can find templates in folders on the file system and is the preferred way
+   to load them.
+
+ * `Twig_Loader_String`: Loads templates from a string. It's a dummy loader as
+   you pass it the source code directly.
+
+ * `Twig_Loader_Array`: Loads a template from a PHP array. It's passed an
+   array of strings bound to template names. This loader is useful for unit
+   testing.
+
+### Create your own Loader
+
+All loaders implement the `Twig_LoaderInterface`:
+
+    [php]
+    interface Twig_LoaderInterface
+    {
+      /**
+       * Loads a template by name.
+       *
+       * @param  string $name The template name
+       *
+       * @return string The class name of the compiled template
+       */
+      public function load($name);
+    }
+
+But if you want to create your own loader, you'd better inherit from the
+`Twig_Loader` class, which already provides a lot of useful features. In this
+case, you just need to implement the `getSource()` method. As an example, here
+is how the built-in `Twig_Loader_String` reads:
+
+    [php]
+    class Twig_Loader_String extends Twig_Loader
+    {
+      /**
+       * Gets the source code of a template, given its name.
+       *
+       * @param  string $name string The name of the template to load
+       *
+       * @return array An array consisting of the source code as the first element,
+       *               and the last modification time as the second one
+       *               or false if it's not relevant
+       */
+      public function getSource($name)
+      {
+        return array($name, false);
+      }
+    }
+
+Using Extensions
+----------------
+
+Twig extensions are packages that adds new features to Twig. Using an
+extension is as simple as using the `addExtension()` method:
+
+    [php]
+    $twig->addExtension('Escaper');
+
+Twig comes bundled with three extensions:
+
+ * *Core*: Defines all the core features of Twig and is automatically
+   registered when you create a new environment.
+
+ * *Escaper*: Adds automatic output-escaping and the possibility to
+   escape/unescape blocks of code.
+
+ * *Sandbox*: Adds a sandbox mode to the default Twig environment, making it
+   safe to evaluated untrusted code.
+
+Built-in Extensions
+-------------------
+
+This section describes the features added by the built-in extensions.
+
+>**TIP**
+>Read the chapter about extending Twig to learn how to create your own
+>extensions.
+
+### Core Extension
+
+The `core` extension defines all the core features of Twig:
+
+  * Tags:
+
+     * `for`
+     * `if`
+     * `extends`
+     * `include`
+     * `block`
+     * `parent`
+     * `display`
+     * `filter`
+
+  * Filters:
+
+     * `date`
+     * `format`
+     * `even`
+     * `odd`
+     * `urlencode`
+     * `title`
+     * `capitalize`
+     * `upper`
+     * `lower`
+     * `striptags`
+     * `join`
+     * `reverse`
+     * `length`
+     * `sort`
+     * `default`
+     * `keys`
+     * `items`
+     * `escape`
+     * `e`
+
+The core extension does not need to be added to the Twig environment, as it is
+registered by default.
+
+### Escaper Extension
+
+The `escaper` extension adds automatic output escaping to Twig. It defines a
+new tag, `autoescape`, and a new filter, `safe`.
+
+When creating the escaper extension, you can switch on or off the global
+output escaping strategy:
+
+    [php]
+    $escaper = new Twig_Extension_Escaper(true);
+    $twig->addExtension($escaper);
+
+If set to `true`, all variables in templates are escaped, except those using
+the `safe` filter:
+
+    [twig]
+    {{ article.to_html|safe }}
+
+You can also change the escaping mode locally by using the `autoescape` tag:
+
+    [twig]
+    {% autoescape on %}
+      {% var %}
+      {% var|safe %}     {# var won't be escaped #}
+      {% var|escape %}   {# var won't be doubled-escaped #}
+    {% endautoescape %}
+
+### Sandbox Extension
+
+The `sandbox` extension can be used to evaluate untrusted code. Access to
+unsafe attributes and methods is prohibited. The sandbox security is managed
+by a policy instance. By default, Twig comes with one policy class:
+`Twig_Sandbox_SecurityPolicy`. This class allows you to white-list some tags,
+filters, and methods:
+
+    [php]
+    $tags = array('if');
+    $filters = array('upper');
+    $methods = array(
+      'Article' => array('getTitle', 'getBody'),
+    );
+    $policy = new Twig_Sandbox_SecurityPolicy($tags, $filters, $methods);
+
+With the previous configuration, the security policy will only allow usage of
+the `if` tag, and the `upper` filter. Moreover, the templates will only be
+able to call the `getTitle()` and `getBody()` methods on `Article` objects.
+Everything else won't be allowed and will generate a
+`Twig_Sandbox_SecurityError` exception.
+
+The policy object is the first argument of the sandbox constructor:
+
+    [php]
+    $sandbox = new Twig_Extension_Sandbox($policy);
+    $twig->addExtension($sandbox);
+
+By default, the sandbox mode is disabled and should be enabled when including
+untrusted templates:
+
+    [php]
+    {% include "user.html" sandboxed %}
+
+You can sandbox all templates by passing `true` as the second argument of the
+extension constructor:
+
+    [php]
+    $sandbox = new Twig_Extension_Sandbox($policy, true);
+
+Exceptions
+----------
+
+Twig can throw exceptions:
+
+ * `Twig_Error`: The base exception for all template errors.
+
+ * `Twig_SyntaxError`: Thrown to tell the user that there is a problem with
+   the template syntax.
+
+ * `Twig_RuntimeError`: Thrown when an error occurs at runtime (when a filter
+   does not exist for instance).
+
+ * `Twig_Sandbox_SecurityError`: Thrown when an unallowed tag, filter, or
+   method is called in a sandboxed template.
diff --git a/doc/04-Extending-Twig.markdown b/doc/04-Extending-Twig.markdown
new file mode 100644 (file)
index 0000000..59676cb
--- /dev/null
@@ -0,0 +1,290 @@
+Extending Twig
+==============
+
+Twig supports extensions that can add extra tags, filters, or even extend the
+parser itself with node transformer classes. The main motivation for writing
+an extension is to move often used code into a reusable class like adding
+support for internationalization.
+
+Most of the time, it is useful to create a single extension for your project,
+to host all the specific tags and filters you want to add to Twig.
+
+Anatomy of an Extension
+-----------------------
+
+An extension is a class that implements the `Twig_ExtensionInterface`:
+
+    [php]
+    interface Twig_ExtensionInterface
+    {
+      /**
+       * Initializes the runtime environment.
+       *
+       * This is where you can load some file that contains filter functions for instance.
+       */
+      public function initRuntime();
+
+      /**
+       * Returns the token parser instances to add to the existing list.
+       *
+       * @return array An array of Twig_TokenParser instances
+       */
+      public function getTokenParsers();
+
+      /**
+       * Returns the node transformer instances to add to the existing list.
+       *
+       * @return array An array of Twig_NodeTransformer instances
+       */
+      public function getNodeTransformers();
+
+      /**
+       * Returns a list of filters to add to the existing list.
+       *
+       * @return array An array of filters
+       */
+      public function getFilters();
+
+      /**
+       * Returns the name of the extension.
+       *
+       * @return string The extension name
+       */
+      public function getName();
+    }
+
+Instead of you implementing the whole interface, your extension class can
+inherit from the `Twig_Extension` class, which provides empty implementations
+of all the above methods to keep your extension clean.
+
+>**TIP**
+>The bundled extensions are great examples of how extensions work.
+
+Defining new Filters
+--------------------
+
+The most common element you will want to add to Twig is filters. A filter is
+just a regular PHP callable that takes the left side of the filter as first
+argument and the arguments passed to the filter as extra arguments.
+
+Let's create a filter, named `rot13`, which returns the
+[rot13](http://www.php.net/manual/en/function.str-rot13.php) transformation of
+a string:
+
+    [twig]
+    {{ "Twig"|rot13 }}
+
+    {# should displays Gjvt #}
+
+Here is the simplest extension class you can create to add this filter:
+
+    [php]
+    class Project_Twig_Extension extends Twig_Extension
+    {
+      public function getFilters()
+      {
+        return array(
+          'rot13' => array('str_rot13', false),
+        );
+      }
+
+      public function getName()
+      {
+        return 'project';
+      }
+    }
+
+Registering the new extension is like registering core extensions:
+
+    [php]
+    $twig->addExtension(new Project_Twig_Extension());
+
+You can of course use any valid PHP callable, like a method call:
+
+    [php]
+    class Project_Twig_Extension extends Twig_Extension
+    {
+      public function getFilters()
+      {
+        return array(
+          'rot13' => array(array($this, 'computeRot13'), false),
+        );
+      }
+
+      public function computeRot13($string)
+      {
+        return str_rot13($string);
+      }
+
+      // ...
+    }
+
+Filters can also be passed the current environment. You might have noticed
+that a filter is defined by a callable and a Boolean. If you change the
+Boolean to `true`, Twig will pass the current environment as the first
+argument to the filter call:
+
+    [php]
+    class Project_Twig_Extension extends Twig_Extension
+    {
+      public function getFilters()
+      {
+        return array(
+          'rot13' => array(array($this, 'computeRot13'), true),
+        );
+      }
+
+      public function computeRot13(Twig_Environment $env, $string)
+      {
+        // get the current charset for instance
+        $charset = $env->getCharset();
+
+        return str_rot13($string);
+      }
+
+      // ...
+    }
+
+Defining new Tags
+-----------------
+
+One of the most exiting feature of a template engine like Twig is the
+possibility to define new language constructs.
+
+Let's create a simple `set` tag that allows the definition of simple variables
+from within a template. The tag can be used like follows:
+
+    [twig]
+    {% set name "value" %}
+
+    {{ name }}
+
+    {# should output value #}
+
+First, we need to create a `Twig_TokenParser` class which will be able to
+parse this new language construct:
+
+    [php]
+    class Project_Twig_Set_TokenParser extends Twig_TokenParser
+    {
+      // ...
+    }
+
+Of course, we need to register this token parser in our extension class:
+
+    [php]
+    class Project_Twig_Extension extends Twig_Extension
+    {
+      public function getTokenParsers()
+      {
+        return array(new Project_Twig_Set_TokenParser());
+      }
+
+      // ...
+    }
+
+Now, let's see the actual code of the token parser class:
+
+    [php]
+    class Project_Twig_Set_TokenParser extends Twig_TokenParser
+    {
+      public function parse(Twig_Token $token)
+      {
+        $lineno = $token->getLine();
+        $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+        $value = $this->parser->getExpressionParser()->parseExpression();
+
+        $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+        return new Project_Twig_Set_Node($name, $value, $lineno, $this->getTag());
+      }
+
+      public function getTag()
+      {
+        return 'set';
+      }
+    }
+
+The `getTag()` method must return the tag we want to parse, here `set`. The
+`parse()` method is invoked whenever the parser encounters a `set` tag. It
+should return a `Twig_Node` instance that represents the node. The parsing
+process is simplified thanks to a bunch of methods you can call from the token
+stream (`$this->parser->getStream()`):
+
+ * `test()`: Tests the type and optionally the value of the next token and
+   returns it.
+
+ * `expect()`: Expects a token and returns it (like `test()`) or throw a
+   syntax error if not found.
+
+ * `look()`: Looks a the next token. This is how you can have a look at the
+   next token without consume it.
+
+Parsing expressions is done by calling the `parseExpression()` like we did for
+the `set` tag.
+
+>**TIP**
+>Reading the existing `TokenParser` classes is the best way to learn all the
+>nitty-gritty details of the parsing process.
+
+The `Project_Twig_Set_Node` class itself is rather simple:
+
+    [php]
+    class Project_Twig_Set_Node extends Twig_Node
+    {
+      protected $name;
+      protected $value;
+
+      public function __construct($name, Twig_Node_Expression $value, $lineno)
+      {
+        parent::__construct($lineno);
+
+        $this->name = $name;
+        $this->value = $value;
+      }
+
+      public function compile($compiler)
+      {
+        $compiler
+          ->addDebugInfo($this)
+          ->write('$context[\''.$this->name.'\'] = ')
+          ->subcompile($this->value)
+          ->raw(";\n")
+        ;
+      }
+    }
+
+The compiler implements a fluid interface and provides methods that helps the
+developer generate beautiful and readable PHP code:
+
+ * `subcompile()`: Compiles a node.
+
+ * `raw()`: Writes the given string as is.
+
+ * `write()`: Writes the given string by adding indentation at the beginning
+   of each line.
+
+ * `string()`: Writes a quoted string.
+
+ * `repr()`: Writes a PHP representation of a given value (see `Twig_Node_For`
+   for a usage example).
+
+ * `pushContext()`: Pushes the current context on the stack (see
+   `Twig_Node_For` for a usage example).
+
+ * `popContext()`: Pops a context from the stack (see `Twig_Node_For` for a
+   usage example).
+
+ * `addDebugInfo()`: Adds the line of the original template file related to
+   the current node as a comment.
+
+ * `indent()`: Indents the generated code (see `Twig_Node_Block` for a usage
+   example).
+
+ * `outdent()`: Outdents the generated code (see `Twig_Node_Block` for a usage
+   example).
+
+Creating a Node Transformer
+---------------------------
+
+To be written...
diff --git a/doc/05-Hacking-Twig.markdown b/doc/05-Hacking-Twig.markdown
new file mode 100644 (file)
index 0000000..74afd0d
--- /dev/null
@@ -0,0 +1,196 @@
+Hacking Twig
+============
+
+Twig is very extensible and you can easily hack it. Keep in mind that you
+should probably try to create an extension before hacking the core, as most
+features and enhancements can be done with extensions. This chapter is also
+useful for people who want to understand how Twig works under the hood.
+
+How Twig works?
+---------------
+
+The rendering of a Twig template can be summarized into four key steps:
+
+ * **Load** the template: If the template is already compiled, load it and go
+   to the *evaluation* step, otherwise:
+
+   * First, the **lexer** tokenizes the template source code into small pieces
+     for easier processing;
+
+   * Then, the **parser** converts the token stream into a meaningful tree
+     of nodes (the Abstract Syntax Tree);
+
+   * Eventually, the *compiler* transforms the AST into PHP code;
+
+ * **Evaluate** the template: It basically means calling the `display()`
+   method of the compiled template and passing it the context.
+
+The Lexer
+---------
+
+The Twig lexer goal is to tokenize a source code into a token stream (each
+token is of class `Token`, and the stream is an instance of
+`Twig_TokenStream`). The default lexer recognizes nine different token types:
+
+  * `Twig_Token::TEXT_TYPE`
+  * `Twig_Token::BLOCK_START_TYPE`
+  * `Twig_Token::VAR_START_TYPE`
+  * `Twig_Token::BLOCK_END_TYPE`
+  * `Twig_Token::VAR_END_TYPE`
+  * `Twig_Token::NAME_TYPE`
+  * `Twig_Token::NUMBER_TYPE`
+  * `Twig_Token::STRING_TYPE`
+  * `Twig_Token::OPERATOR_TYPE`
+  * `Twig_Token::EOF_TYPE`
+
+You can manually convert a source code into a token stream by calling the
+`tokenize()` of an environment:
+
+    [php]
+    $stream = $twig->tokenize($source, $identifier);
+
+As the stream has a `__toString()` method, you can have a textual
+representation of it by echoing the object:
+
+    [php]
+    echo $stream."\n";
+
+Here is the output for the `Hello {{ name }}` template:
+
+    [txt]
+    TEXT_TYPE(Hello )
+    VAR_START_TYPE()
+    NAME_TYPE(name)
+    VAR_END_TYPE()
+    EOF_TYPE()
+
+You can change the default lexer use by Twig (`Twig_Lexer`) by calling the
+`setLexer()` method:
+
+    [php]
+    $twig->setLexer($lexer);
+
+Lexer classes must implement the `Twig_LexerInterface`:
+
+    [php]
+    interface Twig_LexerInterface
+    {
+      /**
+       * Tokenizes a source code.
+       *
+       * @param  string $code     The source code
+       * @param  string $filename A unique identifier for the source code
+       *
+       * @return Twig_TokenStream A token stream instance
+       */
+      public function tokenize($code, $filename = 'n/a');
+    }
+
+The Parser
+----------
+
+The parser converts the token stream into an AST (Abstract Syntax Tree), or a
+node tree (of class `Twig_Node_Module`). The core extension defines the basic
+nodes like: `for`, `if`, ... and the expression nodes.
+
+You can manually convert a token stream into a node tree by calling the
+`parse()` method of an environment:
+
+    [php]
+    $nodes = $twig->parse($stream);
+
+Echoing the node object gives you a nice representation of the tree:
+
+    [php]
+    echo $nodes."\n";
+
+Here is the output for the `Hello {{ name }}` template:
+
+    [txt]
+    Twig_Node_Module(
+      Twig_Node_Text(Hello )
+      Twig_Node_Print(
+        Twig_Node_Expression_Name(name)
+      )
+    )
+
+The default parser (`Twig_TokenParser`) can be also changed by calling the
+`setParser()` method:
+
+    [php]
+    $twig->setParser($parser);
+
+All Twig parsers must implement the `Twig_ParserInterface`:
+
+    [php]
+    interface Twig_ParserInterface
+    {
+      /**
+       * Converts a token stream to a node tree.
+       *
+       * @param  Twig_TokenStream $stream A token stream instance
+       *
+       * @return Twig_Node_Module A node tree
+       */
+      public function parser(Twig_TokenStream $code);
+    }
+
+The Compiler
+------------
+
+The last step is done by the compiler. It takes a node tree as an input and
+generates PHP code usable for runtime execution of the templates. The default
+compiler generates PHP classes to ease the implementation of the template
+inheritance feature.
+
+You can call the compiler by hand with the `compile()` method of an
+environment:
+
+    [php]
+    $php = $twig->compile($nodes);
+
+The `compile()` method returns the PHP source code representing the node.
+
+The generated template for a `Hello {{ name }}` template reads as follows:
+
+    [php]
+    /* Hello {{ name }} */
+    class __TwigTemplate_1121b6f109fe93ebe8c6e22e3712bceb extends Twig_Template
+    {
+      public function display($context)
+      {
+        $this->env->initRuntime();
+
+        // line 1
+        echo "Hello ";
+        echo (isset($context['name']) ? $context['name'] : null);
+      }
+    }
+
+As for the lexer and the parser, the default compiler (`Twig_Compiler`) can be
+changed by calling the `setCompiler()` method:
+
+    [php]
+    $twig->setCompiler($compiler);
+
+All Twig compilers must implement the `Twig_CompilerInterface`:
+
+    [php]
+    interface Twig_CompilerInterface
+    {
+      /**
+       * Compiles a node.
+       *
+       * @param  Twig_Node $node The node to compile
+       *
+       * @return Twig_Compiler The current compiler instance
+       */
+      public function compile(Twig_Node $node);
+
+      /**
+       * Gets the current PHP code after compilation.
+       *
+       * @return string The PHP code
+       */
+      public function getSource();
+    }
diff --git a/lib/Twig/Autoloader.php b/lib/Twig/Autoloader.php
new file mode 100644 (file)
index 0000000..bb30b6b
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Autoloads Twig classes.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Autoloader
+{
+  /**
+   * Registers Twig_Autoloader as an SPL autoloader.
+   */
+  static public function register()
+  {
+    ini_set('unserialize_callback_func', 'spl_autoload_call');
+    spl_autoload_register(array(new self, 'autoload'));
+  }
+
+  /**
+   * Handles autoloading of classes.
+   *
+   * @param  string  $class  A class name.
+   *
+   * @return boolean Returns true if the class has been loaded
+   */
+  public function autoload($class)
+  {
+    if (0 !== strpos($class, 'Twig'))
+    {
+      return false;
+    }
+
+    require dirname(__FILE__).'/../'.str_replace('_', '/', $class).'.php';
+
+    return true;
+  }
+}
diff --git a/lib/Twig/Compiler.php b/lib/Twig/Compiler.php
new file mode 100644 (file)
index 0000000..552febe
--- /dev/null
@@ -0,0 +1,245 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Compiles a node to PHP code.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Compiler implements Twig_CompilerInterface
+{
+  protected $lastLine;
+  protected $source;
+  protected $indentation;
+  protected $env;
+
+  /**
+   * Constructor.
+   *
+   * @param Twig_Environment $env The twig environment instance
+   */
+  public function __construct(Twig_Environment $env = null)
+  {
+    $this->env = $env;
+  }
+
+  public function setEnvironment(Twig_Environment $env)
+  {
+    $this->env = $env;
+  }
+
+  /**
+   * Gets the current PHP code after compilation.
+   *
+   * @return string The PHP code
+   */
+  public function getSource()
+  {
+    return $this->source;
+  }
+
+  /**
+   * Compiles a node.
+   *
+   * @param  Twig_Node $node The node to compile
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function compile(Twig_Node $node)
+  {
+    $this->lastLine = null;
+    $this->source = '';
+    $this->indentation = 0;
+
+    $node->compile($this);
+
+    return $this;
+  }
+
+  public function subcompile(Twig_Node $node)
+  {
+    $node->compile($this);
+
+    return $this;
+  }
+
+  /**
+   * Adds a raw string to the compiled code.
+   *
+   * @param  string $string The string
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function raw($string)
+  {
+    $this->source .= $string;
+
+    return $this;
+  }
+
+  /**
+   * Writes a string to the compiled code by adding indentation.
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function write()
+  {
+    $strings = func_get_args();
+    foreach ($strings as $string)
+    {
+      $this->source .= str_repeat(' ', $this->indentation * 2).$string;
+    }
+
+    return $this;
+  }
+
+  /**
+   * Adds a quoted string to the compiled code.
+   *
+   * @param  string $string The string
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function string($value)
+  {
+    $this->source .= sprintf('"%s"', addcslashes($value, "\t\""));
+
+    return $this;
+  }
+
+  /**
+   * Returns a PHP representation of a given value.
+   *
+   * @param  mixed $value The value to convert
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function repr($value)
+  {
+    if (is_int($value) || is_float($value))
+    {
+      $this->raw($value);
+    }
+    else if (is_null($value))
+    {
+      $this->raw('null');
+    }
+    else if (is_bool($value))
+    {
+      $this->raw($value ? 'true' : 'false');
+    }
+    else if (is_array($value))
+    {
+      $this->raw('array(');
+      $i = 0;
+      foreach ($value as $key => $value)
+      {
+        if ($i++)
+        {
+          $this->raw(', ');
+        }
+        $this->repr($key);
+        $this->raw(' => ');
+        $this->repr($value);
+      }
+      $this->raw(')');
+    }
+    else
+    {
+      $this->string($value);
+    }
+
+    return $this;
+  }
+
+  /**
+   * Pushes the current context on the stack.
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function pushContext()
+  {
+    $this->write('$context[\'_parent\'] = $context;'."\n");
+
+    return $this;
+  }
+
+  /**
+   * Pops a context from the stack.
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function popContext()
+  {
+    $this->write('$context = $context[\'_parent\'];'."\n");
+
+    return $this;
+  }
+
+  /**
+   * Adds debugging information.
+   *
+   * @param Twig_Node $node The related twig node
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function addDebugInfo(Twig_Node $node)
+  {
+    if ($node->getLine() != $this->lastLine)
+    {
+      $this->lastLine = $node->getLine();
+      $this->write("// line {$node->getLine()}\n");
+    }
+
+    return $this;
+  }
+
+  /**
+   * Indents the generated code.
+   *
+   * @param integer $indent The number of indentation to add
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function indent($step = 1)
+  {
+    $this->indentation += $step;
+
+    return $this;
+  }
+
+  /**
+   * Outdents the generated code.
+   *
+   * @param integer $indent The number of indentation to remove
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function outdent($step = 1)
+  {
+    $this->indentation -= $step;
+
+    return $this;
+  }
+
+  /**
+   * Returns the environment instance related to this compiler.
+   *
+   * @return Twig_Environment The environment instance
+   */
+  public function getEnvironment()
+  {
+    return $this->env;
+  }
+}
diff --git a/lib/Twig/CompilerInterface.php b/lib/Twig/CompilerInterface.php
new file mode 100644 (file)
index 0000000..365b59f
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Interface implemented by compiler classes.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+interface Twig_CompilerInterface
+{
+  /**
+   * Compiles a node.
+   *
+   * @param  Twig_Node $node The node to compile
+   *
+   * @return Twig_Compiler The current compiler instance
+   */
+  public function compile(Twig_Node $node);
+
+  /**
+   * Gets the current PHP code after compilation.
+   *
+   * @return string The PHP code
+   */
+  public function getSource();
+}
diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php
new file mode 100644 (file)
index 0000000..c807a97
--- /dev/null
@@ -0,0 +1,251 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Environment
+{
+  const VERSION = '0.9-DEV';
+
+  protected $charset;
+  protected $loader;
+  protected $trimBlocks;
+  protected $debug;
+  protected $lexer;
+  protected $parser;
+  protected $compiler;
+  protected $baseTemplateClass;
+  protected $extensions;
+  protected $parsers;
+  protected $transformers;
+  protected $filters;
+
+  public function __construct(Twig_LoaderInterface $loader = null, $options = array())
+  {
+    if (null !== $loader)
+    {
+      $this->setLoader($loader);
+    }
+
+    $this->debug             = isset($options['debug']) ? (bool) $options['debug'] : false;
+    $this->trimBlocks        = isset($options['trim_blocks']) ? (bool) $options['trim_blocks'] : false;
+    $this->charset           = isset($options['charset']) ? (bool) $options['charset'] : 'UTF-8';
+    $this->baseTemplateClass = isset($options['base_template_class']) ? (bool) $options['base_template_class'] : 'Twig_Template';
+    $this->extensions        = array(new Twig_Extension_Core());
+  }
+
+  public function getBaseTemplateClass()
+  {
+    return $this->baseTemplateClass;
+  }
+
+  public function setBaseTemplateClass($class)
+  {
+    $this->baseTemplateClass = $class;
+  }
+
+  public function enableDebug()
+  {
+    $this->debug = true;
+  }
+
+  public function disableDebug()
+  {
+    $this->debug = false;
+  }
+
+  public function isDebug()
+  {
+    return $this->debug;
+  }
+
+  public function getTrimBlocks()
+  {
+    return $this->trimBlocks;
+  }
+
+  public function setTrimBlocks($bool)
+  {
+    $this->trimBlocks = (bool) $bool;
+  }
+
+  public function loadTemplate($name)
+  {
+    $cls = $this->getLoader()->load($name, $this);
+
+    return new $cls($this);
+  }
+
+  public function getLexer()
+  {
+    if (null === $this->lexer)
+    {
+      $this->lexer = new Twig_Lexer($this);
+    }
+
+    return $this->lexer;
+  }
+
+  public function setLexer(Twig_LexerInterface $lexer)
+  {
+    $this->lexer = $lexer;
+    $lexer->setEnvironment($this);
+  }
+
+  public function tokenize($source, $name = null)
+  {
+    return $this->getLexer()->tokenize($source, null === $name ? $source : $name);
+  }
+
+  public function getParser()
+  {
+    if (null === $this->parser)
+    {
+      $this->parser = new Twig_Parser($this);
+    }
+
+    return $this->parser;
+  }
+
+  public function setParser(Twig_ParserInterface $parser)
+  {
+    $this->parser = $parser;
+    $parser->setEnvironment($this);
+  }
+
+  public function parse(Twig_TokenStream $tokens)
+  {
+    return $this->getParser()->parse($tokens);
+  }
+
+  public function getCompiler()
+  {
+    if (null === $this->compiler)
+    {
+      $this->compiler = new Twig_Compiler($this);
+    }
+
+    return $this->compiler;
+  }
+
+  public function setCompiler(Twig_CompilerInterface $compiler)
+  {
+    $this->compiler = $compiler;
+    $compiler->setEnvironment($this);
+  }
+
+  public function compile(Twig_Node $node)
+  {
+    return $this->getCompiler()->compile($node)->getSource();
+  }
+
+  public function setLoader(Twig_LoaderInterface $loader)
+  {
+    $this->loader = $loader;
+    $loader->setEnvironment($this);
+  }
+
+  public function getLoader()
+  {
+    return $this->loader;
+  }
+
+  public function setCharset($charset)
+  {
+    $this->charset = $charset;
+  }
+
+  public function getCharset()
+  {
+    return $this->charset;
+  }
+
+  public function initRuntime()
+  {
+    foreach ($this->getExtensions() as $extension)
+    {
+      $extension->initRuntime();
+    }
+  }
+
+  public function hasExtension($name)
+  {
+    return isset($this->extensions[$name]);
+  }
+
+  public function getExtension($name)
+  {
+    return $this->extensions[$name];
+  }
+
+  public function addExtension(Twig_ExtensionInterface $extension)
+  {
+    $this->extensions[$extension->getName()] = $extension;
+  }
+
+  public function removeExtension($name)
+  {
+    unset($this->extensions[$name]);
+  }
+
+  public function setExtensions(array $extensions)
+  {
+    foreach ($extensions as $extension)
+    {
+      $this->setExtension($extension);
+    }
+  }
+
+  public function getExtensions()
+  {
+    return $this->extensions;
+  }
+
+  public function getTokenParsers()
+  {
+    if (null === $this->parsers)
+    {
+      $this->parsers = array();
+      foreach ($this->getExtensions() as $extension)
+      {
+        $this->parsers = array_merge($this->parsers, $extension->getTokenParsers());
+      }
+    }
+
+    return $this->parsers;
+  }
+
+  public function getNodeTransformers()
+  {
+    if (null === $this->transformers)
+    {
+      $this->transformers = array();
+      foreach ($this->getExtensions() as $extension)
+      {
+        $this->transformers = array_merge($this->transformers, $extension->getNodeTransformers());
+      }
+    }
+
+    return $this->transformers;
+  }
+
+  public function getFilters()
+  {
+    if (null === $this->filters)
+    {
+      $this->filters = array();
+      foreach ($this->getExtensions() as $extension)
+      {
+        $this->filters = array_merge($this->filters, $extension->getFilters());
+      }
+    }
+
+    return $this->filters;
+  }
+}
diff --git a/lib/Twig/Error.php b/lib/Twig/Error.php
new file mode 100644 (file)
index 0000000..b88fee4
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Twig base exception.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Error extends Exception
+{
+}
diff --git a/lib/Twig/ExpressionParser.php b/lib/Twig/ExpressionParser.php
new file mode 100644 (file)
index 0000000..6cb5441
--- /dev/null
@@ -0,0 +1,397 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Parses expressions.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_ExpressionParser
+{
+  protected $parser;
+
+  public function __construct(Twig_Parser $parser)
+  {
+    $this->parser = $parser;
+  }
+
+  public function parseExpression()
+  {
+    return $this->parseConditionalExpression();
+  }
+
+  public function parseConditionalExpression()
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $expr1 = $this->parseOrExpression();
+    while ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, '?'))
+    {
+      $this->parser->getStream()->next();
+      $expr2 = $this->parseOrExpression();
+      $this->parser->getStream()->expect(Twig_Token::OPERATOR_TYPE, ':');
+      $expr3 = $this->parseConditionalExpression();
+      $expr1 = new Twig_Node_Expression_Conditional($expr1, $expr2, $expr3, $lineno);
+      $lineno = $this->parser->getCurrentToken()->getLine();
+    }
+
+    return $expr1;
+  }
+
+  public function parseOrExpression()
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $left = $this->parseAndExpression();
+    while ($this->parser->getStream()->test('or'))
+    {
+      $this->parser->getStream()->next();
+      $right = $this->parseAndExpression();
+      $left = new Twig_Node_Expression_Binary_Or($left, $right, $lineno);
+      $lineno = $this->parser->getCurrentToken()->getLine();
+    }
+
+    return $left;
+  }
+
+  public function parseAndExpression()
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $left = $this->parseCompareExpression();
+    while ($this->parser->getStream()->test('and'))
+    {
+      $this->parser->getStream()->next();
+      $right = $this->parseCompareExpression();
+      $left = new Twig_Node_Expression_Binary_And($left, $right, $lineno);
+      $lineno = $this->parser->getCurrentToken()->getLine();
+    }
+
+    return $left;
+  }
+
+  public function parseCompareExpression()
+  {
+    static $operators = array('==', '!=', '<', '>', '>=', '<=');
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $expr = $this->parseAddExpression();
+    $ops = array();
+    while ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, $operators))
+    {
+      $ops[] = array($this->parser->getStream()->next()->getValue(), $this->parseAddExpression());
+    }
+
+    if (empty($ops))
+    {
+      return $expr;
+    }
+
+    return new Twig_Node_Expression_Compare($expr, $ops, $lineno);
+  }
+
+  public function parseAddExpression()
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $left = $this->parseSubExpression();
+    while ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, '+'))
+    {
+      $this->parser->getStream()->next();
+      $right = $this->parseSubExpression();
+      $left = new Twig_Node_Expression_Binary_Add($left, $right, $lineno);
+      $lineno = $this->parser->getCurrentToken()->getLine();
+    }
+
+    return $left;
+  }
+
+  public function parseSubExpression()
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $left = $this->parseConcatExpression();
+    while ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, '-'))
+    {
+      $this->parser->getStream()->next();
+      $right = $this->parseConcatExpression();
+      $left = new Twig_Node_Expression_Binary_Sub($left, $right, $lineno);
+      $lineno = $this->parser->getCurrentToken()->getLine();
+    }
+
+    return $left;
+  }
+
+  public function parseConcatExpression()
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $left = $this->parseMulExpression();
+    while ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, '~'))
+    {
+      $this->parser->getStream()->next();
+      $right = $this->parseMulExpression();
+      $left = new Twig_Node_Expression_Binary_Concat($left, $right, $lineno);
+      $lineno = $this->parser->getCurrentToken()->getLine();
+    }
+
+    return $left;
+  }
+
+  public function parseMulExpression()
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $left = $this->parseDivExpression();
+    while ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, '*'))
+    {
+      $this->parser->getStream()->next();
+      $right = $this->parseDivExpression();
+      $left = new Twig_Node_Expression_Binary_Mul($left, $right, $lineno);
+      $lineno = $this->parser->getCurrentToken()->getLine();
+    }
+
+    return $left;
+  }
+
+  public function parseDivExpression()
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $left = $this->parseModExpression();
+    while ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, '/'))
+    {
+      $this->parser->getStream()->next();
+      $right = $this->parseModExpression();
+      $left = new Twig_Node_Expression_Binary_Div($left, $right, $lineno);
+      $lineno = $this->parser->getCurrentToken()->getLine();
+    }
+
+    return $left;
+  }
+
+  public function parseModExpression()
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $left = $this->parseUnaryExpression();
+    while ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, '%'))
+    {
+      $this->parser->getStream()->next();
+      $right = $this->parseUnaryExpression();
+      $left = new Twig_Node_Expression_Binary_Mod($left, $right, $lineno);
+      $lineno = $this->parser->getCurrentToken()->getLine();
+    }
+
+    return $left;
+  }
+
+  public function parseUnaryExpression()
+  {
+    if ($this->parser->getStream()->test('not'))
+    {
+      return $this->parseNotExpression();
+    }
+    if ($this->parser->getCurrentToken()->getType() == Twig_Token::OPERATOR_TYPE)
+    {
+      switch ($this->parser->getCurrentToken()->getValue())
+      {
+        case '-':
+          return $this->parseNegExpression();
+        case '+':
+          return $this->parsePosExpression();
+      }
+    }
+
+    return $this->parsePrimaryExpression();
+  }
+
+  public function parseNotExpression()
+  {
+    $token = $this->parser->getStream()->next();
+    $node = $this->parseUnaryExpression();
+
+    return new Twig_Node_Expression_Unary_Not($node, $token->getLine());
+  }
+
+  public function parseNegExpression()
+  {
+    $token = $this->parser->getStream()->next();
+    $node = $this->parseUnaryExpression();
+
+    return new Twig_Node_Expression_Unary_Neg($node, $token->getLine());
+  }
+
+  public function parsePosExpression()
+  {
+    $token = $this->parser->getStream()->next();
+    $node = $this->parseUnaryExpression();
+
+    return new Twig_Node_Expression_Unary_Pos($node, $token->getLine());
+  }
+
+  public function parsePrimaryExpression($assignment = false)
+  {
+    $token = $this->parser->getCurrentToken();
+    switch ($token->getType())
+    {
+      case Twig_Token::NAME_TYPE:
+        $this->parser->getStream()->next();
+        switch ($token->getValue())
+        {
+          case 'true':
+            $node = new Twig_Node_Expression_Constant(true, $token->getLine());
+            break;
+
+          case 'false':
+            $node = new Twig_Node_Expression_Constant(false, $token->getLine());
+            break;
+
+          case 'none':
+            $node = new Twig_Node_Expression_Constant(null, $token->getLine());
+            break;
+
+          default:
+            $cls = $assignment ? 'Twig_Node_Expression_AssignName' : 'Twig_Node_Expression_Name';
+            $node = new $cls($token->getValue(), $token->getLine());
+        }
+        break;
+
+      case Twig_Token::NUMBER_TYPE:
+      case Twig_Token::STRING_TYPE:
+        $this->parser->getStream()->next();
+        $node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
+        break;
+
+      default:
+        if ($token->test(Twig_Token::OPERATOR_TYPE, '('))
+        {
+          $this->parser->getStream()->next();
+          $node = $this->parseExpression();
+          $this->parser->getStream()->expect(Twig_Token::OPERATOR_TYPE, ')');
+        }
+        else
+        {
+          throw new Twig_SyntaxError('Unexpected token', $token->getLine());
+        }
+    }
+    if (!$assignment)
+    {
+      $node = $this->parsePostfixExpression($node);
+    }
+
+    return $node;
+  }
+
+  public function parsePostfixExpression($node)
+  {
+    $stop = false;
+    while (!$stop && $this->parser->getCurrentToken()->getType() == Twig_Token::OPERATOR_TYPE)
+    {
+      switch ($this->parser->getCurrentToken()->getValue())
+      {
+        case '.':
+        case '[':
+          $node = $this->parseSubscriptExpression($node);
+          break;
+
+        case '|':
+          $node = $this->parseFilterExpression($node);
+          break;
+
+        default:
+          $stop = true;
+          break;
+      }
+    }
+
+    return $node;
+  }
+
+  public function parseSubscriptExpression($node)
+  {
+    $token = $this->parser->getStream()->next();
+    $lineno = $token->getLine();
+    if ($token->getValue() == '.')
+    {
+      $token = $this->parser->getStream()->next();
+      if ($token->getType() == Twig_Token::NAME_TYPE || $token->getType() == Twig_Token::NUMBER_TYPE)
+      {
+        $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno);
+      }
+      else
+      {
+        throw new Twig_SyntaxError('Expected name or number', $lineno);
+      }
+    }
+    else
+    {
+      $arg = $this->parseExpression();
+      $this->parser->getStream()->expect(Twig_Token::OPERATOR_TYPE, ']');
+    }
+
+    return new Twig_Node_Expression_GetAttr($node, $arg, $lineno, $token->getValue());
+  }
+
+  public function parseFilterExpression($node)
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $filters = array();
+    while ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, '|'))
+    {
+      $this->parser->getStream()->next();
+      $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE);
+      $args = array();
+      if ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, '('))
+      {
+        $this->parser->getStream()->next();
+        while (!$this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, ')'))
+        {
+          if (!empty($args))
+          {
+            $this->parser->getStream()->expect(Twig_Token::OPERATOR_TYPE, ',');
+          }
+          $args[] = $this->parseExpression();
+        }
+        $this->parser->getStream()->expect(Twig_Token::OPERATOR_TYPE, ')');
+      }
+      $filters[] = array($token->getValue(), $args);
+    }
+
+    return new Twig_Node_Expression_Filter($node, $filters, $lineno);
+  }
+
+  public function parseAssignmentExpression()
+  {
+    $lineno = $this->parser->getCurrentToken()->getLine();
+    $targets = array();
+    $is_multitarget = false;
+    while (true)
+    {
+      if (!empty($targets))
+      {
+        $this->parser->getStream()->expect(Twig_Token::OPERATOR_TYPE, ',');
+      }
+      if ($this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, ')') ||
+          $this->parser->getStream()->test(Twig_Token::VAR_END_TYPE) ||
+          $this->parser->getStream()->test(Twig_Token::BLOCK_END_TYPE) ||
+          $this->parser->getStream()->test('in'))
+      {
+        break;
+      }
+      $targets[] = $this->parsePrimaryExpression(true);
+      if (!$this->parser->getStream()->test(Twig_Token::OPERATOR_TYPE, ','))
+      {
+        break;
+      }
+      $is_multitarget = true;
+    }
+    if (!$is_multitarget && count($targets) == 1)
+    {
+      return array(false, $targets[0]);
+    }
+
+    return array(true, $targets);
+  }
+}
diff --git a/lib/Twig/Extension.php b/lib/Twig/Extension.php
new file mode 100644 (file)
index 0000000..82046e9
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+abstract class Twig_Extension implements Twig_ExtensionInterface
+{
+  /**
+   * Initializes the runtime environment.
+   *
+   * This is where you can load some file that contains filter functions for instance.
+   */
+  public function initRuntime()
+  {
+  }
+
+  /**
+   * Returns the token parser instances to add to the existing list.
+   *
+   * @return array An array of Twig_TokenParser instances
+   */
+  public function getTokenParsers()
+  {
+    return array();
+  }
+
+  /**
+   * Returns the node transformer instances to add to the existing list.
+   *
+   * @return array An array of Twig_NodeTransformer instances
+   */
+  public function getNodeTransformers()
+  {
+    return array();
+  }
+
+  /**
+   * Returns a list of filters to add to the existing list.
+   *
+   * @return array An array of filters
+   */
+  public function getFilters()
+  {
+    return array();
+  }
+}
diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php
new file mode 100644 (file)
index 0000000..1b6eeca
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Extension_Core extends Twig_Extension
+{
+  /**
+   * Initializes the runtime environment.
+   *
+   * This is where you can load some file that contains filter functions for instance.
+   */
+  public function initRuntime()
+  {
+    require_once dirname(__FILE__).'/../runtime.php';
+    require_once dirname(__FILE__).'/../runtime_for.php';
+  }
+
+  /**
+   * Returns the token parser instance to add to the existing list.
+   *
+   * @return array An array of Twig_TokenParser instances
+   */
+  public function getTokenParsers()
+  {
+    return array(
+      new Twig_TokenParser_For(),
+      new Twig_TokenParser_If(),
+      new Twig_TokenParser_Extends(),
+      new Twig_TokenParser_Include(),
+      new Twig_TokenParser_Block(),
+      new Twig_TokenParser_Parent(),
+      new Twig_TokenParser_Display(),
+      new Twig_TokenParser_Filter(),
+    );
+  }
+
+  /**
+   * Returns the node transformer instances to add to the existing list.
+   *
+   * @return array An array of Twig_NodeTransformer instances
+   */
+  public function getNodeTransformers()
+  {
+    return array(new Twig_NodeTransformer_Filter());
+  }
+
+  /**
+   * Returns a list of filters to add to the existing list.
+   *
+   * @return array An array of filters
+   */
+  public function getFilters()
+  {
+    $filters = array(
+      // formatting filters
+      'date'   => array('twig_date_format_filter', false),
+      'format' => array('sprintf', false),
+
+      // numbers
+      'even' => array('twig_is_even_filter', false),
+      'odd'  => array('twig_is_odd_filter', false),
+
+      // encoding
+      'urlencode' => array('twig_urlencode_filter', false),
+
+      // string filters
+      'title'      => array('twig_title_string_filter', true),
+      'capitalize' => array('twig_capitalize_string_filter', true),
+      'upper'      => array('strtoupper', false),
+      'lower'      => array('strtolower', false),
+      'striptags'  => array('strip_tags', false),
+
+      // array helpers
+      'join'    => array('twig_join_filter', false),
+      'reverse' => array('twig_reverse_filter', false),
+      'length'  => array('twig_length_filter', false),
+      'sort'    => array('twig_sort_filter', false),
+
+      // iteration and runtime
+      'default' => array('twig_default_filter', false),
+      'keys'    => array('twig_get_array_keys_filter', false),
+      'items'   => array('twig_get_array_items_filter', false),
+
+      // escaping
+      'escape' => array('twig_escape_filter', true),
+      'e'      => array('twig_escape_filter', true),
+    );
+
+    if (function_exists('mb_get_info'))
+    {
+      $filters['upper'] = array('twig_upper_filter', true);
+      $filters['lower'] = array('twig_lower_filter', true);
+    }
+
+    return $filters;
+  }
+
+  /**
+   * Returns the name of the extension.
+   *
+   * @return string The extension name
+   */
+  public function getName()
+  {
+    return 'core';
+  }
+}
diff --git a/lib/Twig/Extension/Escaper.php b/lib/Twig/Extension/Escaper.php
new file mode 100644 (file)
index 0000000..c6ac240
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Extension_Escaper extends Twig_Extension
+{
+  protected $autoescape;
+
+  public function __construct($autoescape = true)
+  {
+    $this->autoescape = $autoescape;
+  }
+
+  /**
+   * Initializes the runtime environment.
+   *
+   * This is where you can load some file that contains filter functions for instance.
+   */
+  public function initRuntime()
+  {
+    require_once dirname(__FILE__).'/../runtime_escaper.php';
+  }
+
+  /**
+   * Returns the token parser instance to add to the existing list.
+   *
+   * @return array An array of Twig_TokenParser instances
+   */
+  public function getTokenParsers()
+  {
+    return array(new Twig_TokenParser_AutoEscape());
+  }
+
+  /**
+   * Returns the node transformer instances to add to the existing list.
+   *
+   * @return array An array of Twig_NodeTransformer instances
+   */
+  public function getNodeTransformers()
+  {
+    return array(new Twig_NodeTransformer_Escaper());
+  }
+
+  /**
+   * Returns a list of filters to add to the existing list.
+   *
+   * @return array An array of filters
+   */
+  public function getFilters()
+  {
+    return array(
+      'safe' => array('twig_safe_filter', false),
+    );
+  }
+
+  public function isGlobal()
+  {
+    return $this->autoescape;
+  }
+
+  /**
+   * Returns the name of the extension.
+   *
+   * @return string The extension name
+   */
+  public function getName()
+  {
+    return 'escaper';
+  }
+}
diff --git a/lib/Twig/Extension/Macro.php b/lib/Twig/Extension/Macro.php
new file mode 100644 (file)
index 0000000..70ed24e
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Extension_Macro extends Twig_Extension
+{
+  public function getTokenParsers()
+  {
+    return array(
+      new Twig_TokenParser_Macro(),
+      new Twig_TokenParser_Call(),
+    );
+  }
+
+  public function getName()
+  {
+    return 'macro';
+  }
+}
diff --git a/lib/Twig/Extension/Sandbox.php b/lib/Twig/Extension/Sandbox.php
new file mode 100644 (file)
index 0000000..b900803
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Extension_Sandbox extends Twig_Extension
+{
+  protected $sandboxedGlobally;
+  protected $sandboxed;
+  protected $policy;
+
+  public function __construct(Twig_Sandbox_SecurityPolicyInterface $policy, $sandboxed = false)
+  {
+    $this->policy            = $policy;
+    $this->sandboxedGlobally = $sandboxed;
+  }
+
+  /**
+   * Returns the node transformer instances to add to the existing list.
+   *
+   * @return array An array of Twig_NodeTransformer instances
+   */
+  public function getNodeTransformers()
+  {
+    return array(new Twig_NodeTransformer_Sandbox());
+  }
+
+  public function enableSandbox()
+  {
+    $this->sandboxed = true;
+  }
+
+  public function disableSandbox()
+  {
+    $this->sandboxed = false;
+  }
+
+  public function isSandboxed()
+  {
+    return $this->sandboxedGlobally || $this->sandboxed;
+  }
+
+  public function isSandboxedGlobally()
+  {
+    return $this->sandboxedGlobally;
+  }
+
+  public function setSecurityPolicy(Twig_Sandbox_SecurityPolicyInterface $policy)
+  {
+    $this->policy = $policy;
+  }
+
+  public function getSecurityPolicy()
+  {
+    return $this->policy;
+  }
+
+  public function checkSecurity($tags, $filters)
+  {
+    if ($this->isSandboxed())
+    {
+      $this->policy->checkSecurity($tags, $filters);
+    }
+  }
+
+  public function checkMethodAllowed($obj, $method)
+  {
+    if ($this->isSandboxed())
+    {
+      $this->policy->checkMethodAllowed($obj, $method);
+    }
+  }
+
+  /**
+   * Returns the name of the extension.
+   *
+   * @return string The extension name
+   */
+  public function getName()
+  {
+    return 'sandbox';
+  }
+}
diff --git a/lib/Twig/Extension/Set.php b/lib/Twig/Extension/Set.php
new file mode 100644 (file)
index 0000000..2524575
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+class Twig_Extension_Set extends Twig_Extension
+{
+  public function getTokenParsers()
+  {
+    return array(
+      new Twig_TokenParser_Set(),
+    );
+  }
+
+  public function getName()
+  {
+    return 'set';
+  }
+}
diff --git a/lib/Twig/ExtensionInterface.php b/lib/Twig/ExtensionInterface.php
new file mode 100644 (file)
index 0000000..cbfb96c
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Interface implemented by extension classes.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+interface Twig_ExtensionInterface
+{
+  /**
+   * Initializes the runtime environment.
+   *
+   * This is where you can load some file that contains filter functions for instance.
+   */
+  public function initRuntime();
+
+  /**
+   * Returns the token parser instances to add to the existing list.
+   *
+   * @return array An array of Twig_TokenParser instances
+   */
+  public function getTokenParsers();
+
+  /**
+   * Returns the node transformer instances to add to the existing list.
+   *
+   * @return array An array of Twig_NodeTransformer instances
+   */
+  public function getNodeTransformers();
+
+  /**
+   * Returns a list of filters to add to the existing list.
+   *
+   * @return array An array of filters
+   */
+  public function getFilters();
+
+  /**
+   * Returns the name of the extension.
+   *
+   * @return string The extension name
+   */
+  public function getName();
+}
diff --git a/lib/Twig/Lexer.php b/lib/Twig/Lexer.php
new file mode 100644 (file)
index 0000000..5459736
--- /dev/null
@@ -0,0 +1,304 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Lexes a template string.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Lexer implements Twig_LexerInterface
+{
+  protected $cursor;
+  protected $position;
+  protected $end;
+  protected $pushedBack;
+  protected $code;
+  protected $lineno;
+  protected $filename;
+  protected $env;
+
+  const TAG_COMMENT_START  = '{#';
+  const TAG_COMMENT_END    = '#}';
+
+  const TAG_BLOCK_START    = '{%';
+  const TAG_BLOCK_END      = '%}';
+
+  const TAG_VARIABLE_START = '{{';
+  const TAG_VARIABLE_END   = '}}';
+
+  const POSITION_DATA  = 0;
+  const POSITION_BLOCK = 1;
+  const POSITION_VAR   = 2;
+
+  const REGEX_NAME     = '/[A-Za-z_][A-Za-z0-9_]*/A';
+  const REGEX_NUMBER   = '/[0-9]+(?:\.[0-9])?/A';
+  const REGEX_STRING   = '/(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')/Asm';
+  const REGEX_OPERATOR = '/<=? | >=? | [!=]= | [(){}.,%*\/+~|-] | \[ | \] | \? | \:/Ax';
+
+  public function __construct(Twig_Environment $env = null)
+  {
+    $this->env = $env;
+  }
+
+  /**
+   * Tokenizes a source code.
+   *
+   * @param  string $code     The source code
+   * @param  string $filename A unique identifier for the source code
+   *
+   * @return Twig_TokenStream A token stream instance
+   */
+  public function tokenize($code, $filename = 'n/a')
+  {
+    $this->code = preg_replace('/(\r\n|\r|\n)/', '\n', $code);
+    $this->filename = $filename;
+    $this->cursor = 0;
+    $this->lineno = 1;
+    $this->pushedBack = array();
+    $this->end = strlen($this->code);
+    $this->position = self::POSITION_DATA;
+
+    $tokens = array();
+    $end = false;
+    while (!$end)
+    {
+      $token = $this->nextToken();
+
+      $tokens[] = $token;
+
+      $end = $token->getType() === Twig_Token::EOF_TYPE;
+    }
+
+    return new Twig_TokenStream($tokens, $this->filename, $this->env->getTrimBlocks());
+  }
+
+  public function setEnvironment(Twig_Environment $env)
+  {
+    $this->env = $env;
+  }
+
+  /**
+   * Parses the next token and returns it.
+   */
+  protected function nextToken()
+  {
+    // do we have tokens pushed back? get one
+    if (!empty($this->pushedBack))
+    {
+      return array_shift($this->pushedBack);
+    }
+
+    // have we reached the end of the code?
+    if ($this->cursor >= $this->end)
+    {
+      return new Twig_Token(Twig_Token::EOF_TYPE, '', $this->lineno);
+    }
+
+    // otherwise dispatch to the lexing functions depending
+    // on our current position in the code.
+    switch ($this->position)
+    {
+      case self::POSITION_DATA:
+        $tokens = $this->lexData();
+        break;
+
+      case self::POSITION_BLOCK:
+        $tokens = $this->lexBlock();
+        break;
+
+      case self::POSITION_VAR:
+        $tokens = $this->lexVar();
+        break;
+    }
+
+    // if the return value is not an array it's a token
+    if (!is_array($tokens))
+    {
+      return $tokens;
+    }
+    // empty array, call again
+    else if (empty($tokens))
+    {
+      return $this->nextToken();
+    }
+    // if we have multiple items we push them to the buffer
+    else if (count($tokens) > 1)
+    {
+      $first = array_shift($tokens);
+      $this->pushedBack = $tokens;
+
+      return $first;
+    }
+    // otherwise return the first item of the array.
+    else
+    {
+      return $tokens[0];
+    }
+  }
+
+  protected function lexData()
+  {
+    $match = null;
+
+    // if no matches are left we return the rest of the template
+    // as simple text token
+    if (!preg_match('/(.*?)('.self::TAG_COMMENT_START.'|'.self::TAG_BLOCK_START.'|'.self::TAG_VARIABLE_START.')/A', $this->code, $match, null, $this->cursor))
+    {
+      $rv = new Twig_Token(Twig_Token::TEXT_TYPE, substr($this->code, $this->cursor), $this->lineno);
+      $this->cursor = $this->end;
+
+      return $rv;
+    }
+
+    // update the lineno on the instance
+    $lineno = $this->lineno;
+
+    $this->cursor += strlen($match[0]);
+    $this->lineno += substr_count($match[0], '\n');
+
+    // array of tokens
+    $result = array();
+
+    // push the template text first
+    $text = $match[1];
+    if (!empty($text))
+    {
+      $result[] = new Twig_Token(Twig_Token::TEXT_TYPE, $text, $lineno);
+      $lineno += substr_count($text, '\n');
+    }
+
+    $token = $match[2];
+    switch ($token)
+    {
+      case self::TAG_COMMENT_START:
+        if (!preg_match('/(.*?)'.self::TAG_COMMENT_END.'/A', $this->code, $match, null, $this->cursor))
+        {
+          throw new Twig_SyntaxError('unclosed comment', $this->lineno, $this->filename);
+        }
+        $this->cursor += strlen($match[0]);
+        $this->lineno += substr_count($match[0], '\n');
+        break;
+
+      case self::TAG_BLOCK_START:
+        // raw data?
+        if (preg_match('/\s*raw\s*'.self::TAG_BLOCK_END.'(.*?)'.self::TAG_BLOCK_START.'\s*endraw\s*'.self::TAG_BLOCK_END.'/A', $this->code, $match, null, $this->cursor))
+        {
+          $result[] = new Twig_Token(Twig_Token::TEXT_TYPE, $match[1], $lineno);
+          $this->cursor += strlen($match[0]);
+          $this->lineno += substr_count($match[0], '\n');
+          $this->position = self::POSITION_DATA;
+        }
+        else
+        {
+          $result[] = new Twig_Token(Twig_Token::BLOCK_START_TYPE, '', $lineno);
+          $this->position = self::POSITION_BLOCK;
+        }
+        break;
+
+      case self::TAG_VARIABLE_START:
+        $result[] = new Twig_Token(Twig_Token::VAR_START_TYPE, '', $lineno);
+        $this->position = self::POSITION_VAR;
+        break;
+    }
+
+    return $result;
+  }
+
+  protected function lexBlock()
+  {
+    if (preg_match('/\s*'.self::TAG_BLOCK_END.'/A', $this->code, $match, null, $this->cursor))
+    {
+      $lineno = $this->lineno;
+      $this->cursor += strlen($match[0]);
+      $this->lineno += substr_count($match[0], '\n');
+      $this->position = self::POSITION_DATA;
+
+      return new Twig_Token(Twig_Token::BLOCK_END_TYPE, '', $lineno);
+    }
+
+    return $this->lexExpression();
+  }
+
+  protected function lexVar()
+  {
+    if (preg_match('/\s*'.self::TAG_VARIABLE_END.'/A', $this->code, $match, null, $this->cursor))
+    {
+      $lineno = $this->lineno;
+      $this->cursor += strlen($match[0]);
+      $this->lineno += substr_count($match[0], '\n');
+      $this->position = self::POSITION_DATA;
+
+      return new Twig_Token(Twig_Token::VAR_END_TYPE, '', $lineno);
+    }
+
+    return $this->lexExpression();
+  }
+
+  protected function lexExpression()
+  {
+    $match = null;
+
+    // whitespace
+    while (preg_match('/\s+/A', $this->code, $match, null, $this->cursor))
+    {
+      $this->cursor += strlen($match[0]);
+      $this->lineno += substr_count($match[0], '\n');
+    }
+
+    // sanity check
+    if ($this->cursor >= $this->end)
+    {
+      throw new Twig_SyntaxError('Unexpected end of stream', $this->lineno, $this->filename);
+    }
+
+    // first parse operators
+    if (preg_match(self::REGEX_OPERATOR, $this->code, $match, null, $this->cursor))
+    {
+      $this->cursor += strlen($match[0]);
+
+      return new Twig_Token(Twig_Token::OPERATOR_TYPE, $match[0], $this->lineno);
+    }
+    // now names
+    else if (preg_match(self::REGEX_NAME, $this->code, $match, null, $this->cursor))
+    {
+      $this->cursor += strlen($match[0]);
+
+      return new Twig_Token(Twig_Token::NAME_TYPE, $match[0], $this->lineno);
+    }
+    // then numbers
+    else if (preg_match(self::REGEX_NUMBER, $this->code, $match, null, $this->cursor))
+    {
+      $this->cursor += strlen($match[0]);
+      $value = (float)$match[0];
+      if ((int)$value === $value)
+      {
+        $value = (int)$value;
+      }
+
+      return new Twig_Token(Twig_Token::NUMBER_TYPE, $value, $this->lineno);
+    }
+    // and finally strings
+    else if (preg_match(self::REGEX_STRING, $this->code, $match, null, $this->cursor))
+    {
+      $this->cursor += strlen($match[0]);
+      $this->lineno += substr_count($match[0], '\n');
+      $value = stripcslashes(substr($match[0], 1, strlen($match[0]) - 2));
+
+      return new Twig_Token(Twig_Token::STRING_TYPE, $value, $this->lineno);
+    }
+
+    // unlexable
+    throw new Twig_SyntaxError(sprintf("Unexpected character '%s'", $this->code[$this->cursor]), $this->lineno, $this->filename);
+  }
+}
diff --git a/lib/Twig/LexerInterface.php b/lib/Twig/LexerInterface.php
new file mode 100644 (file)
index 0000000..4ec1985
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Interface implemented by lexer classes.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+interface Twig_LexerInterface
+{
+  /**
+   * Tokenizes a source code.
+   *
+   * @param  string $code     The source code
+   * @param  string $filename A unique identifier for the source code
+   *
+   * @return Twig_TokenStream A token stream instance
+   */
+  public function tokenize($code, $filename = 'n/a');
+}
diff --git a/lib/Twig/Loader.php b/lib/Twig/Loader.php
new file mode 100644 (file)
index 0000000..a90b97c
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Base loader class for all builtin loaders.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+abstract class Twig_Loader implements Twig_LoaderInterface
+{
+  protected $cache;
+  protected $autoReload;
+  protected $env;
+
+  public function __construct($cache = null, $autoReload = true)
+  {
+    $this->cache      = $cache;
+    $this->autoReload = $autoReload;
+  }
+
+  /**
+   * Loads a template by name.
+   *
+   * @param  string $name The template name
+   *
+   * @return string The class name of the compiled template
+   */
+  public function load($name)
+  {
+    $cls = $this->getTemplateName($name);
+
+    if (class_exists($cls))
+    {
+      return $cls;
+    }
+
+    list($template, $mtime) = $this->getSource($name);
+
+    if (is_null($this->cache))
+    {
+      $this->evalString($template, $name);
+
+      return $cls;
+    }
+
+    $cache = $this->getCacheFilename($name);
+    if (!file_exists($cache) || false === $mtime || ($this->autoReload && filemtime($cache) < $mtime))
+    {
+      $fp = @fopen($cache, 'wb');
+      if (!$fp)
+      {
+        $this->evalString($template, $name);
+
+        return $cls;
+      }
+      file_put_contents($cache, $this->compile($template, $name));
+      fclose($fp);
+    }
+
+    require_once $cache;
+
+    return $cls;
+  }
+
+  public function setEnvironment(Twig_Environment $env)
+  {
+    $this->env = $env;
+  }
+
+  public function getTemplateName($name)
+  {
+    return '__TwigTemplate_'.md5($name);
+  }
+
+  public function getCacheFilename($name)
+  {
+    return $this->cache.'/twig_'.md5($name).'.cache';
+  }
+
+  protected function compile($source, $name)
+  {
+    return $this->env->compile($this->env->parse($this->env->tokenize($source, $name)));
+  }
+
+  protected function evalString($source, $name)
+  {
+    eval('?>'.$this->compile($source, $name));
+  }
+
+  /**
+   * Gets the source code of a template, given its name.
+   *
+   * @param  string $name string The name of the template to load
+   *
+   * @return array An array consisting of the source code as the first element,
+   *               and the last modification time as the second one
+   *               or false if it's not relevant
+   */
+  abstract protected function getSource($name);
+}
diff --git a/lib/Twig/Loader/Array.php b/lib/Twig/Loader/Array.php
new file mode 100644 (file)
index 0000000..67068aa
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Loads a template from an array.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Loader_Array extends Twig_Loader
+{
+  protected $templates;
+
+  public function __construct(array $templates)
+  {
+    $this->templates = array();
+    foreach ($templates as $name => $template)
+    {
+      $this->templates[$name] = $template;
+    }
+  }
+
+  public function getSource($name)
+  {
+    if (!isset($this->templates[$name]))
+    {
+      throw new LogicException(sprintf('Template "%s" is not defined.', $name));
+    }
+
+    return array($this->templates[$name], false);
+  }
+}
diff --git a/lib/Twig/Loader/Filesystem.php b/lib/Twig/Loader/Filesystem.php
new file mode 100644 (file)
index 0000000..d1394a2
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Loads template from the filesystem.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Loader_Filesystem extends Twig_Loader
+{
+  protected $folder;
+
+  public function __construct($folder, $cache = null, $autoReload = true)
+  {
+    $this->folder = realpath($folder);
+
+    parent::__construct($cache, $autoReload);
+  }
+
+  /**
+   * Gets the source code of a template, given its name.
+   *
+   * @param  string $name string The name of the template to load
+   *
+   * @return array An array consisting of the source code as the first element,
+   *               and the last modification time as the second one
+   *               or false if it's not relevant
+   */
+  public function getSource($name)
+  {
+    $file = realpath($this->folder.DIRECTORY_SEPARATOR.$name);
+
+    if (0 !== strpos($file, $this->folder))
+    {
+      throw new RuntimeException(sprintf('Unable to find template "%s".', $name));
+    }
+
+    // simple security check
+    if (0 !== strpos($file, $this->folder))
+    {
+      throw new RuntimeException(sprintf('You cannot load a template outside the "%s" directory.', $this->folder));
+    }
+
+    return array(file_get_contents($file), filemtime($file));
+  }
+}
diff --git a/lib/Twig/Loader/String.php b/lib/Twig/Loader/String.php
new file mode 100644 (file)
index 0000000..c135fe4
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Loads a template from a string.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Loader_String extends Twig_Loader
+{
+  /**
+   * Gets the source code of a template, given its name.
+   *
+   * @param  string $name string The name of the template to load
+   *
+   * @return array An array consisting of the source code as the first element,
+   *               and the last modification time as the second one
+   *               or false if it's not relevant
+   */
+  public function getSource($source)
+  {
+    return array($source, false);
+  }
+}
diff --git a/lib/Twig/LoaderInterface.php b/lib/Twig/LoaderInterface.php
new file mode 100644 (file)
index 0000000..6780003
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Interface all loaders must implement.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+interface Twig_LoaderInterface
+{
+  /**
+   * Loads a template by name.
+   *
+   * @param  string $name The template name
+   *
+   * @return string The class name of the compiled template
+   */
+  public function load($name);
+}
diff --git a/lib/Twig/Node.php b/lib/Twig/Node.php
new file mode 100644 (file)
index 0000000..366617e
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a node in the AST.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+abstract class Twig_Node
+{
+  protected $lineno;
+  protected $tag;
+
+  public function __construct($lineno, $tag = null)
+  {
+    $this->lineno = $lineno;
+    $this->tag = $tag;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'()';
+  }
+
+  abstract public function compile($compiler);
+
+  public function getLine()
+  {
+    return $this->lineno;
+  }
+
+  public function getTag()
+  {
+    return $this->tag;
+  }
+}
diff --git a/lib/Twig/Node/AutoEscape.php b/lib/Twig/Node/AutoEscape.php
new file mode 100644 (file)
index 0000000..5eefbd9
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents an autoescape node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_AutoEscape extends Twig_Node implements Twig_NodeListInterface
+{
+  protected $value;
+  protected $body;
+
+  public function __construct($value, Twig_NodeList $body, $lineno, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+    $this->value = $value;
+    $this->body  = $body;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->value.')';
+  }
+
+  public function getNodes()
+  {
+    return $this->body->getNodes();
+  }
+
+  public function setNodes(array $nodes)
+  {
+    $this->body = new Twig_NodeList($nodes, $this->lineno);
+  }
+
+  public function compile($compiler)
+  {
+    $compiler->subcompile($this->body);
+  }
+
+  public function getValue()
+  {
+    return $this->value;
+  }
+}
diff --git a/lib/Twig/Node/Block.php b/lib/Twig/Node/Block.php
new file mode 100644 (file)
index 0000000..8b5a64a
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a block node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_Block extends Twig_Node implements Twig_NodeListInterface
+{
+  protected $name;
+  protected $body;
+  protected $parent;
+
+  public function __construct($name, Twig_NodeList $body, $lineno, $parent = null, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+    $this->name = $name;
+    $this->body = $body;
+    $this->parent = $parent;
+  }
+
+  public function __toString()
+  {
+    $repr = array(get_class($this).' '.$this->name.'(');
+    foreach ($this->body->getNodes() as $node)
+    {
+      foreach (explode("\n", $node->__toString()) as $line)
+      {
+        $repr[] = '  '.$line;
+      }
+    }
+    $repr[] = ')';
+
+    return implode("\n", $repr);
+  }
+
+  public function getNodes()
+  {
+    return $this->body->getNodes();
+  }
+
+  public function setNodes(array $nodes)
+  {
+    $this->body = new Twig_NodeList($nodes, $this->lineno);
+  }
+
+  public function replace($other)
+  {
+    $this->body = $other->body;
+  }
+
+  public function compile($compiler)
+  {
+    $compiler
+      ->addDebugInfo($this)
+      ->write(sprintf("public function block_%s(\$context)\n", $this->name), "{\n")
+      ->indent()
+    ;
+
+    $compiler
+      ->subcompile($this->body)
+      ->outdent()
+      ->write("}\n\n")
+    ;
+  }
+
+  public function getParent()
+  {
+    return $this->parent;
+  }
+
+  public function setParent($parent)
+  {
+    $this->parent = $parent;
+  }
+}
diff --git a/lib/Twig/Node/BlockReference.php b/lib/Twig/Node/BlockReference.php
new file mode 100644 (file)
index 0000000..3fadb78
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a block call node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_BlockReference extends Twig_Node
+{
+  protected $name;
+
+  public function __construct($name, $lineno, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+    $this->name = $name;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->name.')';
+  }
+
+  public function compile($compiler)
+  {
+    $compiler
+      ->addDebugInfo($this)
+      ->write(sprintf('$this->block_%s($context);'."\n", $this->name))
+    ;
+  }
+
+  public function getName()
+  {
+    return $this->name;
+  }
+}
diff --git a/lib/Twig/Node/Call.php b/lib/Twig/Node/Call.php
new file mode 100644 (file)
index 0000000..1c4f1a3
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a call node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_Call extends Twig_Node
+{
+  protected $name;
+  protected $arguments;
+
+  public function __construct($name, Twig_NodeList $arguments, $lineno, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+    $this->name = $name;
+    $this->arguments  = $arguments;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->name.')';
+  }
+
+  public function compile($compiler)
+  {
+//    $compiler->subcompile($this->body);
+  }
+
+  public function getName()
+  {
+    return $this->name;
+  }
+}
diff --git a/lib/Twig/Node/Expression.php b/lib/Twig/Node/Expression.php
new file mode 100644 (file)
index 0000000..938966c
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Abstract class for all nodes that represents an expression.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+abstract class Twig_Node_Expression extends Twig_Node
+{
+}
diff --git a/lib/Twig/Node/Expression/AssignName.php b/lib/Twig/Node/Expression/AssignName.php
new file mode 100644 (file)
index 0000000..eec3f99
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Node_Expression_AssignName extends Twig_Node_Expression_Name
+{
+  public function compile($compiler)
+  {
+    $compiler->raw(sprintf('$context[\'%s\']', $this->name));
+  }
+}
diff --git a/lib/Twig/Node/Expression/Binary.php b/lib/Twig/Node/Expression/Binary.php
new file mode 100644 (file)
index 0000000..dbe6757
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+abstract class Twig_Node_Expression_Binary extends Twig_Node_Expression
+{
+  protected $left;
+  protected $right;
+
+  public function __construct(Twig_Node $left, Twig_Node $right, $lineno)
+  {
+    parent::__construct($lineno);
+    $this->left = $left;
+    $this->right = $right;
+  }
+
+  public function __toString()
+  {
+    $repr = array(get_class($this).'(');
+
+    foreach (explode("\n", $this->left->__toString()) as $line)
+    {
+      $repr[] = '  '.$line;
+    }
+
+    $repr[] = ', ';
+
+    foreach (explode("\n", $this->right->__toString()) as $line)
+    {
+      $repr[] = '  '.$line;
+    }
+
+    $repr[] = ')';
+
+    return implode("\n", $repr);
+  }
+
+  public function compile($compiler)
+  {
+    $compiler
+      ->raw('(')
+      ->subcompile($this->left)
+      ->raw(') ')
+    ;
+    $this->operator($compiler);
+    $compiler
+      ->raw(' (')
+      ->subcompile($this->right)
+      ->raw(')')
+    ;
+  }
+
+  abstract public function operator($compiler);
+}
diff --git a/lib/Twig/Node/Expression/Binary/Add.php b/lib/Twig/Node/Expression/Binary/Add.php
new file mode 100644 (file)
index 0000000..63bc0a2
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Binary_Add extends Twig_Node_Expression_Binary
+{
+  public function operator($compiler)
+  {
+    return $compiler->raw('+');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Binary/And.php b/lib/Twig/Node/Expression/Binary/And.php
new file mode 100644 (file)
index 0000000..1fe4bc3
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Binary_And extends Twig_Node_Expression_Binary
+{
+  public function operator($compiler)
+  {
+    return $compiler->raw('&&');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Binary/Concat.php b/lib/Twig/Node/Expression/Binary/Concat.php
new file mode 100644 (file)
index 0000000..61e6954
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Binary_Concat extends Twig_Node_Expression_Binary
+{
+  public function operator($compiler)
+  {
+    return $compiler->raw('.');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Binary/Div.php b/lib/Twig/Node/Expression/Binary/Div.php
new file mode 100644 (file)
index 0000000..7361b73
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Binary_Div extends Twig_Node_Expression_Binary
+{
+  public function operator($compiler)
+  {
+    return $compiler->raw('/');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Binary/Mod.php b/lib/Twig/Node/Expression/Binary/Mod.php
new file mode 100644 (file)
index 0000000..e2c9117
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Binary_Mod extends Twig_Node_Expression_Binary
+{
+  public function operator($compiler)
+  {
+    return $compiler->raw('%');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Binary/Mul.php b/lib/Twig/Node/Expression/Binary/Mul.php
new file mode 100644 (file)
index 0000000..a236101
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Binary_Mul extends Twig_Node_Expression_Binary
+{
+  public function operator($compiler)
+  {
+    return $compiler->raw('*');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Binary/Or.php b/lib/Twig/Node/Expression/Binary/Or.php
new file mode 100644 (file)
index 0000000..a968b82
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Binary_Or extends Twig_Node_Expression_Binary
+{
+  public function operator($compiler)
+  {
+    return $compiler->raw('||');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Binary/Sub.php b/lib/Twig/Node/Expression/Binary/Sub.php
new file mode 100644 (file)
index 0000000..f67117b
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Binary_Sub extends Twig_Node_Expression_Binary
+{
+  public function operator($compiler)
+  {
+    return $compiler->raw('-');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Compare.php b/lib/Twig/Node/Expression/Compare.php
new file mode 100644 (file)
index 0000000..71b8e4d
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Compare extends Twig_Node_Expression
+{
+  protected $expr;
+  protected $ops;
+
+  public function __construct(Twig_Node_Expression $expr, $ops, $lineno)
+  {
+    parent::__construct($lineno);
+    $this->expr = $expr;
+    $this->ops = $ops;
+  }
+
+  public function compile($compiler)
+  {
+    $this->expr->compile($compiler);
+    $i = 0;
+    $useTmpVars = count($this->ops) > 1;
+    foreach ($this->ops as $op)
+    {
+      if ($i)
+      {
+        $compiler->raw(' && ($tmp'.$i);
+      }
+      list($op, $node) = $op;
+      $compiler->raw(' '.$op.' ');
+
+      if ($useTmpVars)
+      {
+        $compiler
+          ->raw('($tmp'.++$i.' = ')
+          ->subcompile($node)
+          ->raw(')')
+        ;
+      }
+      else
+      {
+        $compiler->subcompile($node);
+      }
+    }
+
+    for ($j = 1; $j < $i; $j++)
+    {
+      $compiler->raw(')');
+    }
+  }
+}
diff --git a/lib/Twig/Node/Expression/Conditional.php b/lib/Twig/Node/Expression/Conditional.php
new file mode 100644 (file)
index 0000000..d89d639
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Conditional extends Twig_Node_Expression
+{
+  protected $expr1;
+  protected $expr2;
+  protected $expr3;
+
+  public function __construct(Twig_Node_Expression $expr1, Twig_Node_Expression $expr2, Twig_Node_Expression $expr3, $lineno)
+  {
+    parent::__construct($lineno);
+    $this->expr1 = $expr1;
+    $this->expr2 = $expr2;
+    $this->expr3 = $expr3;
+  }
+
+  public function compile($compiler)
+  {
+    $compiler
+      ->raw('(')
+      ->subcompile($this->expr1)
+      ->raw(') ? (')
+      ->subcompile($this->expr2)
+      ->raw(') : (')
+      ->subcompile($this->expr3)
+      ->raw(')')
+    ;
+  }
+}
diff --git a/lib/Twig/Node/Expression/Constant.php b/lib/Twig/Node/Expression/Constant.php
new file mode 100644 (file)
index 0000000..976b381
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Constant extends Twig_Node_Expression
+{
+  protected $value;
+
+  public function __construct($value, $lineno)
+  {
+    parent::__construct($lineno);
+    $this->value = $value;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->value.')';
+  }
+
+  public function compile($compiler)
+  {
+    $compiler->repr($this->value);
+  }
+
+  public function getValue()
+  {
+    return $this->value;
+  }
+}
diff --git a/lib/Twig/Node/Expression/Filter.php b/lib/Twig/Node/Expression/Filter.php
new file mode 100644 (file)
index 0000000..0a9fc52
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Filter extends Twig_Node_Expression implements Twig_NodeListInterface
+{
+  protected $node;
+  protected $filters;
+
+  public function __construct(Twig_Node $node, array $filters, $lineno, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+
+    $this->node = $node;
+    $this->filters = $filters;
+  }
+
+  public function __toString()
+  {
+    $filters = array();
+    foreach ($this->filters as $filter)
+    {
+      $filters[] = $filter[0];
+    }
+
+    $repr = array(get_class($this).'(');
+
+    foreach (explode("\n", $this->node->__toString()) as $line)
+    {
+      $repr[] = '  '.$line;
+    }
+
+    $repr[] = '  ('.implode(', ', $filters).')';
+    $repr[] = ')';
+
+    return implode("\n", $repr);
+  }
+
+  public function getNodes()
+  {
+    return array($this->node);
+  }
+
+  public function setNodes(array $nodes)
+  {
+    $this->node = $nodes[0];
+  }
+
+  public function compile($compiler)
+  {
+    $filterMap = $compiler->getEnvironment()->getFilters();
+
+    $postponed = array();
+    for ($i = count($this->filters) - 1; $i >= 0; --$i)
+    {
+      list($name, $attrs) = $this->filters[$i];
+      if (!isset($filterMap[$name]))
+      {
+        $compiler
+          ->raw('$this->resolveMissingFilter(')
+          ->repr($name)
+          ->raw(', ')
+        ;
+      }
+      else
+      {
+        $compiler->raw($filterMap[$name][0].($filterMap[$name][1] ? '($this, ' : '('));
+      }
+      $postponed[] = $attrs;
+    }
+    $this->node->compile($compiler);
+    foreach (array_reverse($postponed) as $attributes)
+    {
+      foreach ($attributes as $node)
+      {
+        $compiler
+          ->raw(', ')
+          ->subcompile($node)
+        ;
+      }
+      $compiler->raw(')');
+    }
+  }
+
+  public function getFilters()
+  {
+    return $this->filters;
+  }
+
+  public function appendFilter($filter)
+  {
+    $this->filters[] = $filter;
+  }
+
+  public function hasFilter($name)
+  {
+    foreach ($this->filters as $filter)
+    {
+      if ($name == $filter[0])
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+}
diff --git a/lib/Twig/Node/Expression/GetAttr.php b/lib/Twig/Node/Expression/GetAttr.php
new file mode 100644 (file)
index 0000000..1a03623
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_GetAttr extends Twig_Node_Expression implements Twig_NodeListInterface
+{
+  protected $node;
+  protected $attr;
+
+  public function __construct(Twig_Node $node, $attr, $lineno, $token_value)
+  {
+    parent::__construct($lineno);
+    $this->node = $node;
+    $this->attr = $attr;
+    $this->token_value = $token_value;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->node.', '.$this->attr.')';
+  }
+
+  public function getNodes()
+  {
+    return array($this->node);
+  }
+
+  public function setNodes(array $nodes)
+  {
+    $this->node = $nodes[0];
+  }
+
+  public function compile($compiler)
+  {
+    $compiler
+      ->raw('$this->getAttribute(')
+      ->subcompile($this->node)
+      ->raw(', ')
+      ->subcompile($this->attr)
+    ;
+
+    if ('[' == $this->token_value) # Don't look for functions if they're using foo[bar]
+    {
+      $compiler->raw(', false');
+    }
+
+    $compiler->raw(')');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Name.php b/lib/Twig/Node/Expression/Name.php
new file mode 100644 (file)
index 0000000..439a4b3
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Name extends Twig_Node_Expression
+{
+  protected $name;
+
+  public function __construct($name, $lineno)
+  {
+    parent::__construct($lineno);
+    $this->name = $name;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->name.')';
+  }
+
+  public function compile($compiler)
+  {
+    $compiler->raw(sprintf('(isset($context[\'%s\']) ? $context[\'%s\'] : null)', $this->name, $this->name));
+  }
+
+  public function getName()
+  {
+    return $this->name;
+  }
+}
diff --git a/lib/Twig/Node/Expression/Unary.php b/lib/Twig/Node/Expression/Unary.php
new file mode 100644 (file)
index 0000000..652c7f6
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+abstract class Twig_Node_Expression_Unary extends Twig_Node_Expression
+{
+  protected $node;
+
+  public function __construct(Twig_Node $node, $lineno)
+  {
+    parent::__construct($lineno);
+    $this->node = $node;
+  }
+
+  public function __toString()
+  {
+    $repr = array(get_class($this).'(');
+
+    foreach (explode("\n", $this->node->__toString()) as $line)
+    {
+      $repr[] = '  '.$line;
+    }
+
+    $repr[] = ')';
+
+    return implode("\n", $repr);
+  }
+  public function compile($compiler)
+  {
+    $compiler->raw('(');
+    $this->operator($compiler);
+    $this->node->compile($compiler);
+    $compiler->raw(')');
+  }
+
+  abstract public function operator($compiler);
+}
diff --git a/lib/Twig/Node/Expression/Unary/Neg.php b/lib/Twig/Node/Expression/Unary/Neg.php
new file mode 100644 (file)
index 0000000..7893619
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Unary_Neg extends Twig_Node_Expression_Unary
+{
+  public function operator($compiler)
+  {
+    $compiler->raw('-');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Unary/Not.php b/lib/Twig/Node/Expression/Unary/Not.php
new file mode 100644 (file)
index 0000000..2771a23
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Unary_Not extends Twig_Node_Expression_Unary
+{
+  public function operator($compiler)
+  {
+    $compiler->raw('!');
+  }
+}
diff --git a/lib/Twig/Node/Expression/Unary/Pos.php b/lib/Twig/Node/Expression/Unary/Pos.php
new file mode 100644 (file)
index 0000000..40cec40
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Unary_Pos extends Twig_Node_Expression_Unary
+{
+  public function operator($compiler)
+  {
+    $compiler->raw('+');
+  }
+}
diff --git a/lib/Twig/Node/Filter.php b/lib/Twig/Node/Filter.php
new file mode 100644 (file)
index 0000000..bbc161c
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a filter node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_Filter extends Twig_Node implements Twig_NodeListInterface
+{
+  protected $filter;
+  protected $body;
+
+  public function __construct($filter, Twig_NodeList $body, $lineno, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+    $this->filter = $filter;
+    $this->body  = $body;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->filter.')';
+  }
+
+  public function getNodes()
+  {
+    return $this->body->getNodes();
+  }
+
+  public function setNodes(array $nodes)
+  {
+    $this->body = new Twig_NodeList($nodes, $this->lineno);
+  }
+
+  public function compile($compiler)
+  {
+    $compiler->subcompile($this->body);
+  }
+
+  public function getFilter()
+  {
+    return $this->filter;
+  }
+}
diff --git a/lib/Twig/Node/For.php b/lib/Twig/Node/For.php
new file mode 100644 (file)
index 0000000..5c2449e
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a for node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_For extends Twig_Node implements Twig_NodeListInterface
+{
+  protected $isMultitarget;
+  protected $item;
+  protected $seq;
+  protected $body;
+  protected $else;
+
+  public function __construct($isMultitarget, $item, $seq, Twig_NodeList $body, Twig_Node $else = null, $lineno, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+    $this->isMultitarget = $isMultitarget;
+    $this->item = $item;
+    $this->seq = $seq;
+    $this->body = $body;
+    $this->else = $else;
+    $this->lineno = $lineno;
+  }
+
+  public function getNodes()
+  {
+    return $this->body->getNodes();
+  }
+
+  public function setNodes(array $nodes)
+  {
+    $this->body = new Twig_NodeList($nodes, $this->lineno);
+  }
+
+  public function compile($compiler)
+  {
+    $compiler
+      ->addDebugInfo($this)
+      ->pushContext()
+      ->write("\$context['_iterated'] = false;\n")
+      ->write('foreach (twig_iterate($context, ')
+      ->subcompile($this->seq)
+      ->raw(") as \$iterator)\n")
+      ->write("{\n")
+      ->indent()
+      ->write("\$context['_iterated'] = true;\n")
+      ->write('twig_set_loop_context($context, $iterator, ');
+    ;
+
+    if ($this->isMultitarget)
+    {
+      $compiler->raw('array(');
+      foreach ($this->item as $idx => $node)
+      {
+        if ($idx)
+        {
+          $compiler->raw(', ');
+        }
+        $compiler->repr($node->getName());
+      }
+      $compiler->raw(')');
+    }
+    else
+    {
+      $compiler->repr($this->item->getName());
+    }
+
+    $compiler
+      ->raw(");\n")
+      ->subcompile($this->body)
+      ->outdent()
+      ->write("}\n")
+    ;
+
+    if (!is_null($this->else))
+    {
+      $compiler
+        ->write("if (!\$context['_iterated'])\n")
+        ->write("{\n")
+        ->indent()
+        ->subcompile($this->else)
+        ->outdent()
+        ->write("}\n")
+      ;
+    }
+    $compiler->popContext();
+  }
+}
diff --git a/lib/Twig/Node/If.php b/lib/Twig/Node/If.php
new file mode 100644 (file)
index 0000000..4cb6869
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents an if node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_If extends Twig_Node implements Twig_NodeListInterface
+{
+  protected $tests;
+  protected $else;
+
+  public function __construct($tests, Twig_NodeList $else = null, $lineno, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+    $this->tests = $tests;
+    $this->else = $else;
+  }
+
+  public function getNodes()
+  {
+    $nodes = array();
+    foreach ($this->tests as $test)
+    {
+      $nodes[] = $test[1];
+    }
+
+    if ($this->else)
+    {
+      $nodes[] = $this->else;
+    }
+
+    return $nodes;
+  }
+
+  public function setNodes(array $nodes)
+  {
+    foreach ($this->tests as $i => $test)
+    {
+      $this->tests[$i][1] = $nodes[$i];
+    }
+
+    if ($this->else)
+    {
+      $nodes = $nodes[count($nodes) - 1];
+    }
+  }
+
+  public function compile($compiler)
+  {
+    $compiler->addDebugInfo($this);
+    $idx = 0;
+    foreach ($this->tests as $test)
+    {
+      if ($idx++)
+      {
+        $compiler
+          ->outdent()
+          ->write("}\n", "elseif (")
+        ;
+      }
+      else
+      {
+        $compiler
+          ->write('if (')
+        ;
+      }
+
+      $compiler
+        ->subcompile($test[0])
+        ->raw(")\n")
+        ->write("{\n")
+        ->indent()
+        ->subcompile($test[1])
+      ;
+    }
+    if (!is_null($this->else))
+    {
+      $compiler
+        ->outdent()
+        ->write("}\n", "else\n", "{\n")
+        ->indent()
+        ->subcompile($this->else)
+      ;
+    }
+
+    $compiler
+      ->outdent()
+      ->write("}\n");
+  }
+}
diff --git a/lib/Twig/Node/Include.php b/lib/Twig/Node/Include.php
new file mode 100644 (file)
index 0000000..f8f0b84
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents an include node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_Include extends Twig_Node
+{
+  protected $expr;
+  protected $sandboxed;
+
+  public function __construct(Twig_Node_Expression $expr, $sandboxed, $lineno, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+
+    $this->expr = $expr;
+    $this->sandboxed = $sandboxed;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->expr.')';
+  }
+
+  public function compile($compiler)
+  {
+    if (!$compiler->getEnvironment()->hasExtension('sandbox') && $this->sandboxed)
+    {
+      throw new Twig_SyntaxError('Unable to use the sanboxed attribute on an include if the sandbox extension is not enabled.');
+    }
+
+    $compiler->addDebugInfo($this);
+
+    if ($this->sandboxed)
+    {
+      $compiler
+        ->write("\$sandbox = \$this->env->getExtension('sandbox');\n")
+        ->write("\$alreadySandboxed = \$sandbox->isSandboxed();\n")
+        ->write("\$sandbox->enableSandbox();\n")
+      ;
+    }
+
+    $compiler
+      ->write('$this->env->loadTemplate(')
+      ->subcompile($this->expr)
+      ->raw(')->display($context);'."\n")
+    ;
+
+    if ($this->sandboxed)
+    {
+      $compiler
+        ->write("if (!\$alreadySandboxed)\n", "{\n")
+        ->indent()
+        ->write("\$sandbox->disableSandbox();\n")
+        ->outdent()
+        ->write("}\n")
+      ;
+    }
+  }
+}
diff --git a/lib/Twig/Node/Macro.php b/lib/Twig/Node/Macro.php
new file mode 100644 (file)
index 0000000..c5f67f6
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a macro node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_Macro extends Twig_Node implements Twig_NodeListInterface
+{
+  protected $name;
+  protected $body;
+
+  public function __construct($name, Twig_NodeList $body, $lineno, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+    $this->name = $name;
+    $this->body  = $body;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->name.')';
+  }
+
+  public function getNodes()
+  {
+    return $this->body->getNodes();
+  }
+
+  public function setNodes(array $nodes)
+  {
+    $this->body = new Twig_NodeList($nodes, $this->lineno);
+  }
+
+  public function compile($compiler)
+  {
+    $compiler->subcompile($this->body);
+  }
+
+  public function getName()
+  {
+    return $this->name;
+  }
+}
diff --git a/lib/Twig/Node/Module.php b/lib/Twig/Node/Module.php
new file mode 100644 (file)
index 0000000..90abdb4
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a module node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_Module extends Twig_Node implements Twig_NodeListInterface
+{
+  protected $body;
+  protected $extends;
+  protected $blocks;
+  protected $filename;
+  protected $usedFilters;
+  protected $usedTags;
+
+  public function __construct(Twig_NodeList $body, $extends, $blocks, $filename)
+  {
+    parent::__construct(1);
+
+    $this->body = $body;
+    $this->extends = $extends;
+    $this->blocks = array_values($blocks);
+    $this->filename = $filename;
+    $this->usedFilters = array();
+    $this->usedTags = array();
+  }
+
+  public function __toString()
+  {
+    $repr = array(get_class($this).'(');
+    foreach ($this->body->getNodes() as $node)
+    {
+      foreach (explode("\n", $node->__toString()) as $line)
+      {
+        $repr[] = '  '.$line;
+      }
+    }
+
+    foreach ($this->blocks as $node)
+    {
+      foreach (explode("\n", $node->__toString()) as $line)
+      {
+        $repr[] = '  '.$line;
+      }
+    }
+    $repr[] = ')';
+
+    return implode("\n", $repr);
+  }
+
+  public function getNodes()
+  {
+    return array_merge(array($this->body), $this->blocks);
+  }
+
+  public function setNodes(array $nodes)
+  {
+    $this->body   = array_shift($nodes);
+    $this->blocks = $nodes;
+  }
+
+  public function setUsedFilters(array $filters)
+  {
+    $this->usedFilters = $filters;
+  }
+
+  public function setUsedTags(array $tags)
+  {
+    $this->usedTags = $tags;
+  }
+
+  public function compile($compiler)
+  {
+    $sandboxed = $compiler->getEnvironment()->hasExtension('sandbox');
+
+    $compiler->write("<?php\n\n");
+
+    if (!is_null($this->extends))
+    {
+      $compiler
+        ->write('$this->load(')
+        ->repr($this->extends)
+        ->raw(");\n\n")
+      ;
+    }
+
+    $compiler
+      ->write("/* $this->filename */\n")
+      ->write('class __TwigTemplate_'.md5($this->filename))
+    ;
+
+    if (!is_null($this->extends))
+    {
+      $parent = md5($this->extends);
+      $compiler
+        ->raw(" extends __TwigTemplate_$parent\n")
+        ->write("{\n")
+        ->indent()
+      ;
+    }
+    else
+    {
+      $compiler
+        ->write(" extends ".$compiler->getEnvironment()->getBaseTemplateClass()."\n", "{\n")
+        ->indent()
+        ->write("public function display(\$context)\n", "{\n")
+        ->indent()
+      ;
+
+      if ($sandboxed)
+      {
+        $compiler->write("\$this->checkSecurity();\n");
+      }
+
+      $compiler
+        ->write("\$this->env->initRuntime();\n\n")
+        ->subcompile($this->body)
+        ->outdent()
+        ->write("}\n\n")
+      ;
+    }
+
+    // blocks
+    foreach ($this->blocks as $node)
+    {
+      $compiler->subcompile($node);
+    }
+
+    if ($sandboxed)
+    {
+      // sandbox information
+      $compiler
+        ->write("protected function checkSecurity()\n", "{\n")
+        ->indent()
+        ->write("\$this->env->getExtension('sandbox')->checkSecurity(\n")
+        ->indent()
+        ->write(!$this->usedTags ? "array(),\n" : "array('".implode('\', \'', $this->usedTags)."'),\n")
+        ->write(!$this->usedFilters ? "array()\n" : "array('".implode('\', \'', $this->usedFilters)."')\n")
+        ->outdent()
+        ->write(");\n")
+        ->outdent()
+        ->write("}\n\n")
+      ;
+    }
+
+    // debug information
+    if ($compiler->getEnvironment()->isDebug())
+    {
+      $compiler
+        ->write("public function __toString()\n", "{\n")
+        ->indent()
+        ->write('return ')
+        ->string($this)
+        ->raw(";\n")
+        ->outdent()
+        ->write("}\n\n")
+      ;
+    }
+
+    $compiler
+      ->outdent()
+      ->write("}\n")
+    ;
+  }
+}
diff --git a/lib/Twig/Node/Parent.php b/lib/Twig/Node/Parent.php
new file mode 100644 (file)
index 0000000..0e83be1
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a parent node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_Parent extends Twig_Node
+{
+  protected $blockName;
+
+  public function __construct($blockName, $lineno, $tag = null)
+  {
+    parent::__construct($lineno, $tag);
+    $this->blockName = $blockName;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->blockName.')';
+  }
+
+  public function compile($compiler)
+  {
+    $compiler
+      ->addDebugInfo($this)
+      ->write('parent::block_'.$this->blockName.'($context);'."\n")
+    ;
+  }
+}
diff --git a/lib/Twig/Node/Print.php b/lib/Twig/Node/Print.php
new file mode 100644 (file)
index 0000000..ed34f01
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a node that outputs an expression.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_Print extends Twig_Node implements Twig_NodeListInterface
+{
+  protected $expr;
+
+  public function __construct(Twig_Node_Expression $expr, $lineno)
+  {
+    parent::__construct($lineno);
+    $this->expr = $expr;
+  }
+
+  public function __toString()
+  {
+    $repr = array(get_class($this).'(');
+    foreach (explode("\n", $this->expr->__toString()) as $line)
+    {
+      $repr[] = '  '.$line;
+    }
+    $repr[] = ')';
+
+    return implode("\n", $repr);
+  }
+
+  public function getNodes()
+  {
+    return array($this->expr);
+  }
+
+  public function setNodes(array $nodes)
+  {
+    $this->expr = $nodes[0];
+  }
+
+  public function compile($compiler)
+  {
+    $compiler
+      ->addDebugInfo($this)
+      ->write('echo ')
+      ->subcompile($this->expr)
+      ->raw(";\n")
+    ;
+  }
+
+  public function getExpression()
+  {
+    return $this->expr;
+  }
+}
diff --git a/lib/Twig/Node/Set.php b/lib/Twig/Node/Set.php
new file mode 100644 (file)
index 0000000..7816242
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+class Twig_Node_Set extends Twig_Node
+{
+  protected $name;
+  protected $value;
+
+  public function __construct($name, Twig_Node_Expression $value, $lineno)
+  {
+    parent::__construct($lineno);
+
+    $this->name = $name;
+    $this->value = $value;
+  }
+
+  public function compile($compiler)
+  {
+    $compiler
+      ->addDebugInfo($this)
+      ->write('$context[')
+      ->string($this->name)
+      ->write('] = ')
+      ->subcompile($this->value)
+      ->raw(";\n")
+    ;
+  }
+}
diff --git a/lib/Twig/Node/Text.php b/lib/Twig/Node/Text.php
new file mode 100644 (file)
index 0000000..89dfc5a
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a text node.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Node_Text extends Twig_Node
+{
+  protected $data;
+
+  public function __construct($data, $lineno)
+  {
+    parent::__construct($lineno);
+    $this->data = $data;
+  }
+
+  public function __toString()
+  {
+    return get_class($this).'('.$this->data.')';
+  }
+
+  public function compile($compiler)
+  {
+    $compiler
+      ->addDebugInfo($this)
+      ->write('echo ')
+      ->string($this->data)
+      ->raw(";\n")
+    ;
+  }
+
+  public function getData()
+  {
+    return $this->data;
+  }
+}
diff --git a/lib/Twig/NodeList.php b/lib/Twig/NodeList.php
new file mode 100644 (file)
index 0000000..79c5fbc
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a list of nodes.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_NodeList extends Twig_Node implements Twig_NodeListInterface
+{
+  protected $nodes;
+
+  public function __construct(array $nodes, $lineno = 0)
+  {
+    parent::__construct($lineno);
+
+    $this->nodes = $nodes;
+  }
+
+  public function __toString()
+  {
+    $repr = array(get_class($this).'(');
+    foreach ($this->nodes as $node)
+    {
+      foreach (explode("\n", $node->__toString()) as $line)
+      {
+        $repr[] = '  '.$line;
+      }
+    }
+
+    return implode("\n", $repr);
+  }
+
+  public function compile($compiler)
+  {
+    foreach ($this->nodes as $node)
+    {
+      $node->compile($compiler);
+    }
+  }
+
+  public function getNodes()
+  {
+    return $this->nodes;
+  }
+
+  public function setNodes(array $nodes)
+  {
+    $this->nodes = $nodes;
+  }
+}
diff --git a/lib/Twig/NodeListInterface.php b/lib/Twig/NodeListInterface.php
new file mode 100644 (file)
index 0000000..43df815
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Interface implemented by node list classes.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+interface Twig_NodeListInterface
+{
+  /**
+   * Returns an array of embedded nodes
+   */
+  public function getNodes();
+
+  /**
+   * Sets the array of embedded nodes
+   */
+  public function setNodes(array $nodes);
+}
diff --git a/lib/Twig/NodeTransformer.php b/lib/Twig/NodeTransformer.php
new file mode 100644 (file)
index 0000000..82c4fbc
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+abstract class Twig_NodeTransformer
+{
+  protected $env;
+
+  public function setEnvironment(Twig_Environment $env)
+  {
+    $this->env = $env;
+  }
+
+  abstract public function visit(Twig_Node $node);
+
+  protected function visitDeep(Twig_Node $node)
+  {
+    if (!$node instanceof Twig_NodeListInterface)
+    {
+      return $node;
+    }
+
+    $newNodes = array();
+    foreach ($nodes = $node->getNodes() as $k => $n)
+    {
+      if (null !== $n = $this->visit($n))
+      {
+        $newNodes[$k] = $n;
+      }
+    }
+
+    $node->setNodes($newNodes);
+
+    return $node;
+  }
+}
diff --git a/lib/Twig/NodeTransformer/Chain.php b/lib/Twig/NodeTransformer/Chain.php
new file mode 100644 (file)
index 0000000..6fd6fa0
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_NodeTransformer_Chain extends Twig_NodeTransformer
+{
+  protected $transformers;
+
+  public function __construct(array $transformers)
+  {
+    $this->transformers = $transformers;
+  }
+
+  public function setEnvironment(Twig_Environment $env)
+  {
+    parent::setEnvironment($env);
+
+    foreach ($this->transformers as $transformer)
+    {
+      $transformer->setEnvironment($env);
+    }
+  }
+
+  public function visit(Twig_Node $node)
+  {
+    foreach ($this->transformers as $transformer)
+    {
+      $node = $transformer->visit($node);
+    }
+
+    return $node;
+  }
+}
diff --git a/lib/Twig/NodeTransformer/Escaper.php b/lib/Twig/NodeTransformer/Escaper.php
new file mode 100644 (file)
index 0000000..914d241
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_NodeTransformer_Escaper extends Twig_NodeTransformer
+{
+  protected $statusStack = array();
+
+  public function visit(Twig_Node $node)
+  {
+    // autoescape?
+    if ($node instanceof Twig_Node_AutoEscape)
+    {
+      $this->statusStack[] = $node->getValue();
+
+      $node = $this->visitDeep($node);
+
+      array_pop($this->statusStack);
+
+      // remove the node
+      return $node;
+    }
+
+    if (!$node instanceof Twig_Node_Print)
+    {
+      return $this->visitDeep($node);
+    }
+
+    if (false === $this->needEscaping())
+    {
+      return $node;
+    }
+
+    $expression = $node->getExpression();
+
+    // don't escape if escape has already been called
+    // or if we want the safe string
+    if (
+      $expression instanceof Twig_Node_Expression_Filter
+      &&
+      (
+        $expression->hasFilter('escape')
+        ||
+        $expression->hasFilter('safe')
+      )
+    )
+    {
+      return $node;
+    }
+
+    // escape
+    if ($expression instanceof Twig_Node_Expression_Filter)
+    {
+      $expression->appendFilter(array('escape', array()));
+
+      return $node;
+    }
+    else
+    {
+      return new Twig_Node_Print(
+        new Twig_Node_Expression_Filter($expression, array(array('escape', array())), $node->getLine())
+        , $node->getLine()
+      );
+    }
+  }
+
+  protected function needEscaping()
+  {
+    if (count($this->statusStack))
+    {
+      return $this->statusStack[count($this->statusStack) - 1];
+    }
+    else
+    {
+      return $this->env->hasExtension('escaper') ? $this->env->getExtension('escaper')->isGlobal() : false;
+    }
+  }
+}
diff --git a/lib/Twig/NodeTransformer/Filter.php b/lib/Twig/NodeTransformer/Filter.php
new file mode 100644 (file)
index 0000000..6a5dd8f
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_NodeTransformer_Filter extends Twig_NodeTransformer
+{
+  protected $statusStack = array();
+
+  public function visit(Twig_Node $node)
+  {
+    // filter?
+    if ($node instanceof Twig_Node_Filter)
+    {
+      $this->statusStack[] = $node->getFilter();
+
+      $node = $this->visitDeep($node);
+
+      array_pop($this->statusStack);
+
+      return $node;
+    }
+
+    if (!$node instanceof Twig_Node_Print && !$node instanceof Twig_Node_Text)
+    {
+      return $this->visitDeep($node);
+    }
+
+    if (false === $filter = $this->getCurrentFilter())
+    {
+      return $node;
+    }
+
+    if ($node instanceof Twig_Node_Text)
+    {
+      $expression = new Twig_Node_Expression_Constant($node->getData(), $node->getLine());
+    }
+    else
+    {
+      $expression = $node->getExpression();
+    }
+
+    // filter
+    if ($expression instanceof Twig_Node_Expression_Filter)
+    {
+      $expression->appendFilter(array($filter, array()));
+
+      return $node;
+    }
+    else
+    {
+      return new Twig_Node_Print(
+        new Twig_Node_Expression_Filter($expression, array(array($filter, array())), $node->getLine())
+        , $node->getLine()
+      );
+    }
+  }
+
+  protected function getCurrentFilter()
+  {
+    return count($this->statusStack) ? $this->statusStack[count($this->statusStack) - 1] : false;
+  }
+}
diff --git a/lib/Twig/NodeTransformer/Sandbox.php b/lib/Twig/NodeTransformer/Sandbox.php
new file mode 100644 (file)
index 0000000..0b08819
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_NodeTransformer_Sandbox extends Twig_NodeTransformer
+{
+  protected $inAModule = false;
+  protected $tags;
+  protected $filters;
+
+  public function visit(Twig_Node $node)
+  {
+    if ($node instanceof Twig_Node_Module)
+    {
+      $this->inAModule = true;
+      $this->tags = array();
+      $this->filters = array();
+
+      $node = $this->visitDeep($node);
+
+      $node->setUsedFilters(array_keys($this->filters));
+      $node->setUsedTags(array_keys($this->tags));
+      $this->inAModule = false;
+
+      return $node;
+    }
+
+    if (!$this->inAModule)
+    {
+      return $node;
+    }
+
+    // look for tags
+    if ($node->getTag())
+    {
+      $this->tags[$node->getTag()] = true;
+    }
+
+    // look for filters
+    if ($node instanceof Twig_Node_Expression_Filter)
+    {
+      foreach ($node->getFilters() as $filter)
+      {
+        $this->filters[$filter[0]] = true;
+      }
+    }
+
+    $this->visitDeep($node);
+
+    return $node;
+  }
+}
diff --git a/lib/Twig/Parser.php b/lib/Twig/Parser.php
new file mode 100644 (file)
index 0000000..def516d
--- /dev/null
@@ -0,0 +1,213 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Parser
+{
+  protected $stream;
+  protected $extends;
+  protected $handlers;
+  protected $transformers;
+  protected $expressionParser;
+  protected $blocks;
+  protected $currentBlock;
+  protected $env;
+
+  public function __construct(Twig_Environment $env = null)
+  {
+    $this->setEnvironment($env);
+  }
+
+  public function setEnvironment(Twig_Environment $env)
+  {
+    $this->env = $env;
+
+    $this->handlers = array();
+    $this->transformers = array();
+
+    // tag handlers
+    foreach ($this->env->getTokenParsers() as $handler)
+    {
+      $handler->setParser($this);
+
+      $this->handlers[$handler->getTag()] = $handler;
+    }
+
+    // node transformers
+    $this->transformers = $env->getNodeTransformers();
+  }
+
+  /**
+   * Converts a token stream to a node tree.
+   *
+   * @param  Twig_TokenStream $stream A token stream instance
+   *
+   * @return Twig_Node_Module A node tree
+   */
+  public function parse(Twig_TokenStream $stream)
+  {
+    if (null === $this->expressionParser)
+    {
+      $this->expressionParser = new Twig_ExpressionParser($this);
+    }
+
+    $this->stream = $stream;
+    $this->extends = null;
+    $this->blocks = array();
+    $this->currentBlock = null;
+
+    try
+    {
+      $body = $this->subparse(null);
+    }
+    catch (Twig_SyntaxError $e)
+    {
+      if (is_null($e->getFilename()))
+      {
+        $e->setFilename($this->stream->getFilename());
+      }
+
+      throw $e;
+    }
+
+    if (!is_null($this->extends))
+    {
+      foreach ($this->blocks as $block)
+      {
+        $block->setParent($this->extends);
+      }
+    }
+
+    $node = new Twig_Node_Module($body, $this->extends, $this->blocks, $this->stream->getFilename());
+
+    $transformer = new Twig_NodeTransformer_Chain($this->transformers);
+    $transformer->setEnvironment($this->env);
+    $node = $transformer->visit($node);
+
+    return $node;
+  }
+
+  public function subparse($test, $drop_needle = false)
+  {
+    $lineno = $this->getCurrentToken()->getLine();
+    $rv = array();
+    while (!$this->stream->isEOF())
+    {
+      switch ($this->getCurrentToken()->getType())
+      {
+        case Twig_Token::TEXT_TYPE:
+          $token = $this->stream->next();
+          $rv[] = new Twig_Node_Text($token->getValue(), $token->getLine());
+          break;
+
+        case Twig_Token::VAR_START_TYPE:
+          $token = $this->stream->next();
+          $expr = $this->expressionParser->parseExpression();
+          $this->stream->expect(Twig_Token::VAR_END_TYPE);
+          $rv[] = new Twig_Node_Print($expr, $token->getLine());
+          break;
+
+        case Twig_Token::BLOCK_START_TYPE:
+          $this->stream->next();
+          $token = $this->getCurrentToken();
+
+          if ($token->getType() !== Twig_Token::NAME_TYPE)
+          {
+            throw new Twig_SyntaxError('A block must start with a tag name', $token->getLine());
+          }
+
+          if (!is_null($test) && call_user_func($test, $token))
+          {
+            if ($drop_needle)
+            {
+              $this->stream->next();
+            }
+
+            return new Twig_NodeList($rv, $lineno);
+          }
+
+          if (!isset($this->handlers[$token->getValue()]))
+          {
+            throw new Twig_SyntaxError(sprintf('Unknown tag name "%s"', $token->getValue()), $token->getLine());
+          }
+
+          $this->stream->next();
+
+          $subparser = $this->handlers[$token->getValue()];
+          $node = $subparser->parse($token);
+          if (!is_null($node))
+          {
+            $rv[] = $node;
+          }
+          break;
+
+        default:
+          throw new LogicException('Lexer or parser ended up in unsupported state.');
+      }
+    }
+
+    return new Twig_NodeList($rv, $lineno);
+  }
+
+  public function addHandler($name, $class)
+  {
+    $this->handlers[$name] = $class;
+  }
+
+  public function addTransformer(Twig_NodeTransformer $transformer)
+  {
+    $this->transformers[] = $transformer;
+  }
+
+  public function getCurrentBlock()
+  {
+    return $this->currentBlock;
+  }
+
+  public function setCurrentBlock($name)
+  {
+    $this->currentBlock = $name;
+  }
+
+  public function hasBlock($name)
+  {
+    return isset($this->blocks[$name]);
+  }
+
+  public function setBlock($name, $value)
+  {
+    $this->blocks[$name] = $value;
+  }
+
+  public function getExpressionParser()
+  {
+    return $this->expressionParser;
+  }
+
+  public function getParent()
+  {
+    return $this->extends;
+  }
+
+  public function setParent($extends)
+  {
+    $this->extends = $extends;
+  }
+
+  public function getStream()
+  {
+    return $this->stream;
+  }
+
+  public function getCurrentToken()
+  {
+    return $this->stream->getCurrent();
+  }
+}
diff --git a/lib/Twig/ParserInterface.php b/lib/Twig/ParserInterface.php
new file mode 100644 (file)
index 0000000..34156e8
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Interface implemented by parser classes.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+interface Twig_ParserInterface
+{
+  /**
+   * Converts a token stream to a node tree.
+   *
+   * @param  Twig_TokenStream $stream A token stream instance
+   *
+   * @return Twig_Node_Module A node tree
+   */
+  public function parser(Twig_TokenStream $code);
+}
diff --git a/lib/Twig/RuntimeError.php b/lib/Twig/RuntimeError.php
new file mode 100644 (file)
index 0000000..5457f4d
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Exception thrown when an error occurs at runtime.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_RuntimeError extends Twig_Error
+{
+}
diff --git a/lib/Twig/Sandbox/SecurityError.php b/lib/Twig/Sandbox/SecurityError.php
new file mode 100644 (file)
index 0000000..94bdae6
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Exception thrown when a security error occurs at runtime.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Sandbox_SecurityError extends Twig_Error
+{
+}
diff --git a/lib/Twig/Sandbox/SecurityPolicy.php b/lib/Twig/Sandbox/SecurityPolicy.php
new file mode 100644 (file)
index 0000000..92b3756
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a security policy which need to be enforced when sandbox mode is enabled.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Sandbox_SecurityPolicy implements Twig_Sandbox_SecurityPolicyInterface
+{
+  protected $allowedTags;
+  protected $allowedFilters;
+  protected $allowedMethods;
+
+  public function __construct(array $allowedTags = array(), array $allowedFilters = array(), array $allowedMethods = array())
+  {
+    $this->allowedTags = $allowedTags;
+    $this->allowedFilters = $allowedFilters;
+    $this->allowedMethods = $allowedMethods;
+  }
+
+  public function setAllowedTags(array $tags)
+  {
+    $this->allowedTags = $tags;
+  }
+
+  public function setAllowedFilters(array $filters)
+  {
+    $this->allowedFilters = $filters;
+  }
+
+  public function setAllowedMethods(array $methods)
+  {
+    $this->allowedMethods = $methods;
+  }
+
+  public function checkSecurity($tags, $filters)
+  {
+    foreach ($tags as $tag)
+    {
+      if (!in_array($tag, $this->allowedTags))
+      {
+        throw new Twig_Sandbox_SecurityError(sprintf('Tag "%s" is not allowed.', $tag));
+      }
+    }
+
+    foreach ($filters as $filter)
+    {
+      if (!in_array($filter, $this->allowedFilters))
+      {
+        throw new Twig_Sandbox_SecurityError(sprintf('Filter "%s" is not allowed.', $filter));
+      }
+    }
+  }
+
+  public function checkMethodAllowed($obj, $method)
+  {
+    $allowed = false;
+    foreach ($this->allowedMethods as $class => $methods)
+    {
+      if ($obj instanceof $class)
+      {
+        $allowed = in_array($method, is_array($methods) ? $methods : array($methods));
+
+        break;
+      }
+    }
+
+    if (!$allowed)
+    {
+      throw new Twig_Sandbox_SecurityError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, get_class($obj)));
+    }
+  }
+}
diff --git a/lib/Twig/Sandbox/SecurityPolicyInterface.php b/lib/Twig/Sandbox/SecurityPolicyInterface.php
new file mode 100644 (file)
index 0000000..afb7e61
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Interfaces that all security policy classes must implements.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+interface Twig_Sandbox_SecurityPolicyInterface
+{
+  public function checkSecurity($tags, $filters);
+
+  public function checkMethodAllowed($obj, $method);
+}
diff --git a/lib/Twig/SyntaxError.php b/lib/Twig/SyntaxError.php
new file mode 100644 (file)
index 0000000..865a542
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Exception thrown when a syntax error occurs during lexing or parsing of a template.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_SyntaxError extends Twig_Error
+{
+  protected $lineno;
+  protected $filename;
+  protected $rawMessage;
+
+  public function __construct($message, $lineno, $filename = null)
+  {
+    $this->lineno = $lineno;
+    $this->filename = $filename;
+    $this->rawMessage = $message;
+
+    $this->updateRepr();
+  }
+
+  public function getFilename()
+  {
+    return $this->filename;
+  }
+
+  public function setFilename($filename)
+  {
+    $this->filename = $filename;
+
+    $this->updateRepr();
+  }
+
+  protected function updateRepr()
+  {
+    $this->message = $this->rawMessage.' in '.($this->filename ? $this->filename : 'n/a').' at line '.$this->lineno;
+  }
+}
diff --git a/lib/Twig/Template.php b/lib/Twig/Template.php
new file mode 100644 (file)
index 0000000..6154385
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+abstract class Twig_Template implements Twig_TemplateInterface
+{
+  protected $env;
+
+  public function __construct(Twig_Environment $env)
+  {
+    $this->env = $env;
+  }
+
+  /**
+   * Renders the template with the given context and returns it as string.
+   */
+  public function render($context)
+  {
+    ob_start();
+    $this->display($context);
+
+    return ob_get_clean();
+  }
+
+  public function getEnvironment()
+  {
+    return $this->env;
+  }
+
+  protected function resolveMissingFilter($name)
+  {
+    throw new Twig_RuntimeError(sprintf('The filter "%s" does not exist', $name));
+  }
+
+  protected function getAttribute($object, $item)
+  {
+    $item = (string) $item;
+
+    if (is_array($object) && isset($object[$item]))
+    {
+      return $object[$item];
+    }
+
+    if (
+      !is_object($object) ||
+      (
+        !method_exists($object, $method = $item) &&
+        !method_exists($object, $method = 'get'.ucfirst($item))
+      )
+    )
+    {
+      return null;
+    }
+
+    if ($this->env->hasExtension('sandbox'))
+    {
+      $this->env->getExtension('sandbox')->checkMethodAllowed($object, $method);
+    }
+
+    return $object->$method();
+  }
+}
diff --git a/lib/Twig/TemplateInterface.php b/lib/Twig/TemplateInterface.php
new file mode 100644 (file)
index 0000000..ac69fbd
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+interface Twig_TemplateInterface
+{
+  public function render($context);
+
+  public function display($context);
+
+  public function getEnvironment();
+}
diff --git a/lib/Twig/Token.php b/lib/Twig/Token.php
new file mode 100644 (file)
index 0000000..4c1c907
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Token
+{
+  protected $value;
+  protected $type;
+  protected $lineno;
+
+  const EOF_TYPE         = -1;
+  const TEXT_TYPE        = 0;
+  const BLOCK_START_TYPE = 1;
+  const VAR_START_TYPE   = 2;
+  const BLOCK_END_TYPE   = 3;
+  const VAR_END_TYPE     = 4;
+  const NAME_TYPE        = 5;
+  const NUMBER_TYPE      = 6;
+  const STRING_TYPE      = 7;
+  const OPERATOR_TYPE    = 8;
+
+  public function __construct($type, $value, $lineno)
+  {
+    $this->type   = $type;
+    $this->value  = $value;
+    $this->lineno = $lineno;
+  }
+
+  public function __toString()
+  {
+    return sprintf('%s(%s)', self::getTypeAsString($this->type, true), $this->value);
+  }
+
+  /**
+   * Test the current token for a type.  The first argument is the type
+   * of the token (if not given Twig_Token::NAME_NAME), the second the
+   * value of the token (if not given value is not checked).
+   * the token value can be an array if multiple checks shoudl be
+   * performed.
+   */
+  public function test($type, $values = null)
+  {
+    if (is_null($values) && !is_int($type))
+    {
+      $values = $type;
+      $type = self::NAME_TYPE;
+    }
+
+    return ($this->type === $type) && (
+      is_null($values) ||
+      (is_array($values) && in_array($this->value, $values)) ||
+      $this->value == $values
+    );
+  }
+
+  public function getLine()
+  {
+    return $this->lineno;
+  }
+
+  public function getType()
+  {
+    return $this->type;
+  }
+
+  public function getValue()
+  {
+    return $this->value;
+  }
+
+  public function setValue($value)
+  {
+    $this->value = $value;
+  }
+
+  static public function getTypeAsString($type, $short = false)
+  {
+    switch ($type)
+    {
+      case 0:
+        return 'TEXT_TYPE';
+      case -1:
+        return 'EOF_TYPE';
+      case 1:
+        return 'BLOCK_START_TYPE';
+      case 2:
+        return 'VAR_START_TYPE';
+      case 3:
+        return 'BLOCK_END_TYPE';
+      case 4:
+        return 'VAR_END_TYPE';
+      case 5:
+        return 'NAME_TYPE';
+      case 6:
+        return 'NUMBER_TYPE';
+      case 7:
+        return 'STRING_TYPE';
+      case 8:
+        return 'OPERATOR_TYPE';
+    }
+
+    return $short ? $name : 'Twig_Token::'.$name;
+  }
+}
diff --git a/lib/Twig/TokenParser.php b/lib/Twig/TokenParser.php
new file mode 100644 (file)
index 0000000..9fe3dc5
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+abstract class Twig_TokenParser
+{
+  protected $parser;
+
+  public function setParser(Twig_Parser $parser)
+  {
+    $this->parser = $parser;
+  }
+
+  abstract public function parse(Twig_Token $token);
+
+  abstract public function getTag();
+}
diff --git a/lib/Twig/TokenParser/AutoEscape.php b/lib/Twig/TokenParser/AutoEscape.php
new file mode 100644 (file)
index 0000000..45e514a
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_AutoEscape extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    $lineno = $token->getLine();
+    $value = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+    if (!in_array($value, array('on', 'off')))
+    {
+      throw new Twig_SyntaxError("Autoescape value must be 'on' or 'off'", $lineno);
+    }
+
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+    $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true);
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return new Twig_Node_AutoEscape('on' === $value ? true : false, $body, $lineno, $this->getTag());
+  }
+
+  public function decideBlockEnd($token)
+  {
+    return $token->test('endautoescape');
+  }
+
+  public function getTag()
+  {
+    return 'autoescape';
+  }
+}
diff --git a/lib/Twig/TokenParser/Block.php b/lib/Twig/TokenParser/Block.php
new file mode 100644 (file)
index 0000000..a6df2a5
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_Block extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    $lineno = $token->getLine();
+    $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+    if ($this->parser->hasBlock($name))
+    {
+      throw new Twig_SyntaxError("The block '$name' has already been defined", $lineno);
+    }
+    $this->parser->setCurrentBlock($name);
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+    $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true);
+    if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE))
+    {
+      $value = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+
+      if ($value != $name)
+      {
+        throw new Twig_SyntaxError(sprintf("Expected endblock for block '$name' (but %s given)", $value), $lineno);
+      }
+    }
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    $block = new Twig_Node_Block($name, $body, $lineno);
+    $this->parser->setBlock($name, $block);
+    $this->parser->setCurrentBlock(null);
+
+    return new Twig_Node_BlockReference($name, $lineno, $this->getTag());
+  }
+
+  public function decideBlockEnd($token)
+  {
+    return $token->test('endblock');
+  }
+
+  public function getTag()
+  {
+    return 'block';
+  }
+}
diff --git a/lib/Twig/TokenParser/Call.php b/lib/Twig/TokenParser/Call.php
new file mode 100644 (file)
index 0000000..c564fb4
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_Call extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    $lineno = $token->getLine();
+    $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+
+    // arguments
+    $arguments = array();
+
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return new Twig_Node_Call($name, $arguments, $lineno, $this->getTag());
+  }
+
+  public function getTag()
+  {
+    return 'call';
+  }
+}
diff --git a/lib/Twig/TokenParser/Display.php b/lib/Twig/TokenParser/Display.php
new file mode 100644 (file)
index 0000000..ea201e8
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_Display extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    $lineno = $token->getLine();
+    $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+    if (!$this->parser->hasBlock($name))
+    {
+      throw new Twig_SyntaxError("The block '$name' cannot be displayed as it has not yet been defined", $lineno);
+    }
+    $this->parser->setCurrentBlock($name);
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return new Twig_Node_BlockReference($name, $lineno, $this->getTag());
+  }
+
+  public function getTag()
+  {
+    return 'display';
+  }
+}
diff --git a/lib/Twig/TokenParser/Extends.php b/lib/Twig/TokenParser/Extends.php
new file mode 100644 (file)
index 0000000..9fa9f77
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_Extends extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    if (null !== $this->parser->getParent())
+    {
+      throw new Twig_SyntaxError('Multiple extend tags are forbidden', $token->getLine());
+    }
+    $this->parser->setParent($this->parser->getStream()->expect(Twig_Token::STRING_TYPE)->getValue());
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return null;
+  }
+
+  public function getTag()
+  {
+    return 'extends';
+  }
+}
diff --git a/lib/Twig/TokenParser/Filter.php b/lib/Twig/TokenParser/Filter.php
new file mode 100644 (file)
index 0000000..ddd7c63
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_Filter extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    $lineno = $token->getLine();
+    $filter = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+    $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true);
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return new Twig_Node_Filter($filter, $body, $lineno, $this->getTag());
+  }
+
+  public function decideBlockEnd($token)
+  {
+    return $token->test('endfilter');
+  }
+
+  public function getTag()
+  {
+    return 'filter';
+  }
+}
diff --git a/lib/Twig/TokenParser/For.php b/lib/Twig/TokenParser/For.php
new file mode 100644 (file)
index 0000000..ced0235
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_For extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    $lineno = $token->getLine();
+    list($is_multitarget, $item) = $this->parser->getExpressionParser()->parseAssignmentExpression();
+    $this->parser->getStream()->expect('in');
+    $seq = $this->parser->getExpressionParser()->parseExpression();
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+    $body = $this->parser->subparse(array($this, 'decideForFork'));
+    if ($this->parser->getStream()->next()->getValue() == 'else')
+    {
+      $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+      $else = $this->parser->subparse(array($this, 'decideForEnd'), true);
+    }
+    else
+    {
+      $else = null;
+    }
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return new Twig_Node_For($is_multitarget, $item, $seq, $body, $else, $lineno, $this->getTag());
+  }
+
+  public function decideForFork($token)
+  {
+    return $token->test(array('else', 'endfor'));
+  }
+
+  public function decideForEnd($token)
+  {
+    return $token->test('endfor');
+  }
+
+  public function getTag()
+  {
+    return 'for';
+  }
+}
diff --git a/lib/Twig/TokenParser/If.php b/lib/Twig/TokenParser/If.php
new file mode 100644 (file)
index 0000000..adf8f7b
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_If extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    $lineno = $token->getLine();
+    $expr = $this->parser->getExpressionParser()->parseExpression();
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+    $body = $this->parser->subparse(array($this, 'decideIfFork'));
+    $tests = array(array($expr, $body));
+    $else = null;
+
+    $end = false;
+    while (!$end)
+    {
+      switch ($this->parser->getStream()->next()->getValue())
+      {
+        case 'else':
+          $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+          $else = $this->parser->subparse(array($this, 'decideIfEnd'));
+          break;
+
+        case 'elseif':
+          $expr = $this->parser->getExpressionParser()->parseExpression();
+          $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+          $body = $this->parser->subparse(array($this, 'decideIfFork'));
+          $tests[] = array($expr, $body);
+          break;
+
+        case 'endif':
+          $end = true;
+          break;
+      }
+    }
+
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return new Twig_Node_If($tests, $else, $lineno, $this->getTag());
+  }
+
+  public function decideIfFork($token)
+  {
+    return $token->test(array('elseif', 'else', 'endif'));
+  }
+
+  public function decideIfEnd($token)
+  {
+    return $token->test(array('endif'));
+  }
+
+  public function getTag()
+  {
+    return 'if';
+  }
+}
diff --git a/lib/Twig/TokenParser/Include.php b/lib/Twig/TokenParser/Include.php
new file mode 100644 (file)
index 0000000..240d629
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_Include extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    $expr = $this->parser->getExpressionParser()->parseExpression();
+
+    $sandboxed = false;
+    if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE))
+    {
+      $this->parser->getStream()->expect(Twig_Token::NAME_TYPE);
+      $sandboxed = true;
+    }
+
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return new Twig_Node_Include($expr, $sandboxed, $token->getLine(), $this->getTag());
+  }
+
+  public function getTag()
+  {
+    return 'include';
+  }
+}
diff --git a/lib/Twig/TokenParser/Macro.php b/lib/Twig/TokenParser/Macro.php
new file mode 100644 (file)
index 0000000..45d6567
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_Macro extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    $lineno = $token->getLine();
+    $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+
+    // arguments
+    
+
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+    $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true);
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return new Twig_Node_Macro($name, $body, $lineno, $this->getTag());
+  }
+
+  public function decideBlockEnd($token)
+  {
+    return $token->test('endmacro');
+  }
+
+  public function getTag()
+  {
+    return 'macro';
+  }
+}
diff --git a/lib/Twig/TokenParser/Parent.php b/lib/Twig/TokenParser/Parent.php
new file mode 100644 (file)
index 0000000..9771488
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_Parent extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    if (null === $this->parser->getCurrentBlock())
+    {
+      throw new Twig_SyntaxError('Calling "parent" outside a block is forbidden', $token->getLine());
+    }
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return new Twig_Node_Parent($this->parser->getCurrentBlock(), $token->getLine(), $this->getTag());
+  }
+
+  public function getTag()
+  {
+    return 'parent';
+  }
+}
diff --git a/lib/Twig/TokenParser/Set.php b/lib/Twig/TokenParser/Set.php
new file mode 100644 (file)
index 0000000..01f011f
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+class Twig_TokenParser_Set extends Twig_TokenParser
+{
+  public function parse(Twig_Token $token)
+  {
+    $lineno = $token->getLine();
+    $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+    $value = $this->parser->getExpressionParser()->parseExpression();
+
+    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+    return new Twig_Node_Set($name, $value, $lineno, $this->getTag());
+  }
+
+  public function getTag()
+  {
+    return 'set';
+  }
+}
diff --git a/lib/Twig/TokenStream.php b/lib/Twig/TokenStream.php
new file mode 100644 (file)
index 0000000..7b255d5
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenStream
+{
+  protected $pushed;
+  protected $originalTokens;
+  protected $tokens;
+  protected $eof;
+  protected $current;
+  protected $filename;
+  protected $trimBlocks;
+
+  public function __construct(array $tokens, $filename, $trimBlocks = true)
+  {
+    $this->pushed = array();
+    $this->originalTokens = $tokens;
+    $this->tokens = $tokens;
+    $this->filename = $filename;
+    $this->trimBlocks = $trimBlocks;
+    $this->next();
+  }
+
+  public function __toString()
+  {
+    $repr = '';
+    foreach ($this->originalTokens as $token)
+    {
+      $repr .= $token."\n";
+    }
+
+    return $repr;
+  }
+
+  public function push($token)
+  {
+    $this->pushed[] = $token;
+  }
+
+  /**
+   * Sets the pointer to the next token and returns the old one.
+   */
+  public function next()
+  {
+    if (!empty($this->pushed))
+    {
+      $token = array_shift($this->pushed);
+    }
+    else
+    {
+      $token = array_shift($this->tokens);
+    }
+
+    // trim blocks
+    if ($this->trimBlocks &&
+      $this->current &&
+      Twig_Token::BLOCK_END_TYPE === $this->current->getType() &&
+      Twig_Token::TEXT_TYPE === $token->getType() &&
+      $token->getValue() &&
+      '\n' === substr($token->getValue(), 0, 2)
+    )
+    {
+      $value = substr($token->getValue(), 2);
+
+      if (!$value)
+      {
+        return $this->next();
+      }
+
+      $token->setValue($value);
+    }
+
+    $old = $this->current;
+    $this->current = $token;
+
+    $this->eof = $token->getType() === Twig_Token::EOF_TYPE;
+
+    return $old;
+  }
+
+  /**
+   * Looks at the next token.
+   */
+  public function look()
+  {
+    $old = $this->next();
+    $new = $this->current;
+    $this->push($old);
+    $this->push($new);
+
+    return $new;
+  }
+
+  /**
+   * Expects a token (like $token->test()) and returns it or throw a syntax error.
+   */
+  public function expect($primary, $secondary = null)
+  {
+    $token = $this->current;
+    if (!$token->test($primary, $secondary))
+    {
+      throw new Twig_SyntaxError(sprintf('Unexpected token %s (%s expected, value: %s)', Twig_Token::getTypeAsString($token->getType()), Twig_Token::getTypeAsString($primary), $token->getValue()), $this->current->getLine());
+    }
+    $this->next();
+
+    return $token;
+  }
+
+  /**
+   * Forwards that call to the current token.
+   */
+  public function test($primary, $secondary = null)
+  {
+    return $this->current->test($primary, $secondary);
+  }
+
+  public function isEOF()
+  {
+    return $this->eof;
+  }
+
+  public function getCurrent()
+  {
+    return $this->current;
+  }
+
+  public function getFilename()
+  {
+    return $this->filename;
+  }
+}
diff --git a/lib/Twig/runtime.php b/lib/Twig/runtime.php
new file mode 100644 (file)
index 0000000..0b5dd24
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+function twig_date_format_filter($timestamp, $format = 'F j, Y H:i')
+{
+  return date($format, $timestamp);
+}
+
+function twig_urlencode_filter($url, $raw = false)
+{
+  if ($raw)
+  {
+    return rawurlencode($url);
+  }
+
+  return urlencode($url);
+}
+
+function twig_join_filter($value, $glue = '')
+{
+  return implode($glue, (array) $value);
+}
+
+function twig_default_filter($value, $default = '')
+{
+  return is_null($value) ? $default : $value;
+}
+
+function twig_get_array_keys_filter($array)
+{
+  if (is_object($array) && $array instanceof Iterator)
+  {
+    $keys = array();
+    foreach ($array as $key => $value)
+    {
+      $keys[] = $key;
+    }
+
+    return $keys;
+  }
+
+  if (!is_array($array))
+  {
+    return array();
+  }
+
+  return array_keys($array);
+}
+
+function twig_reverse_filter($array)
+{
+  if (is_object($array) && $array instanceof Iterator)
+  {
+    $values = array();
+    foreach ($array as $value)
+    {
+      $values[] = $value;
+    }
+
+    return array_reverse($values);
+  }
+
+  if (!is_array($array))
+  {
+    return array();
+  }
+
+  return array_reverse($array);
+}
+
+function twig_is_even_filter($value)
+{
+  return $value % 2 == 0;
+}
+
+function twig_is_odd_filter($value)
+{
+  return $value % 2 == 1;
+}
+
+function twig_length_filter($thing)
+{
+  return is_string($thing) ? strlen($thing) : count($thing);
+}
+
+function twig_sort_filter($array)
+{
+  asort($array);
+
+  return $array;
+}
+
+// add multibyte extensions if possible
+if (function_exists('mb_get_info'))
+{
+  function twig_upper_filter(Twig_TemplateInterface $template, $string)
+  {
+    if (!is_null($template->getEnvironment()->getCharset()))
+    {
+      return mb_strtoupper($string, $template->getEnvironment()->getCharset());
+    }
+
+    return strtoupper($string);
+  }
+
+  function twig_lower_filter(Twig_TemplateInterface $template, $string)
+  {
+    if (!is_null($template->getEnvironment()->getCharset()))
+    {
+      return mb_strtolower($string, $template->getEnvironment()->getCharset());
+    }
+
+    return strtolower($string);
+  }
+
+  function twig_title_string_filter(Twig_TemplateInterface $template, $string)
+  {
+    if (is_null($template->getEnvironment()->getCharset()))
+    {
+      return ucwords(strtolower($string));
+    }
+
+    return mb_convert_case($string, MB_CASE_TITLE, $template->getEnvironment()->getCharset());
+  }
+
+  function twig_capitalize_string_filter(Twig_TemplateInterface $template, $string)
+  {
+    if (is_null($template->getEnvironment()->getCharset()))
+    {
+      return ucfirst(strtolower($string));
+    }
+
+    return mb_strtoupper(mb_substr($string, 0, 1, $template->getEnvironment()->getCharset())).
+           mb_strtolower(mb_substr($string, 1, mb_strlen($string), $template->getEnvironment()->getCharset()));
+  }
+}
+// and byte fallback
+else
+{
+  function twig_title_string_filter(Twig_TemplateInterface $template, $string)
+  {
+    return ucwords(strtolower($string));
+  }
+
+  function twig_capitalize_string_filter(Twig_TemplateInterface $template, $string)
+  {
+    return ucfirst(strtolower($string));
+  }
+}
diff --git a/lib/Twig/runtime_escaper.php b/lib/Twig/runtime_escaper.php
new file mode 100644 (file)
index 0000000..b62badd
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+// tells the escaper node transformer that the string is safe
+function twig_safe_filter($string)
+{
+  return $string;
+}
+
+function twig_escape_filter(Twig_TemplateInterface $template, $string)
+{
+  if (!is_string($string))
+  {
+    return $string;
+  }
+
+  return htmlspecialchars($string, ENT_QUOTES, $template->getEnvironment()->getCharset());
+}
diff --git a/lib/Twig/runtime_for.php b/lib/Twig/runtime_for.php
new file mode 100644 (file)
index 0000000..5b33c71
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_LoopContextIterator implements Iterator
+{
+  public $context;
+  public $seq;
+  public $idx;
+  public $length;
+  public $parent;
+
+  public function __construct(&$context, $seq, $parent)
+  {
+    $this->context = $context;
+    $this->seq = $seq;
+    $this->length = count($this->seq);
+    $this->parent = $parent;
+  }
+
+  public function rewind()
+  {
+    $this->idx = 0;
+  }
+
+  public function key()
+  {
+  }
+
+  public function valid()
+  {
+    return $this->idx < $this->length;
+  }
+
+  public function next()
+  {
+    $this->idx++;
+  }
+
+  public function current()
+  {
+    return $this;
+  }
+}
+
+function twig_iterate(&$context, $seq)
+{
+  $parent = isset($context['loop']) ? $context['loop'] : null;
+
+  // convert the sequence to an array of values
+  // we convert Iterators as an array
+  // as our iterator access the sequence as an array
+  if (is_array($seq))
+  {
+    $array = array_values($seq);
+  }
+  elseif (is_object($seq) && $seq instanceof Iterator)
+  {
+    $array = array();
+    foreach ($seq as $value)
+    {
+      $array[] = $value;
+    }
+  }
+  else
+  {
+    $array = array();
+  }
+
+  $context['loop'] = array('parent' => $parent);
+
+  return new Twig_LoopContextIterator($context, $array, $parent);
+}
+
+function twig_set_loop_context(&$context, $iterator, $target)
+{
+  if (is_array($target))
+  {
+    foreach (array_combine($target, $iterator->seq[$iterator->idx]) as $key => $value)
+    {
+      $context[$key] = $value;
+    }
+  }
+  else
+  {
+    $context[$target] = $iterator->seq[$iterator->idx];
+  }
+
+  $context['loop'] = array(
+    'parent'    => $iterator->parent,
+    'length'    => $iterator->length,
+    'index0'    => $iterator->idx,
+    'index'     => $iterator->idx + 1,
+    'revindex0' => $iterator->length - $iterator->idx - 1,
+    'revindex'  => $iterator->length - $iterator->idx,
+    'first'     => $iterator->idx == 0,
+    'last'      => $iterator->idx + 1 == $iterator->length,
+  );
+}
+
+function twig_get_array_items_filter($array)
+{
+  if (!is_array($array) && is_object($array) && !$array instanceof Iterator)
+  {
+    return array(array(), array());
+  }
+
+  $result = array();
+  foreach ($array as $key => $value)
+  {
+    $result[] = array($key, $value);
+  }
+
+  return $result;
+}
diff --git a/test/bin/coverage.php b/test/bin/coverage.php
new file mode 100644 (file)
index 0000000..015801a
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ * 
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+require_once(dirname(__FILE__).'/../lib/lime/LimeAutoloader.php');
+LimeAutoloader::register();
+
+$suite = new LimeTestSuite(array(
+  'force_colors' => isset($argv) && in_array('--color', $argv),
+  'base_dir'     => realpath(dirname(__FILE__).'/..'),
+));
+
+foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator(dirname(__FILE__).'/../unit'), RecursiveIteratorIterator::LEAVES_ONLY) as $file)
+{
+  if (preg_match('/Test\.php$/', $file))
+  {
+    $suite->register($file->getRealPath());
+  }
+}
+
+$coverage = new LimeCoverage($suite, array(
+  'base_dir'  => realpath(dirname(__FILE__).'/../../lib'),
+  'extension' => '.php',
+  'verbose'   => true,
+));
+
+$files = array();
+foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator(dirname(__FILE__).'/../../lib'), RecursiveIteratorIterator::LEAVES_ONLY) as $file)
+{
+  if (preg_match('/\.php$/', $file))
+  {
+    $files[] = $file->getRealPath();
+  }
+}
+$coverage->setFiles($files);
+
+$coverage->run();
diff --git a/test/bin/prove.php b/test/bin/prove.php
new file mode 100644 (file)
index 0000000..be7bb1b
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+require_once(dirname(__FILE__).'/../lib/lime/LimeAutoloader.php');
+LimeAutoloader::register();
+
+$suite = new LimeTestSuite(array(
+  'force_colors' => isset($argv) && in_array('--color', $argv),
+  'base_dir'     => realpath(dirname(__FILE__).'/..'),
+));
+
+foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator(dirname(__FILE__).'/../unit'), RecursiveIteratorIterator::LEAVES_ONLY) as $file)
+{
+  if (preg_match('/Test\.php$/', $file))
+  {
+    $suite->register($file->getRealPath());
+  }
+}
+
+exit($suite->run() ? 0 : 1);
diff --git a/test/fixtures/expressions/binary.test b/test/fixtures/expressions/binary.test
new file mode 100644 (file)
index 0000000..a969c5d
--- /dev/null
@@ -0,0 +1,40 @@
+--TEST--
+Twig supports binary operations (+, -, *, /, ~, %, and, or)
+--TEMPLATE--
+{{ 1 + 1 }}
+{{ 2 - 1 }}
+{{ 2 * 2 }}
+{{ 2 / 2 }}
+{{ 3 % 2 }}
+{{ 1 and 1 }}
+{{ 1 and 0 }}
+{{ 0 and 1 }}
+{{ 0 and 0 }}
+{{ 1 or 1 }}
+{{ 1 or 0 }}
+{{ 0 or 1 }}
+{{ 0 or 0 }}
+{{ "foo" ~ "bar" }}
+{{ foo ~ "bar" }}
+{{ "foo" ~ bar }}
+{{ foo ~ bar }}
+--DATA--
+return array('foo' => 'bar', 'bar' => 'foo')
+--EXPECT--
+2
+1
+4
+1
+1
+1
+
+
+
+1
+1
+1
+
+foobar
+barbar
+foofoo
+barfoo
diff --git a/test/fixtures/expressions/comparison.test b/test/fixtures/expressions/comparison.test
new file mode 100644 (file)
index 0000000..4ed77e5
--- /dev/null
@@ -0,0 +1,16 @@
+--TEST--
+Twig supports comparison operators (==, !=, <, >, >=, <=)
+--TEMPLATE--
+{{ 1 > 2 }}/{{ 1 > 1 }}/{{ 1 >= 2 }}/{{ 1 >= 1 }}
+{{ 1 < 2 }}/{{ 1 < 1 }}/{{ 1 <= 2 }}/{{ 1 <= 1 }}
+{{ 1 == 1 }}/{{ 1 == 2 }}
+{{ 1 != 1 }}/{{ 1 != 2 }}
+{{ 1 < 2 < 3 }}/{{ 1 < 2 < 3 < 4 }}
+--DATA--
+return array()
+--EXPECT--
+///1
+1//1/1
+1/
+/1
+1/1
diff --git a/test/fixtures/expressions/grouping.test b/test/fixtures/expressions/grouping.test
new file mode 100644 (file)
index 0000000..79f8e0b
--- /dev/null
@@ -0,0 +1,8 @@
+--TEST--
+Twig supports grouping of expressions
+--TEMPLATE--
+{{ (2 + 2) / 2 }}
+--DATA--
+return array()
+--EXPECT--
+2
diff --git a/test/fixtures/expressions/ternary_operator.test b/test/fixtures/expressions/ternary_operator.test
new file mode 100644 (file)
index 0000000..cbf555c
--- /dev/null
@@ -0,0 +1,14 @@
+--TEST--
+Twig supports the ternary operator
+--TEMPLATE--
+{{ 1 ? 'YES' : 'NO' }}
+{{ 0 ? 'YES' : 'NO' }}
+{{ 0 ? 'YES' : (1 ? 'YES1' : 'NO1') }}
+{{ 0 ? 'YES' : (0 ? 'YES1' : 'NO1') }}
+--DATA--
+return array()
+--EXPECT--
+YES
+NO
+YES1
+NO1
diff --git a/test/fixtures/expressions/unary.test b/test/fixtures/expressions/unary.test
new file mode 100644 (file)
index 0000000..2e1cb35
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+Twig supports unary operators (not, -, +)
+--TEMPLATE--
+{{ not 1 }}/{{ not 0 }}
+{{ +1 + 1 }}/{{ -1 - 1 }}
+--DATA--
+return array()
+--EXPECT--
+/1
+2/-2
diff --git a/test/fixtures/filters/date.test b/test/fixtures/filters/date.test
new file mode 100644 (file)
index 0000000..c3f8625
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+"date" filter
+--TEMPLATE--
+{{ date1|date }}
+{{ date1|date('d/m/Y') }}
+--DATA--
+return array('date1' => mktime(13, 45, 0, 10, 4, 2010))
+--EXPECT--
+October 4, 2010 13:45
+04/10/2010
diff --git a/test/fixtures/filters/default.test b/test/fixtures/filters/default.test
new file mode 100644 (file)
index 0000000..8fd8017
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+"default" filter
+--TEMPLATE--
+{{ foo|default('bar') }}
+{{ bar|default('foo') }}
+--DATA--
+return array('foo' => null, 'bar' => 'bar')
+--EXPECT--
+bar
+bar
diff --git a/test/fixtures/filters/even.test b/test/fixtures/filters/even.test
new file mode 100644 (file)
index 0000000..835bb79
--- /dev/null
@@ -0,0 +1,9 @@
+--TEST--
+"even" filter
+--TEMPLATE--
+{{ 1|even }}
+{{ 2|even }}
+--DATA--
+return array()
+--EXPECT--
+1
diff --git a/test/fixtures/filters/format.test b/test/fixtures/filters/format.test
new file mode 100644 (file)
index 0000000..97221ff
--- /dev/null
@@ -0,0 +1,8 @@
+--TEST--
+"format" filter
+--TEMPLATE--
+{{ string|format(foo, 3) }}
+--DATA--
+return array('string' => '%s/%d', 'foo' => 'bar')
+--EXPECT--
+bar/3
diff --git a/test/fixtures/filters/length.test b/test/fixtures/filters/length.test
new file mode 100644 (file)
index 0000000..b5ef879
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+"length" filter
+--TEMPLATE--
+{{ array|length }}
+{{ string|length }}
+--DATA--
+return array('array' => array(1, 4), 'string' => 'foo')
+--EXPECT--
+2
+3
diff --git a/test/fixtures/filters/odd.test b/test/fixtures/filters/odd.test
new file mode 100644 (file)
index 0000000..1de0351
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+"odd" filter
+--TEMPLATE--
+{{ 1|odd }}
+{{ 2|odd }}
+--DATA--
+return array()
+--EXPECT--
+
+1
diff --git a/test/fixtures/filters/sort.test b/test/fixtures/filters/sort.test
new file mode 100644 (file)
index 0000000..21d575f
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+"sort" filter
+--TEMPLATE--
+{{ array1|sort|join }}
+{{ array2|sort|join }}
+--DATA--
+return array('array1' => array(4, 1), 'array2' => array('foo', 'bar'))
+--EXPECT--
+14
+barfoo
diff --git a/test/fixtures/tags/autoescape/basic.test b/test/fixtures/tags/autoescape/basic.test
new file mode 100644 (file)
index 0000000..6f6c1b9
--- /dev/null
@@ -0,0 +1,22 @@
+--TEST--
+"autoescape" tag applies escaping on its children
+--TEMPLATE--
+{% autoescape on %}
+{{ var }}<br />
+{% endautoescape %}
+{% autoescape off %}
+{{ var }}<br />
+{% endautoescape %}
+{% autoescape on %}
+{{ var }}<br />
+{% endautoescape %}
+{% autoescape off %}
+{{ var }}<br />
+{% endautoescape %}
+--DATA--
+return array('var' => '<br />')
+--EXPECT--
+&lt;br /&gt;<br />
+<br /><br />
+&lt;br /&gt;<br />
+<br /><br />
diff --git a/test/fixtures/tags/autoescape/double_escaping.test b/test/fixtures/tags/autoescape/double_escaping.test
new file mode 100644 (file)
index 0000000..fc7aa8c
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+"autoescape" tag does not double-escape
+--TEMPLATE--
+{% autoescape on %}
+{{ var|escape }}
+{% endautoescape %}
+--DATA--
+return array('var' => '<br />')
+--EXPECT--
+&lt;br /&gt;
diff --git a/test/fixtures/tags/autoescape/nested.test b/test/fixtures/tags/autoescape/nested.test
new file mode 100644 (file)
index 0000000..49f4da9
--- /dev/null
@@ -0,0 +1,26 @@
+--TEST--
+"autoescape" tags can be nested at will
+--TEMPLATE--
+{{ var }}
+{% autoescape on %}
+  {{ var }}
+  {% autoescape off %}
+    {{ var }}
+    {% autoescape on %}
+      {{ var }}
+    {% endautoescape %}
+    {{ var }}
+  {% endautoescape %}
+  {{ var }}
+{% endautoescape %}
+{{ var }}
+--DATA--
+return array('var' => '<br />')
+--EXPECT--
+&lt;br /&gt;
+  &lt;br /&gt;
+      <br />
+          &lt;br /&gt;
+        <br />
+    &lt;br /&gt;
+&lt;br /&gt;
diff --git a/test/fixtures/tags/autoescape/safe.test b/test/fixtures/tags/autoescape/safe.test
new file mode 100644 (file)
index 0000000..b8a7a37
--- /dev/null
@@ -0,0 +1,12 @@
+--TEST--
+"autoescape" tag does not escape when raw is used as a filter
+--TEMPLATE--
+{% autoescape on %}
+{{ var|safe }}
+{% endautoescape %}
+{% autoescape on %}
+{% endautoescape %}
+--DATA--
+return array('var' => '<br />')
+--EXPECT--
+<br />
diff --git a/test/fixtures/tags/filter/basic.test b/test/fixtures/tags/filter/basic.test
new file mode 100644 (file)
index 0000000..82094f2
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+"filter" tag applies a filter on its children
+--TEMPLATE--
+{% filter upper %}
+Some text with a {{ var }}
+{% endfilter %}
+--DATA--
+return array('var' => 'var')
+--EXPECT--
+SOME TEXT WITH A VAR
diff --git a/test/fixtures/tags/filter/nested.test b/test/fixtures/tags/filter/nested.test
new file mode 100644 (file)
index 0000000..d26bf6e
--- /dev/null
@@ -0,0 +1,16 @@
+--TEST--
+"filter" tags can be nested at will
+--TEMPLATE--
+{% filter capitalize %}
+  {{ var }}
+  {% filter upper %}
+    {{ var }}
+  {% endfilter %}
+  {{ var }}
+{% endfilter %}
+--DATA--
+return array('var' => 'var')
+--EXPECT--
+  Var
+      VAR
+    Var
diff --git a/test/fixtures/tags/filter/with_for_tag.test b/test/fixtures/tags/filter/with_for_tag.test
new file mode 100644 (file)
index 0000000..22745ea
--- /dev/null
@@ -0,0 +1,13 @@
+--TEST--
+"filter" tag applies the filter on "for" tags
+--TEMPLATE--
+{% filter upper %}
+{% for item in items %}
+{{ item }}
+{% endfor %}
+{% endfilter %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+A
+B
diff --git a/test/fixtures/tags/filter/with_if_tag.test b/test/fixtures/tags/filter/with_if_tag.test
new file mode 100644 (file)
index 0000000..02381dd
--- /dev/null
@@ -0,0 +1,29 @@
+--TEST--
+"filter" tag applies the filter on "if" tags
+--TEMPLATE--
+{% filter upper %}
+{% if items %}
+{{ items|join(', ') }}
+{% endif %}
+
+{% if items.3 %}
+FOO
+{% else %}
+{{ items.1 }}
+{% endif %}
+
+{% if items.3 %}
+FOO
+{% elseif items.1 %}
+{{ items.0 }}
+{% endif %}
+
+{% endfilter %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+A, B
+
+B
+
+A
diff --git a/test/fixtures/tags/for/context.test b/test/fixtures/tags/for/context.test
new file mode 100644 (file)
index 0000000..ddc6930
--- /dev/null
@@ -0,0 +1,18 @@
+--TEST--
+"for" tag keeps the context safe
+--TEMPLATE--
+{% for item in items %}
+  {% for item in items %}
+    * {{ item }}
+  {% endfor %}
+  * {{ item }}
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+      * a
+      * b
+    * a
+      * a
+      * b
+    * b
diff --git a/test/fixtures/tags/for/else.test b/test/fixtures/tags/for/else.test
new file mode 100644 (file)
index 0000000..8c6a7d2
--- /dev/null
@@ -0,0 +1,21 @@
+--TEST--
+"for" tag can use an "else" clause
+--TEMPLATE--
+{% for item in items %}
+  * {{ item }}
+{% else %}
+  no item
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+  * a
+  * b
+--DATA--
+return array('items' => array())
+--EXPECT--
+  no item
+--DATA--
+return array()
+--EXPECT--
+  no item
diff --git a/test/fixtures/tags/for/keys.test b/test/fixtures/tags/for/keys.test
new file mode 100644 (file)
index 0000000..4e22cb4
--- /dev/null
@@ -0,0 +1,11 @@
+--TEST--
+"for" tag can iterate over keys
+--TEMPLATE--
+{% for key in items|keys %}
+  * {{ key }}
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+  * 0
+  * 1
diff --git a/test/fixtures/tags/for/keys_and_values.test b/test/fixtures/tags/for/keys_and_values.test
new file mode 100644 (file)
index 0000000..c4ab516
--- /dev/null
@@ -0,0 +1,11 @@
+--TEST--
+"for" tag can iterate over keys and values
+--TEMPLATE--
+{% for key, item in items|items %}
+  * {{ key }}/{{ item }}
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+  * 0/a
+  * 1/b
diff --git a/test/fixtures/tags/for/loop_context.test b/test/fixtures/tags/for/loop_context.test
new file mode 100644 (file)
index 0000000..93bc76a
--- /dev/null
@@ -0,0 +1,19 @@
+--TEST--
+"for" tag adds a loop variable to the context
+--TEMPLATE--
+{% for item in items %}
+  * {{ loop.index }}/{{ loop.index0 }}
+  * {{ loop.revindex }}/{{ loop.revindex0 }}
+  * {{ loop.first }}/{{ loop.last }}/{{ loop.length }}
+
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+  * 1/0
+  * 2/1
+  * 1//2
+
+  * 2/1
+  * 1/0
+  * /1/2
diff --git a/test/fixtures/tags/for/loop_context_local.test b/test/fixtures/tags/for/loop_context_local.test
new file mode 100644 (file)
index 0000000..4ec5440
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+"for" tag adds a loop variable to the context locally
+--TEMPLATE--
+{% for item in items %}
+{% endfor %}
+{% if not loop %}WORKS{% endif %}
+--DATA--
+return array('items' => array())
+--EXPECT--
+WORKS
diff --git a/test/fixtures/tags/for/nested_else.test b/test/fixtures/tags/for/nested_else.test
new file mode 100644 (file)
index 0000000..f8b9f6b
--- /dev/null
@@ -0,0 +1,17 @@
+--TEST--
+"for" tag can use an "else" clause
+--TEMPLATE--
+{% for item in items %}
+  {% for item in items1 %}
+    * {{ item }}
+  {% else %}
+    no {{ item }}
+  {% endfor %}
+{% else %}
+  no item1
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'), 'items1' => array())
+--EXPECT--
+no a
+        no b
diff --git a/test/fixtures/tags/for/objects.test b/test/fixtures/tags/for/objects.test
new file mode 100644 (file)
index 0000000..e812eb6
--- /dev/null
@@ -0,0 +1,34 @@
+--TEST--
+"for" tag iterates over iterable objects
+--TEMPLATE--
+{% for item in items %}
+  * {{ item }}
+{% endfor %}
+
+{% for key, value in items|items %}
+  * {{ key }}/{{ value }}
+{% endfor %}
+
+{% for key in items|keys %}
+  * {{ key }}
+{% endfor %}
+--DATA--
+class ItemsIterator implements Iterator
+{
+  protected $values = array('foo' => 'bar', 'bar' => 'foo');
+  public function current() { return current($this->values); }
+  public function key() { return key($this->values); }
+  public function next() { return next($this->values); }
+  public function rewind() { return reset($this->values); }
+  public function valid() { return false !== current($this->values); }
+}
+return array('items' => new ItemsIterator())
+--EXPECT--
+  * bar
+  * foo
+
+  * foo/bar
+  * bar/foo
+
+  * foo
+  * bar
diff --git a/test/fixtures/tags/for/recursive.test b/test/fixtures/tags/for/recursive.test
new file mode 100644 (file)
index 0000000..8276821
--- /dev/null
@@ -0,0 +1,18 @@
+--TEST--
+"for" tags can be nested
+--TEMPLATE--
+{% for key, item in items|items %}
+* {{ key }} ({{ loop.length }}):
+{% for value in item %}
+  * {{ value }} ({{ loop.length }})
+{% endfor %}
+{% endfor %}
+--DATA--
+return array('items' => array('a' => array('a1', 'a2', 'a3'), 'b' => array('b1')))
+--EXPECT--
+* a (2):
+  * a1 (3)
+  * a2 (3)
+  * a3 (3)
+* b (2):
+  * b1 (1)
diff --git a/test/fixtures/tags/for/values.test b/test/fixtures/tags/for/values.test
new file mode 100644 (file)
index 0000000..82f2ae8
--- /dev/null
@@ -0,0 +1,11 @@
+--TEST--
+"for" tag iterates over item values
+--TEMPLATE--
+{% for item in items %}
+  * {{ item }}
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+  * a
+  * b
diff --git a/test/fixtures/tags/if/basic.test b/test/fixtures/tags/if/basic.test
new file mode 100644 (file)
index 0000000..2482ddf
--- /dev/null
@@ -0,0 +1,22 @@
+--TEST--
+"if" creates a condition
+--TEMPLATE--
+{% if a %}
+  {{ a }}
+{% elseif b %}
+  {{ b }}
+{% else %}
+  NOTHING
+{% endif %}
+--DATA--
+return array('a' => 'a')
+--EXPECT--
+  a
+--DATA--
+return array('b' => 'b')
+--EXPECT--
+  b
+--DATA--
+return array()
+--EXPECT--
+  NOTHING
diff --git a/test/fixtures/tags/if/expression.test b/test/fixtures/tags/if/expression.test
new file mode 100644 (file)
index 0000000..edfb73d
--- /dev/null
@@ -0,0 +1,22 @@
+--TEST--
+"if" takes an expression as a test
+--TEMPLATE--
+{% if a < 2 %}
+  A1
+{% elseif a > 10 %}
+  A2
+{% else %}
+  A3
+{% endif %}
+--DATA--
+return array('a' => 1)
+--EXPECT--
+  A1
+--DATA--
+return array('a' => 12)
+--EXPECT--
+  A2
+--DATA--
+return array('a' => 7)
+--EXPECT--
+  A3
diff --git a/test/fixtures/tags/include/basic.test b/test/fixtures/tags/include/basic.test
new file mode 100644 (file)
index 0000000..1f52614
--- /dev/null
@@ -0,0 +1,16 @@
+--TEST--
+"include" tag
+--TEMPLATE--
+FOO
+{% include "%prefix%foo.twig" %}
+
+BAR
+--TEMPLATE(foo.twig)--
+FOOBAR
+--DATA--
+return array()
+--EXPECT--
+FOO
+
+FOOBAR
+BAR
diff --git a/test/fixtures/tags/include/expression.test b/test/fixtures/tags/include/expression.test
new file mode 100644 (file)
index 0000000..df40e2a
--- /dev/null
@@ -0,0 +1,16 @@
+--TEST--
+"include" tag allows expressions for the template to include
+--TEMPLATE--
+FOO
+{% include "%prefix%" ~ foo %}
+
+BAR
+--TEMPLATE(foo.twig)--
+FOOBAR
+--DATA--
+return array('foo' => 'foo.twig')
+--EXPECT--
+FOO
+
+FOOBAR
+BAR
diff --git a/test/fixtures/tags/inheritance/basic.test b/test/fixtures/tags/inheritance/basic.test
new file mode 100644 (file)
index 0000000..a9df3df
--- /dev/null
@@ -0,0 +1,14 @@
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends "%prefix%foo.twig" %}
+
+{% block content %}
+FOO
+{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+FOO
diff --git a/test/fixtures/tags/inheritance/parent.test b/test/fixtures/tags/inheritance/parent.test
new file mode 100644 (file)
index 0000000..b2e5daf
--- /dev/null
@@ -0,0 +1,12 @@
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends "%prefix%foo.twig" %}
+
+{% block content %}{% parent %}FOO{% parent %}{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}BAR{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+BARFOOBAR
diff --git a/test/lib/Twig_Loader_Var.php b/test/lib/Twig_Loader_Var.php
new file mode 100644 (file)
index 0000000..c2a5b9c
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Loads a template from variables.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Loader_Var extends Twig_Loader
+{
+  protected $templates;
+  protected $prefix;
+
+  public function __construct(array $templates, $prefix)
+  {
+    $this->prefix = $prefix;
+    $this->templates = array();
+    foreach ($templates as $name => $template)
+    {
+      $this->templates[$this->prefix.'_'.$name] = $template;
+    }
+  }
+
+  public function getSource($name)
+  {
+    if (!isset($this->templates[$this->prefix.'_'.$name]))
+    {
+      throw new LogicException(sprintf('Template "%s" is not defined.', $name));
+    }
+
+    return array(str_replace('%prefix%', $this->prefix.'_', $this->templates[$this->prefix.'_'.$name]), false);
+  }
+}
diff --git a/test/unit/Twig/AutoloaderTest.php b/test/unit/Twig/AutoloaderTest.php
new file mode 100644 (file)
index 0000000..d79c7c8
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+require_once(dirname(__FILE__).'/../../lib/lime/LimeAutoloader.php');
+LimeAutoloader::register();
+
+require_once dirname(__FILE__).'/../../../lib/Twig/Autoloader.php';
+Twig_Autoloader::register();
+
+$t = new LimeTest(3);
+
+// ->autoload()
+$t->diag('->autoload()');
+
+$t->ok(!class_exists('Foo'), '->autoload() does not try to load classes that does not begin with Twig');
+
+$autoloader = new Twig_Autoloader();
+$t->is($autoloader->autoload('Twig_Parser'), true, '->autoload() returns true if it is able to load a class');
+$t->is($autoloader->autoload('Foo'), false, '->autoload() returns false if it is not able to load a class');
diff --git a/test/unit/Twig/Extension/Sandbox.php b/test/unit/Twig/Extension/Sandbox.php
new file mode 100644 (file)
index 0000000..64775d0
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+require_once(dirname(__FILE__).'/../../../lib/lime/LimeAutoloader.php');
+LimeAutoloader::register();
+
+require_once dirname(__FILE__).'/../../../../lib/Twig/Autoloader.php';
+Twig_Autoloader::register();
+
+require_once dirname(__FILE__).'/../../../lib/Twig_Loader_Var.php';
+
+class Object
+{
+  public function foo()
+  {
+    return 'foo';
+  }
+}
+
+$params = array(
+  'name' => 'Fabien',
+  'obj'  => new Object(),
+);
+$templates = array(
+  '1_basic1' => '{{ obj.foo }}',
+  '1_basic2' => '{{ name|upper }}',
+  '1_basic3' => '{% if name %}foo{% endif %}',
+  '1_basic'  => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}',
+);
+
+$t = new LimeTest(9);
+
+$t->diag('Sandbox globally set');
+$twig = get_environment(false, $templates);
+$t->is($twig->loadTemplate('1_basic')->render($params), 'FOO', 'Sandbox does nothing if it is disabled globally');
+
+$twig = get_environment(true, $templates);
+try
+{
+  $twig->loadTemplate('1_basic1')->render($params);
+  $t->fail('Sandbox throws a SecurityError exception if an unallowed method is called');
+}
+catch (Twig_Sandbox_SecurityError $e)
+{
+  $t->pass('Sandbox throws a SecurityError exception if an unallowed method is called');
+}
+
+$twig = get_environment(true, $templates);
+try
+{
+  $twig->loadTemplate('1_basic2')->render($params);
+  $t->fail('Sandbox throws a SecurityError exception if an unallowed filter is called');
+}
+catch (Twig_Sandbox_SecurityError $e)
+{
+  $t->pass('Sandbox throws a SecurityError exception if an unallowed filter is called');
+}
+
+$twig = get_environment(true, $templates);
+try
+{
+  $twig->loadTemplate('1_basic3')->render($params);
+  $t->fail('Sandbox throws a SecurityError exception if an unallowed tag is used in the template');
+}
+catch (Twig_Sandbox_SecurityError $e)
+{
+  $t->pass('Sandbox throws a SecurityError exception if an unallowed tag is used in the template');
+}
+
+$twig = get_environment(true, $templates, array(), array(), array('Object' => 'foo'));
+$t->is($twig->loadTemplate('1_basic1')->render($params), 'foo', 'Sandbox allow some methods');
+
+$twig = get_environment(true, $templates, array(), array('upper'));
+$t->is($twig->loadTemplate('1_basic2')->render($params), 'FABIEN', 'Sandbox allow some filters');
+
+$twig = get_environment(true, $templates, array('if'));
+$t->is($twig->loadTemplate('1_basic3')->render($params), 'foo', 'Sandbox allow some tags');
+
+$t->diag('Sandbox locally set for an include');
+
+$templates = array(
+  '2_basic'    => '{{ obj.foo }}{% include "2_included" %}{{ obj.foo }}',
+  '2_included' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}',
+);
+
+$twig = get_environment(false, $templates);
+$t->is($twig->loadTemplate('2_basic')->render($params), 'fooFOOfoo', 'Sandbox does nothing if disabled globally and sandboxed not used for the include');
+
+$templates = array(
+  '3_basic'    => '{{ obj.foo }}{% include "3_included" sandboxed %}{{ obj.foo }}',
+  '3_included' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}',
+);
+
+$twig = get_environment(false, $templates);
+$twig = get_environment(true, $templates);
+try
+{
+  $twig->loadTemplate('3_basic')->render($params);
+  $t->fail('Sandbox throws a SecurityError exception when the included file is sandboxed');
+}
+catch (Twig_Sandbox_SecurityError $e)
+{
+  $t->pass('Sandbox throws a SecurityError exception when the included file is sandboxed');
+}
+
+
+function get_environment($sandboxed, $templates, $tags = array(), $filters = array(), $methods = array())
+{
+  static $prefix = 0;
+
+  ++$prefix;
+
+  $loader = new Twig_Loader_Var($templates, $prefix);
+  $twig = new Twig_Environment($loader, array('trim_blocks' => true, 'debug' => true));
+  $policy = new Twig_Sandbox_SecurityPolicy($tags, $filters, $methods);
+  $twig->addExtension(new Twig_Extension_Sandbox($policy, $sandboxed));
+
+  return $twig;
+}
diff --git a/test/unit/integrationTest.php b/test/unit/integrationTest.php
new file mode 100644 (file)
index 0000000..3f72464
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+require_once(dirname(__FILE__).'/../lib/lime/LimeAutoloader.php');
+LimeAutoloader::register();
+
+require_once dirname(__FILE__).'/../../lib/Twig/Autoloader.php';
+Twig_Autoloader::register();
+
+require_once dirname(__FILE__).'/../lib/Twig_Loader_Var.php';
+
+$t = new LimeTest(42);
+$fixturesDir = realpath(dirname(__FILE__).'/../fixtures/');
+
+foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fixturesDir), RecursiveIteratorIterator::LEAVES_ONLY) as $file)
+{
+  if (!preg_match('/\.test$/', $file))
+  {
+    continue;
+  }
+
+  $test = file_get_contents($file->getRealpath());
+
+  if (!preg_match('/--TEST--\s*(.*?)\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)--DATA--.*?--EXPECT--.*/s', $test, $match))
+  {
+    throw new InvalidArgumentException(sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file)));
+  }
+
+  $prefix = rand(1, 999999999);
+  $message = $match[1];
+  $templates = array();
+  preg_match_all('/--TEMPLATE(?:\((.*?)\))?--(.*?)(?=\-\-TEMPLATE|$)/s', $match[2], $matches, PREG_SET_ORDER);
+  foreach ($matches as $match)
+  {
+    $templates[$prefix.'_'.($match[1] ? $match[1] : 'index.twig')] = $match[2];
+  }
+
+  $loader = new Twig_Loader_Var($templates, $prefix);
+  $twig = new Twig_Environment($loader, array('trim_blocks' => true));
+  $twig->addExtension(new Twig_Extension_Escaper());
+
+  $template = $twig->loadTemplate($prefix.'_index.twig');
+
+  preg_match_all('/--DATA--(.*?)--EXPECT--(.*?)(?=\-\-DATA\-\-|$)/s', $test, $matches, PREG_SET_ORDER);
+  foreach ($matches as $match)
+  {
+    $output = trim($template->render(eval($match[1].';')), "\n ");
+    $expected = trim($match[2], "\n ");
+
+    $t->is($output, $expected, $message);
+    if ($output != $expected)
+    {
+      $t->comment('Compiled template that failed:');
+
+      foreach (array_keys($templates) as $name)
+      {
+        list($source, ) = $loader->getSource($name);
+        $t->comment($twig->compile($twig->parse($twig->tokenize($source))));
+      }
+    }
+  }
+}