From: fabien Date: Wed, 7 Oct 2009 21:21:11 +0000 (+0000) Subject: initial commit X-Git-Url: http://git.silmor.de/gitweb/?a=commitdiff_plain;h=fd0646e4cfef27c6325da678863764f2f1aaa7f9;p=web%2Fkonrad%2Ftwig.git initial commit git-svn-id: http://svn.twig-project.org/trunk@4 93ef8e89-cb99-4229-a87c-7fa0fa45744b --- diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..eb5db05 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,9 @@ +Twig is written and maintained by the Twig Team: + +Lead Developer: + +- Fabien Potencier + +Project Founder: + +- Armin Ronacher diff --git a/LICENSE b/LICENSE new file mode 100644 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 index 0000000..3c77450 --- /dev/null +++ b/README.markdown @@ -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 index 0000000..5b3d72c --- /dev/null +++ b/doc/01-Introduction.markdown @@ -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 index 0000000..71444b0 --- /dev/null +++ b/doc/02-Twig-for-Template-Designers.markdown @@ -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] + + + + My Webpage + + + + +

My Webpage

+ {{ a_variable }} + + + +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 %} +
    + {% for item in seq %} +
  • {{ item }}
  • + {% endfor %} +
+ {% 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] + + + + + {% block head %} + + {% block title %}{% endblock %} - My Webpage + {% endblock %} + + +
{% block content %}{% endblock %}
+ + + +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 %} + + {% endblock %} + {% block content %} +

Index

+

+ Welcome on my awesome homepage. +

+ {% 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] + {% block title %}{% endblock %} +

{% display title %}

+ {% 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 %} +

Table Of Contents

+ ... + {% 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 %} +
  • {% block loop_item %}{{ item }}{% endblock %}
  • + {% 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] +

    Members

    +
      + {% for user in users %} +
    • {{ user.username|e }}
    • + {% endfor %} +
    + +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] +
      + {% for user in users %} +
    • {{ user.username|e }}
    • + {% else %} +
    • no users found
    • + {% endif %} +
    + +### 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 %} +
      + {% for user in users %} +
    • {{ user.username|e }}
    • + {% endfor %} +
    + {% 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 index 0000000..ce844c4 --- /dev/null +++ b/doc/03-Twig-for-Developers.markdown @@ -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 index 0000000..59676cb --- /dev/null +++ b/doc/04-Extending-Twig.markdown @@ -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 index 0000000..74afd0d --- /dev/null +++ b/doc/05-Hacking-Twig.markdown @@ -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 index 0000000..bb30b6b --- /dev/null +++ b/lib/Twig/Autoloader.php @@ -0,0 +1,48 @@ + + * @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 index 0000000..552febe --- /dev/null +++ b/lib/Twig/Compiler.php @@ -0,0 +1,245 @@ + + * @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 index 0000000..365b59f --- /dev/null +++ b/lib/Twig/CompilerInterface.php @@ -0,0 +1,36 @@ + + * @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 index 0000000..c807a97 --- /dev/null +++ b/lib/Twig/Environment.php @@ -0,0 +1,251 @@ +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 index 0000000..b88fee4 --- /dev/null +++ b/lib/Twig/Error.php @@ -0,0 +1,21 @@ + + * @version SVN: $Id$ + */ +class Twig_Error extends Exception +{ +} diff --git a/lib/Twig/ExpressionParser.php b/lib/Twig/ExpressionParser.php new file mode 100644 index 0000000..6cb5441 --- /dev/null +++ b/lib/Twig/ExpressionParser.php @@ -0,0 +1,397 @@ + + * @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 index 0000000..82046e9 --- /dev/null +++ b/lib/Twig/Extension.php @@ -0,0 +1,51 @@ + 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 index 0000000..c6ac240 --- /dev/null +++ b/lib/Twig/Extension/Escaper.php @@ -0,0 +1,76 @@ +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 index 0000000..70ed24e --- /dev/null +++ b/lib/Twig/Extension/Macro.php @@ -0,0 +1,25 @@ +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 index 0000000..2524575 --- /dev/null +++ b/lib/Twig/Extension/Set.php @@ -0,0 +1,16 @@ + + * @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 index 0000000..5459736 --- /dev/null +++ b/lib/Twig/Lexer.php @@ -0,0 +1,304 @@ + + * @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 index 0000000..4ec1985 --- /dev/null +++ b/lib/Twig/LexerInterface.php @@ -0,0 +1,30 @@ + + * @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 index 0000000..a90b97c --- /dev/null +++ b/lib/Twig/Loader.php @@ -0,0 +1,111 @@ + + * @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 index 0000000..67068aa --- /dev/null +++ b/lib/Twig/Loader/Array.php @@ -0,0 +1,41 @@ + + * @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 index 0000000..d1394a2 --- /dev/null +++ b/lib/Twig/Loader/Filesystem.php @@ -0,0 +1,56 @@ + + * @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 index 0000000..c135fe4 --- /dev/null +++ b/lib/Twig/Loader/String.php @@ -0,0 +1,34 @@ + + * @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 index 0000000..6780003 --- /dev/null +++ b/lib/Twig/LoaderInterface.php @@ -0,0 +1,29 @@ + + * @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 index 0000000..366617e --- /dev/null +++ b/lib/Twig/Node.php @@ -0,0 +1,47 @@ + + * @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 index 0000000..5eefbd9 --- /dev/null +++ b/lib/Twig/Node/AutoEscape.php @@ -0,0 +1,55 @@ + + * @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 index 0000000..8b5a64a --- /dev/null +++ b/lib/Twig/Node/Block.php @@ -0,0 +1,88 @@ + + * @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 index 0000000..3fadb78 --- /dev/null +++ b/lib/Twig/Node/BlockReference.php @@ -0,0 +1,47 @@ + + * @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 index 0000000..1c4f1a3 --- /dev/null +++ b/lib/Twig/Node/Call.php @@ -0,0 +1,45 @@ + + * @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 index 0000000..938966c --- /dev/null +++ b/lib/Twig/Node/Expression.php @@ -0,0 +1,22 @@ + + * @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 index 0000000..eec3f99 --- /dev/null +++ b/lib/Twig/Node/Expression/AssignName.php @@ -0,0 +1,19 @@ +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 index 0000000..dbe6757 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary.php @@ -0,0 +1,61 @@ +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 index 0000000..63bc0a2 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/Add.php @@ -0,0 +1,18 @@ +raw('+'); + } +} diff --git a/lib/Twig/Node/Expression/Binary/And.php b/lib/Twig/Node/Expression/Binary/And.php new file mode 100644 index 0000000..1fe4bc3 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/And.php @@ -0,0 +1,18 @@ +raw('&&'); + } +} diff --git a/lib/Twig/Node/Expression/Binary/Concat.php b/lib/Twig/Node/Expression/Binary/Concat.php new file mode 100644 index 0000000..61e6954 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/Concat.php @@ -0,0 +1,18 @@ +raw('.'); + } +} diff --git a/lib/Twig/Node/Expression/Binary/Div.php b/lib/Twig/Node/Expression/Binary/Div.php new file mode 100644 index 0000000..7361b73 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/Div.php @@ -0,0 +1,18 @@ +raw('/'); + } +} diff --git a/lib/Twig/Node/Expression/Binary/Mod.php b/lib/Twig/Node/Expression/Binary/Mod.php new file mode 100644 index 0000000..e2c9117 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/Mod.php @@ -0,0 +1,18 @@ +raw('%'); + } +} diff --git a/lib/Twig/Node/Expression/Binary/Mul.php b/lib/Twig/Node/Expression/Binary/Mul.php new file mode 100644 index 0000000..a236101 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/Mul.php @@ -0,0 +1,18 @@ +raw('*'); + } +} diff --git a/lib/Twig/Node/Expression/Binary/Or.php b/lib/Twig/Node/Expression/Binary/Or.php new file mode 100644 index 0000000..a968b82 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/Or.php @@ -0,0 +1,18 @@ +raw('||'); + } +} diff --git a/lib/Twig/Node/Expression/Binary/Sub.php b/lib/Twig/Node/Expression/Binary/Sub.php new file mode 100644 index 0000000..f67117b --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/Sub.php @@ -0,0 +1,18 @@ +raw('-'); + } +} diff --git a/lib/Twig/Node/Expression/Compare.php b/lib/Twig/Node/Expression/Compare.php new file mode 100644 index 0000000..71b8e4d --- /dev/null +++ b/lib/Twig/Node/Expression/Compare.php @@ -0,0 +1,57 @@ +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 index 0000000..d89d639 --- /dev/null +++ b/lib/Twig/Node/Expression/Conditional.php @@ -0,0 +1,38 @@ +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 index 0000000..976b381 --- /dev/null +++ b/lib/Twig/Node/Expression/Constant.php @@ -0,0 +1,36 @@ +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 index 0000000..0a9fc52 --- /dev/null +++ b/lib/Twig/Node/Expression/Filter.php @@ -0,0 +1,114 @@ +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 index 0000000..1a03623 --- /dev/null +++ b/lib/Twig/Node/Expression/GetAttr.php @@ -0,0 +1,56 @@ +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 index 0000000..439a4b3 --- /dev/null +++ b/lib/Twig/Node/Expression/Name.php @@ -0,0 +1,36 @@ +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 index 0000000..652c7f6 --- /dev/null +++ b/lib/Twig/Node/Expression/Unary.php @@ -0,0 +1,44 @@ +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 index 0000000..7893619 --- /dev/null +++ b/lib/Twig/Node/Expression/Unary/Neg.php @@ -0,0 +1,18 @@ +raw('-'); + } +} diff --git a/lib/Twig/Node/Expression/Unary/Not.php b/lib/Twig/Node/Expression/Unary/Not.php new file mode 100644 index 0000000..2771a23 --- /dev/null +++ b/lib/Twig/Node/Expression/Unary/Not.php @@ -0,0 +1,18 @@ +raw('!'); + } +} diff --git a/lib/Twig/Node/Expression/Unary/Pos.php b/lib/Twig/Node/Expression/Unary/Pos.php new file mode 100644 index 0000000..40cec40 --- /dev/null +++ b/lib/Twig/Node/Expression/Unary/Pos.php @@ -0,0 +1,18 @@ +raw('+'); + } +} diff --git a/lib/Twig/Node/Filter.php b/lib/Twig/Node/Filter.php new file mode 100644 index 0000000..bbc161c --- /dev/null +++ b/lib/Twig/Node/Filter.php @@ -0,0 +1,55 @@ + + * @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 index 0000000..5c2449e --- /dev/null +++ b/lib/Twig/Node/For.php @@ -0,0 +1,102 @@ + + * @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 index 0000000..4cb6869 --- /dev/null +++ b/lib/Twig/Node/If.php @@ -0,0 +1,103 @@ + + * @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 index 0000000..f8f0b84 --- /dev/null +++ b/lib/Twig/Node/Include.php @@ -0,0 +1,73 @@ + + * @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 index 0000000..c5f67f6 --- /dev/null +++ b/lib/Twig/Node/Macro.php @@ -0,0 +1,55 @@ + + * @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 index 0000000..90abdb4 --- /dev/null +++ b/lib/Twig/Node/Module.php @@ -0,0 +1,178 @@ + + * @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("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 index 0000000..0e83be1 --- /dev/null +++ b/lib/Twig/Node/Parent.php @@ -0,0 +1,42 @@ + + * @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 index 0000000..ed34f01 --- /dev/null +++ b/lib/Twig/Node/Print.php @@ -0,0 +1,66 @@ + + * @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 index 0000000..7816242 --- /dev/null +++ b/lib/Twig/Node/Set.php @@ -0,0 +1,27 @@ +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 index 0000000..89dfc5a --- /dev/null +++ b/lib/Twig/Node/Text.php @@ -0,0 +1,49 @@ + + * @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 index 0000000..79c5fbc --- /dev/null +++ b/lib/Twig/NodeList.php @@ -0,0 +1,62 @@ + + * @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 index 0000000..43df815 --- /dev/null +++ b/lib/Twig/NodeListInterface.php @@ -0,0 +1,30 @@ + + * @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 index 0000000..82c4fbc --- /dev/null +++ b/lib/Twig/NodeTransformer.php @@ -0,0 +1,42 @@ +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 index 0000000..6fd6fa0 --- /dev/null +++ b/lib/Twig/NodeTransformer/Chain.php @@ -0,0 +1,39 @@ +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 index 0000000..914d241 --- /dev/null +++ b/lib/Twig/NodeTransformer/Escaper.php @@ -0,0 +1,84 @@ +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 index 0000000..6a5dd8f --- /dev/null +++ b/lib/Twig/NodeTransformer/Filter.php @@ -0,0 +1,68 @@ +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 index 0000000..0b08819 --- /dev/null +++ b/lib/Twig/NodeTransformer/Sandbox.php @@ -0,0 +1,58 @@ +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 index 0000000..def516d --- /dev/null +++ b/lib/Twig/Parser.php @@ -0,0 +1,213 @@ +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 index 0000000..34156e8 --- /dev/null +++ b/lib/Twig/ParserInterface.php @@ -0,0 +1,29 @@ + + * @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 index 0000000..5457f4d --- /dev/null +++ b/lib/Twig/RuntimeError.php @@ -0,0 +1,22 @@ + + * @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 index 0000000..94bdae6 --- /dev/null +++ b/lib/Twig/Sandbox/SecurityError.php @@ -0,0 +1,21 @@ + + * @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 index 0000000..92b3756 --- /dev/null +++ b/lib/Twig/Sandbox/SecurityPolicy.php @@ -0,0 +1,84 @@ + + * @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 index 0000000..afb7e61 --- /dev/null +++ b/lib/Twig/Sandbox/SecurityPolicyInterface.php @@ -0,0 +1,24 @@ + + * @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 index 0000000..865a542 --- /dev/null +++ b/lib/Twig/SyntaxError.php @@ -0,0 +1,51 @@ + + * @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 index 0000000..6154385 --- /dev/null +++ b/lib/Twig/Template.php @@ -0,0 +1,69 @@ +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 index 0000000..ac69fbd --- /dev/null +++ b/lib/Twig/TemplateInterface.php @@ -0,0 +1,18 @@ +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 index 0000000..9fe3dc5 --- /dev/null +++ b/lib/Twig/TokenParser.php @@ -0,0 +1,24 @@ +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 index 0000000..45e514a --- /dev/null +++ b/lib/Twig/TokenParser/AutoEscape.php @@ -0,0 +1,38 @@ +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 index 0000000..a6df2a5 --- /dev/null +++ b/lib/Twig/TokenParser/Block.php @@ -0,0 +1,52 @@ +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 index 0000000..c564fb4 --- /dev/null +++ b/lib/Twig/TokenParser/Call.php @@ -0,0 +1,30 @@ +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 index 0000000..ea201e8 --- /dev/null +++ b/lib/Twig/TokenParser/Display.php @@ -0,0 +1,31 @@ +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 index 0000000..9fa9f77 --- /dev/null +++ b/lib/Twig/TokenParser/Extends.php @@ -0,0 +1,30 @@ +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 index 0000000..ddd7c63 --- /dev/null +++ b/lib/Twig/TokenParser/Filter.php @@ -0,0 +1,34 @@ +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 index 0000000..ced0235 --- /dev/null +++ b/lib/Twig/TokenParser/For.php @@ -0,0 +1,50 @@ +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 index 0000000..adf8f7b --- /dev/null +++ b/lib/Twig/TokenParser/If.php @@ -0,0 +1,65 @@ +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 index 0000000..240d629 --- /dev/null +++ b/lib/Twig/TokenParser/Include.php @@ -0,0 +1,34 @@ +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 index 0000000..45d6567 --- /dev/null +++ b/lib/Twig/TokenParser/Macro.php @@ -0,0 +1,37 @@ +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 index 0000000..9771488 --- /dev/null +++ b/lib/Twig/TokenParser/Parent.php @@ -0,0 +1,29 @@ +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 index 0000000..01f011f --- /dev/null +++ b/lib/Twig/TokenParser/Set.php @@ -0,0 +1,20 @@ +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 index 0000000..7b255d5 --- /dev/null +++ b/lib/Twig/TokenStream.php @@ -0,0 +1,139 @@ +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 index 0000000..0b5dd24 --- /dev/null +++ b/lib/Twig/runtime.php @@ -0,0 +1,158 @@ + $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 index 0000000..b62badd --- /dev/null +++ b/lib/Twig/runtime_escaper.php @@ -0,0 +1,26 @@ +getEnvironment()->getCharset()); +} diff --git a/lib/Twig/runtime_for.php b/lib/Twig/runtime_for.php new file mode 100644 index 0000000..5b33c71 --- /dev/null +++ b/lib/Twig/runtime_for.php @@ -0,0 +1,123 @@ +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 index 0000000..015801a --- /dev/null +++ b/test/bin/coverage.php @@ -0,0 +1,44 @@ + 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 index 0000000..be7bb1b --- /dev/null +++ b/test/bin/prove.php @@ -0,0 +1,28 @@ + 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 index 0000000..a969c5d --- /dev/null +++ b/test/fixtures/expressions/binary.test @@ -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 index 0000000..4ed77e5 --- /dev/null +++ b/test/fixtures/expressions/comparison.test @@ -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 index 0000000..79f8e0b --- /dev/null +++ b/test/fixtures/expressions/grouping.test @@ -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 index 0000000..cbf555c --- /dev/null +++ b/test/fixtures/expressions/ternary_operator.test @@ -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 index 0000000..2e1cb35 --- /dev/null +++ b/test/fixtures/expressions/unary.test @@ -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 index 0000000..c3f8625 --- /dev/null +++ b/test/fixtures/filters/date.test @@ -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 index 0000000..8fd8017 --- /dev/null +++ b/test/fixtures/filters/default.test @@ -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 index 0000000..835bb79 --- /dev/null +++ b/test/fixtures/filters/even.test @@ -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 index 0000000..97221ff --- /dev/null +++ b/test/fixtures/filters/format.test @@ -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 index 0000000..b5ef879 --- /dev/null +++ b/test/fixtures/filters/length.test @@ -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 index 0000000..1de0351 --- /dev/null +++ b/test/fixtures/filters/odd.test @@ -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 index 0000000..21d575f --- /dev/null +++ b/test/fixtures/filters/sort.test @@ -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 index 0000000..6f6c1b9 --- /dev/null +++ b/test/fixtures/tags/autoescape/basic.test @@ -0,0 +1,22 @@ +--TEST-- +"autoescape" tag applies escaping on its children +--TEMPLATE-- +{% autoescape on %} +{{ var }}
    +{% endautoescape %} +{% autoescape off %} +{{ var }}
    +{% endautoescape %} +{% autoescape on %} +{{ var }}
    +{% endautoescape %} +{% autoescape off %} +{{ var }}
    +{% endautoescape %} +--DATA-- +return array('var' => '
    ') +--EXPECT-- +<br />
    +

    +<br />
    +

    diff --git a/test/fixtures/tags/autoescape/double_escaping.test b/test/fixtures/tags/autoescape/double_escaping.test new file mode 100644 index 0000000..fc7aa8c --- /dev/null +++ b/test/fixtures/tags/autoescape/double_escaping.test @@ -0,0 +1,10 @@ +--TEST-- +"autoescape" tag does not double-escape +--TEMPLATE-- +{% autoescape on %} +{{ var|escape }} +{% endautoescape %} +--DATA-- +return array('var' => '
    ') +--EXPECT-- +<br /> diff --git a/test/fixtures/tags/autoescape/nested.test b/test/fixtures/tags/autoescape/nested.test new file mode 100644 index 0000000..49f4da9 --- /dev/null +++ b/test/fixtures/tags/autoescape/nested.test @@ -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' => '
    ') +--EXPECT-- +<br /> + <br /> +
    + <br /> +
    + <br /> +<br /> diff --git a/test/fixtures/tags/autoescape/safe.test b/test/fixtures/tags/autoescape/safe.test new file mode 100644 index 0000000..b8a7a37 --- /dev/null +++ b/test/fixtures/tags/autoescape/safe.test @@ -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' => '
    ') +--EXPECT-- +
    diff --git a/test/fixtures/tags/filter/basic.test b/test/fixtures/tags/filter/basic.test new file mode 100644 index 0000000..82094f2 --- /dev/null +++ b/test/fixtures/tags/filter/basic.test @@ -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 index 0000000..d26bf6e --- /dev/null +++ b/test/fixtures/tags/filter/nested.test @@ -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 index 0000000..22745ea --- /dev/null +++ b/test/fixtures/tags/filter/with_for_tag.test @@ -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 index 0000000..02381dd --- /dev/null +++ b/test/fixtures/tags/filter/with_if_tag.test @@ -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 index 0000000..ddc6930 --- /dev/null +++ b/test/fixtures/tags/for/context.test @@ -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 index 0000000..8c6a7d2 --- /dev/null +++ b/test/fixtures/tags/for/else.test @@ -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 index 0000000..4e22cb4 --- /dev/null +++ b/test/fixtures/tags/for/keys.test @@ -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 index 0000000..c4ab516 --- /dev/null +++ b/test/fixtures/tags/for/keys_and_values.test @@ -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 index 0000000..93bc76a --- /dev/null +++ b/test/fixtures/tags/for/loop_context.test @@ -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 index 0000000..4ec5440 --- /dev/null +++ b/test/fixtures/tags/for/loop_context_local.test @@ -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 index 0000000..f8b9f6b --- /dev/null +++ b/test/fixtures/tags/for/nested_else.test @@ -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 index 0000000..e812eb6 --- /dev/null +++ b/test/fixtures/tags/for/objects.test @@ -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 index 0000000..8276821 --- /dev/null +++ b/test/fixtures/tags/for/recursive.test @@ -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 index 0000000..82f2ae8 --- /dev/null +++ b/test/fixtures/tags/for/values.test @@ -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 index 0000000..2482ddf --- /dev/null +++ b/test/fixtures/tags/if/basic.test @@ -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 index 0000000..edfb73d --- /dev/null +++ b/test/fixtures/tags/if/expression.test @@ -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 index 0000000..1f52614 --- /dev/null +++ b/test/fixtures/tags/include/basic.test @@ -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 index 0000000..df40e2a --- /dev/null +++ b/test/fixtures/tags/include/expression.test @@ -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 index 0000000..a9df3df --- /dev/null +++ b/test/fixtures/tags/inheritance/basic.test @@ -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 index 0000000..b2e5daf --- /dev/null +++ b/test/fixtures/tags/inheritance/parent.test @@ -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 index 0000000..c2a5b9c --- /dev/null +++ b/test/lib/Twig_Loader_Var.php @@ -0,0 +1,43 @@ + + * @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 index 0000000..d79c7c8 --- /dev/null +++ b/test/unit/Twig/AutoloaderTest.php @@ -0,0 +1,27 @@ +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 index 0000000..64775d0 --- /dev/null +++ b/test/unit/Twig/Extension/Sandbox.php @@ -0,0 +1,127 @@ + '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 index 0000000..3f72464 --- /dev/null +++ b/test/unit/integrationTest.php @@ -0,0 +1,70 @@ +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)))); + } + } + } +}